Search code examples
pythonamazon-web-servicesaws-lambdasha256facebook-webhooks

How to validate payload for facebook whatsapp webhook in AWS Lambda?


I am trying to validate facebook's webhook payload using the instruction they have given in their developer docs. The signature I am generating (expectedHash) is not matching the signature that I am receiving from Facebook (signatureHash). I think I am following what they are saying but I am doing something wrong which I cannot pinpoint yet.

Validating Payloads

We sign all Event Notification payloads with a SHA256 signature and include the signature in the request's X-Hub-Signature-256 header, preceded with sha256=. You don't have to validate the payload, but you should.

To validate the payload:

Generate a SHA256 signature using the payload and your app's App Secret.
Compare your signature to the signature in the X-Hub-Signature-256 header (everything after sha256=). 
If the signatures match, the payload is genuine.

Please note that we generate the signature using an escaped unicode version of the payload, with lowercase hex digits. If you just calculate against the decoded bytes, you will end up with a different signature. For example, the string äöå should be escaped to \u00e4\u00f6\u00e5.

Below is my code in lambda

def lambda_handler(event, context):

response = {
    "status": 500,
    "body" : "failed"
}

print("event is")
print(event)

signature = event["headers"]["X-Hub-Signature-256"]
if(not signature):
    return(f"couldn't find {signature} in headers")
else:
    elements = signature.split("=")
    print("elements is")
    print(elements)
    
    signatureHash = elements[1]
    print("signature hash is " + str(signatureHash))
    
    app_secret = os.environ.get('APP_SECRET')
    print("app_secret is " + str(app_secret)) 
    
    
    expectedHash = hmac.new(bytes(app_secret,'utf-8') ,digestmod=hashlib.sha256).hexdigest()
    print("expected hash is " + expectedHash)
    
    if(signatureHash != expectedHash):
        return response
    else:
        response["status"] = 200
        response["body"] = expectedHash
        return response

response I am getting is:

{ "status": 500, "body": "failed" }

expected response:

{ "status": 200, "body": value of expectedHash }

Could you please help me with this?

Edit 1:

Figured out how to do it.

Apparently I was using a wrong content mapping in AWS API Gateway. I needed to use the $input.body to get the raw payload data in the event argument of AWS lambda handler function. My content mapping looks like this:

#set($allParams = $input.params())
{
    "method": "$context.httpMethod",
    "params" : {
        #foreach($type in $allParams.keySet())
        #set($params = $allParams.get($type))
        "$type" : {
              #foreach($paramName in $params.keySet())
              "$paramName" : "$util.escapeJavaScript($params.get($paramName))"
              #if($foreach.hasNext),#end
              #end
        }
        #if($foreach.hasNext),#end
        #end
    },
    
    "body" : $input.body
    
}

Below is my lambda handler function for validating payload:

def lambda_handler(event, context):

response = {
    "status": 500,
    "body" : "failed"
}

print("event is")
print(event)

signature = event["params"]["header"]["X-Hub-Signature-256"]

if(not signature):
    return(f"couldn't find {signature} in headers")
else:
    try:
        elements = signature.split("=")
        print("elements is")
        print(elements)
        
        signatureHash = elements[1]
        #print("signature hash is " + str(signatureHash))
        
        app_secret = os.environ.get('APP_SECRET') 
        
        key = bytes(app_secret, 'UTF-8')
        payload = event['body']
        json_string = json.dumps(payload)
        print("payload json_string is " + json_string)
    
        expectedHash = hmac.new(key, msg=json_string.encode(), digestmod=hashlib.sha256).hexdigest()
        
        print("expected hash is " + expectedHash)
        
        if(signatureHash != expectedHash):
            print(response)
            return response
        else:
            response["status"] = 200
            response["body"] = expectedHash
            print(response)
            return response
    except Exception as e:
        return e

As of 12/14/2022, the above function works for all webhook fields except messages (which is the one I really need). Trying to figure it out.


Solution

  • This is your code but using Lambda Proxy Integration, so event keys are a bit different, event["body"], is a raw string, then you can parse it to get the elements you need from it, i think that is easier than all the mapping stuff without the lambda proxy:

    import os
    import json
    import hmac
    import hashlib
    
    def lambda_handler(event, context):
    
        response = {
            'statusCode': '200',
            'body' : "OK"
        }
        
        
        print("event is")
        print(event)
    
        
        signature = event["headers"]["X-Hub-Signature-256"]
        
        if(not signature):
            response["body"] =  (f"couldn't find {signature} in headers")
            return response
        else:
            try:
                elements = signature.split("=")
                print("elements is")
                print(elements)
                
                signatureHash = elements[1]
                #print("signature hash is " + str(signatureHash))
                
                app_secret = os.environ.get('APP_SECRET') 
                
                key = bytes(app_secret, 'UTF-8')
                payload = event['body']
                #json_string = json.dumps(payload)
                #print("payload json_string is " + json_string)
            
                expectedHash = hmac.new(key, msg=bytes(payload,'UTF-8'), digestmod=hashlib.sha256).hexdigest()
                
                print("expected hash is " + expectedHash)
                
                if(signatureHash != expectedHash):
                    response["body"] = "eh " + expectedHash + " sh " + signatureHash
                    print(response)
                    return response
                else:
                    response["statusCode"] = 200
                    response["body"] = "Check ok"
                    print(response)
                    return response
            except Exception as err:
                response["body"] = f"Unexpected {err=}, {type(err)=}"
                return response