How to check a request authenticity made to a webhook
When subscribing to a webhook, Twitch suggests that we should pass a
hub.secret param to the request body to authenticate the post requests done on our callback URL.
This is important because if someone finds your callback URL, they can make a post request with the same structure as Twitch and trick you to think an event happened when it didn’t.
hub.secret, twitch will hash the payload with your secret, allowing you to check the authenticity of the request and confirm that it was sent by Twitch and not some dodgy third-party.
Checking Twitch documentation on webhooks you can see that Twitch will use your secret to sign the payload and will include an
X-Hub-Signature to the header of the request, this signature is generated by
When Twitch sends the request you will have to use your
hub.secret together with the payload received from Twitch to get the sha256 hash. Then you need to compare if this hash is equal to the one received on the
Note that the
X-Hub-Signature has the following format
With this information, we can build our validation checker function to see if the request made to our URL did come from Twitch or not. The thing that we have to keep in mind is that we will always work with bytes.
I’m using opsdroid to connect to Twitch and subscribe to webhooks. Opsdroid uses AIOHTTP to create the web server as well to handle requests.
In aiohttp we can do
await request.read() to read the response as bytes, then we just need to create our sha256 hash with this data together with the secret that we passed to
hub.secret when subscribed to the webhook.
We can achieve this with the
1import hashlib23async def validate_request(request, secret):4 # Get x-hub-signature from header and remove 'sha256=' from it5 signature = request.headers.get("x-hub-signature").replace("sha256=", '')67 payload = await request.read()8 validator = hashlib.sha256()910 # Add payload together with secret encoded to bytes11 validator.update(payload + secret.encode())1213 # Get hex representation14 representation = validate.hexdigest()1516 return signature == representation
This function will achieve what we want but seems a bit messy and we could perhaps refactor it a bit more to make it more readable. We can use the
hmac module to pass the payload and the secret as parameters and get the hex representation of our hash.
1async def validate_request(request, secret):2 signature = request.headers.get("x-hub-signature").replace("sha256=", '')34 payload = await request.read()56 computed_hash = hmac.new(secret.encode(), msg=payload, digestmod=hashlib.sha256).hexdigest()78 return signature == computed_hash
Now that we have our validator working, we can use it to check if a request did come from Twitch and then handle that request. If the request didn’t come from Twitch we can just send an error message back.