Verifying Zoom webhook events with Laravel/PHP/hash_hmac

I’m trying to verify Zoom webhook events using Laravel/PHP/hash_hmac. Please see my attempt below:

$message = 'v0:'.$request->header('x-zm-request-timestamp').':'.json_encode($request->all());
$hash = hash_hmac('sha256', $message, config('services.zoom.webhook_secret_token'));
$signature = "v0={$hash}";

Example $message (with sensitive data redacted):

v0:1662618277:{"event":"meeting.updated","payload":{"account_id":"ACCOUNTID","operator":"ACCOUNTEMAIL","operator_id":"OPERATORID","object":{"uuid":"UUID","id":MEETINGID,"duration":60},"old_object":{"uuid":"UUID","id":MEETINGID,"duration":120},"time_stamp":1662618276925},"event_ts":1662618276925}

The resulting signature is the correct length and contains only lowercase alpha-numeric characters. It just doesn’t match the signature provided in the header.

I’ve also attempted base64_encode() for the $message and setting the binary value of hash_hmac to true & false. I’m also certain that the secret token of my marketplace app matches that which is stored in my application .env file.

Finally, I attempted a process very similar to that found as the PHP code sample here: Generate Signature. But though this does work in my app to generate a signature, attempting to copy it (removing irrelevant datapoints) did not work either.

Could anyone provide a code snippet showing exactly how webhooks should be verified with PHP?

Thanks in advance

Found the error:

$message should include $request->getContent() not json_encode($request->all())

The full snippet being:

$message = 'v0:'.$request->header('x-zm-request-timestamp').':'.$request->getContent();
$hash = hash_hmac('sha256', $message, config('services.zoom.webhook_secret_token'));
$signature = "v0={$hash}";
$verified = hash_equals($request->header('x-zm-signature'), $signature);

Hopefully that helps someone!

Live long and prosper.

1 Like

Thanks alot for help :slight_smile: . The documentation is misleading in this regards which is not good.

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