Search code examples
javascriptnode.jssails.jsfacebook-messengermessenger

Facebook messenger API incomplete content in request body


The explanation is a bit long so please bear with me.

I am building a Facebook messenger bot which uses my sails.js/node.js server in the backend and a MongoDB database.

In my sails app, I have applied policies to the method of the controller which handles the operations to be performed after recieving a text from the user. In this policy, I am following the documentation(https://developers.facebook.com/docs/messenger-platform/webhook-reference - "Security" section) and comparing the x-hub-signature that comes in the request's header with the sha1 digest of the request payload(body).

So now whenever I am sending a message to the bot, it says in the policy that the signature from the request and the signature calculated by me is different and thus, doesnt go further. I double checked the app secret which I should use while calculating the digest and it seems to be correct. Another difference which I found was that, Facebook request also sends a "content-length" field in its header, which is different than character length of the body they sent in the same request. And this is what I think is the reason for different signatures but I am unable to resolve it and get to the root of the problem as to why is this happening.

Also another thing to note is that the same code that throws this mismatch error, runs perfectly at certain times(actually, most of the times).

So can somebody please help me this? I ll be forever grateful :)

Here is the code from the policy

var crypto = require('crypto');
if(req.headers['x-hub-signature']){
    //console.log('req headers -----', JSON.stringify(req.headers));
    //console.log('req body -----', JSON.stringify(req.body));

    var hmac, calculatedSignature, payload = req.body;
    hmac = crypto.createHmac('sha1', app_secret);
    hmac.update(JSON.stringify(payload));
    calculatedSignature = 'sha1='+hmac.digest('hex');

    //console.log("signature calculatedSignature",calculatedSignature);
    if(calculatedSignature === req.headers['x-hub-signature']){
        return next();
    }else{
        res.forbidden('You shall not pass!');
    }
}

This is a sample request header -

{"host":"e93d4245id.ngrok.io","accept":"*/*","accept-encoding":"deflate, gzip","content-type":"application/json","x-hub-signature":"sha1=d0cd8177add9b1ff367d411942603b0d08183964","content-length":"274","x-forwarded-proto":"https","x-forwarded-for":"127.0.0.1"}

And this is the body from the same request -

{"object":"page","entry":[{"id":"1778585282425767","time":1479476014038,"messaging":[{"sender":{"id":"userId"},"recipient":{"id":"recipientId"},"timestamp":1479468097895,"message":{"mid":"mid.1479468097895:efdc7d2c68","seq":2355,"text":"Hahahaha"}}]}]}

Solution

  • I think the problem was some specific characters such as @ and % were needed to be converted to their unicode escape sequence as specified in their documentation and replaced in the original stringified JSON. I converted them and then calculated the hmac signature of the new string and it got matched.

    Also the reason why it was working and why it was not in certain cases was I think because of the special characters being present in that string which was being stringified. If it did not have the characters @ or % then it worked without any problem.

    This is how I solved it - inside if var hmac, calculatedSignature, payload = JSON.stringify(req.body);

        var resStr = payload.replace(/\@|\%/g,function(a, i){
            hex = payload.charCodeAt(i).toString(16);
            var s = "\\u" + ("000"+hex).slice(-4);
            return s;
        });
    
        hmac = crypto.createHmac('sha1', app_secret);
        hmac.update(resStr);
        calculatedSignature = 'sha1='+hmac.digest('hex');
    
        if(calculatedSignature === req.headers['x-hub-signature']){
            return next();
        }else{
            res.forbidden('You shall not pass!');
        }