Search code examples
javascriptfacebooksha256hmac

Validate Facebook signed_request signature in Javascript


I'm building a Facebook Page app in Classic ASP. I've been unable to match the signature that Facebook passes into the app as the first part of the POSTed signed_request.

Because there are few libraries for cryptography in VBScript, I'm using server side Javascript and the crypto-js library from https://code.google.com/archive/p/crypto-js/

I've tried to translate the PHP code example from Facebook's docs at https://developers.facebook.com/docs/games/gamesonfacebook/login#parsingsr into Javascript. I can generate an HMAC SHA256 hash of the signed_request payload but that doesn't match the signed_request signature.

I think the problem is that Facebook's signature is in a different format. It looks to be binary (~1抚Ö.....) while the HMAC SHA256 hash I'm generating is a hexadecimal string (7f7e8f5f.....). In Facebook's PHP example the hash_hmac function uses the raw binary parameter. So I think I need to either convert Facebook's signature to hexadecimal or my signature to binary in order to do an "apples-to-apples" comparison and get a match.

Here's my code:

/* Use the libraries from https://code.google.com/archive/p/crypto-js/
crypto-js/crypto-js.min.js
crypto-js/hmac-sha256.min.js
crypto-js/enc-base64.min.js
*/

var signedRequest = Request.queryString("signed_request")

var FB_APP_SECRET = "459f038.....";

var arSR = signedRequest.split(".");
var encodedSig = arSR[0];
var encodedPayload = arSR[1];

var payload = base64UrlDecode(encodedPayload);
var sig = base64UrlDecode(encodedSig);

var expectedSig;

expectedSig = CryptoJS.HmacSHA256(encodedPayload, FB_APP_SECRET); // Unaltered payload string; no match
expectedSig = CryptoJS.HmacSHA256(payload, FB_APP_SECRET); // base64-decoded payload string; no match

if (sig == expectedSig) {
    Response.write(payload);
} else {
    Response.write("Bad signature");
}

function base64UrlDecode(input) {
    // Replace characters and convert from base64.
    return Base64.decode(input.replace("-", "+").replace("_", "/"));
}

Solution

  • After looking into the crypto-js documentation about encoding I found the solution. The de-/encoding methods provided by crypto-js are listed under 'Encoders' at the bottom of https://code.google.com/archive/p/crypto-js/ (Thanks for the nudge, CBroe.)

    The solution was to use .toString() on the signatures. It seems like crypto-js uses a word format that was preventing a comparison match. I did also switch to using the base64 decoding provided by crypto-js in order to stick with one library.

    Here's my updated code:

    /* Use the libraries from https://code.google.com/archive/p/crypto-js/
    crypto-js/crypto-js.min.js
    crypto-js/hmac-sha256.min.js
    crypto-js/enc-base64.min.js
    */
    
    var signedRequest = Request.queryString("signed_request")
    
    var FB_APP_SECRET = "459f038.....";
    
    var arSR = signedRequest.split(".");
    var encodedSig = arSR[0];
    var encodedPayload = arSR[1];
    
    var payload = base64UrlDecode(encodedPayload);
    var sig = base64UrlDecode(encodedSig);
    
    var expectedSig = CryptoJS.HmacSHA256(encodedPayload, FB_APP_SECRET); /******** Correct payload */
    
    if (sig.toString() != expectedSig.toString()) { /******* Use .toString() to convert to normal strings */
        Response.write(payload);
    } else {
        Response.write("Bad signature");
    }
    
    function base64UrlDecode(input) {
        return CryptoJS.enc.Base64.parse( /******** Decode */
            input.replace("-", "+").replace("_", "/") // Replace characters
        );
    }