EventSub HMAC SHA256 Invalid on Production


I have searched all around the forums as well as Stack Overflow and a handful of other smaller development sites but had no luck, so I guess it is time to make a thread myself.

I’m trying to connect the EventSub Webhook system up to my existing application. I’ve followed through the setup documentation time and time again, as well as tested this with the Twitch CLI.

I can create webhooks and the server responds with the challenge just fine, the webhooks are marked as enabled after the server responds with the challenge and Twitch begin to post to my webhook URL everytime an event happens.

The problem happens when an incoming event is posted to our webhook and we follow the procedure for verifying the event

Testing our work using the Twitch CLI, everything looks all and above board

-> twitch event trigger channel.update -F {our-callback-url} -s {secret}
✔ Request Sent. Received Status Code: 204
✔ Server Said: 

However, the second this goes to our production servers, the sha256 hashes are completely different to one another, meaning something somewhere differs…

I’ve cross examined everything I can think of:

  • The secret we send to Twitch
  • Ensuring the secret is used as part of the sha256 salt
  • Validated that both the secret used in the creation of the webhook is the same as the secret used for hashing

Server is PHP
Framework is Laravel

            public static function verifySecret(Request $request): bool
        $secret = self::getEncryptedSecret(); // Returns our secret we sent on creation of the webhook
        $messageId = $request->header('Twitch-Eventsub-Message-Id');

        if ($messageId === null) {
            return false;

        $messageTimestamp = $request->header('Twitch-Eventsub-Message-Timestamp');

        if ($messageTimestamp === null) {
            return false;

        $body = json_encode($request->post());

        $twitchHmac = $request->header('Twitch-Eventsub-Message-Signature');

        $raw = "{$messageId}{$messageTimestamp}{$body}";

        $ourHmac = hash_hmac('sha256', $raw, $secret);

        $ourHmac = "sha256={$ourHmac}";

        return hash_equals($twitchHmac, $ourHmac); // Returns True on Local | False on Production

I have two suspicions:

  1. I test on a Mac OS but the server is Linux and unsure if I should be handling this hashing differently?
  2. The timestamp of the event from Twitch is always about 1-2 seconds before the current time, I’m unsure if this is an issue?

You probably should be using the all lower case version of the header names

PHP (generally) converts all the key names of inbounde header to lower case. So I anticipate that $request->header('UPPPERCASE') or $request->header('PascalCase') is resulting in a blank response instead of the expected value.

Hence the generation of invalid SHAs for comparison.

It’s not in Laravel but heres my PHP example - twitch_misc/index.php at main · BarryCarlyon/twitch_misc · GitHub

So debug/test this out to see what laravel is returning for header values if PascalCase fetching a header actually works as expected or not.


$body = json_encode($request->post());

Use the RAW body not a rencoded JSON payload. Otherwise any payload that has emoji’s in will always fail or you’ll get other weirdness, as Twitch calculates their has using the RAW payload, but you are caclulating the comparisons using a decoded and reencoded payload which could differ

Hey @BarryCarlyon

Thank you for the reply!

So I did actually suspect this as I see with the Javascript example, they had to call toLowerCase() on each of the headers.

I decided to place a low level logger into the function so I could check a small monolog file to see if in fact we were getting anything returned from using uppercase letters with the headers.

This was what I got back:

[2022-10-25 10:52:33] production.DEBUG: Details retrieved to complete verification {
"messageId":"fatmG7lbGyYsrALlB5_-VjvBjQAfP...{REDACTED SOME}",
"body":"{"subscription":{"id":"{REDACTED}","status":"enabled","type":"channel.update","version":"1","condition":{"broadcaster_user_id":"{REDACTED}"},"transport":{"method":"webhook","callback":"{REDACTED}"},"created_at":"2022-10-25T10:51:31.050369765Z","cost":0},"event":{"broadcaster_user_id":"{REDACTED}","broadcaster_user_login":"deerockuk","broadcaster_user_name":"DeeRockUK","title":"Lets finish sorcery | [Private PvE Server] | !sneak !tiktok","language":"en","category_id":"493551","category_name":"Conan Exiles","is_mature":true}}"

As you can see, both messageId and messageTimestamp exist in that log, so whether Laravel deep down lowercase this with the method header() or not, it doesn’t seem this is the problem


I just seen your own edit about the payload being re-encoded. Interesting theory… Let me try a few things on that!

Then I expect this is the problem, you are caclulating the hash using a reencoded JSON payload instead of the RAW payload

Yeah I thought it would but in PHP I always would refer to my headers as all lowercase.

Yeah it’s a very confusing code exmaple… Why waste as cycle toLowerCaseing whne they can be lowercase to start with…


1 Like

@BarryCarlyon I’m sure you hear this multiple times, I do recognise the level of ranks you hold on these forums but you have fixed it!

So… Solution time ladies and gentlemen

Nothing was wrong with the hashing, hmac, secrets, it was indeed, the json_encode

I changed:

$body = json_encode($request->post());


$body = $request->getContent(); // Subject to the fact you're using Laravel and the Illuminate Request class

This fired a response as expected!

Thank you ever so much @BarryCarlyon

P.S - The edit tag… I hate doing it too :stuck_out_tongue: but its nicer than another reply!

1 Like

Good to hear!

Edit: no edit. Spoof edit!

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.