Cloudflare Worker cannot verify webhooks with SDK
Trying to use the
import { WorkOS } from '@workos-inc/node';
const workos = new WorkOS(process.env.WORKOS_API_KEY);
const webhook = await workos.webhooks.constructEvent({
payload: payload,
sigHeader: sigHeader,
secret: process.env.WEBHOOK_SECRET,
});
Webhook processing error: SignatureVerificationException: Signature hash does not match the expected signature hash for payload
/@[email protected]/node_modules/@workos-inc/node/lib/common/crypto/signature-provider.js:30:23) at [object Object] at fulfilled (/@[email protected]/node_modules/@workos-inc/node/lib/common/crypto/signature-provider.js:5:58)
Doing a manual check as describe here https://workos.com/docs/events/data-syncing/webhooks/3-process-the-events/b-validate-the-requests-manually DOES work.
So I guess it's an issue with the SDK and the crypto usage?
https://github.com/workos/workos-node/issues/964
@stefandevo I was actually about to open an issue for this exact thing. As far as I can tell there's two "bugs" that occur when the type of the payload you're passing is a string, as their deserializer and signature-provider expect a parsed js object rather than a string.
🔍 Bug Details
Bug #1: Signature Verification Failure
File: lib/common/crypto/signature-provider.js
Line: 57
Code: payload = JSON.stringify(payload);
Problem: This line always stringifies the payload, even when it's already a string. This causes signature verification to fail because the computed signature doesn't match the expected signature.
Bug #2: Event Deserialization Failure
File: lib/webhooks/webhooks.js
Line: 33
Code: const webhookPayload = payload;
Problem: The deserializeEvent() function expects a parsed JavaScript object, but receives a string payload, causing deserialization to fail.
📊 Debug Evidence
Here's the debug output showing both bugs:
Before Fix - Signature Verification Bug:
[WORKOS DEBUG] computeSignature called with: {
payloadType: 'string',
payloadLength: 446,
isString: true,
isObject: false
}
[WORKOS DEBUG] After JSON.stringify: {
originalPayloadStart: '{"id":"event_01K4JR2K6XTV2ETW2V3H9G52AG"',
stringifiedPayloadStart: '"{\\"id\\":\\"event_01K4JR2K6XTV2ETW2V3H9G52AG\\"', // ❌ Incorrectly stringified!
originalLength: 446,
stringifiedLength: 502 // ❌ Length changed!
}
[WORKOS DEBUG] Signature comparison: {
expectedSignature: '138515c989027ab1c0af71de3ad021833dc051b365669cbd74d54d850654825a',
providedSignature: '0f69d3cea67393cbcbe795217e49fff03bafe5dbc2d6e021', // ❌ Different!
signaturesMatch: false
}
After Fix - Working Correctly:
[WORKOS DEBUG] Fixed JSON.stringify logic: {
payloadType: 'string',
wasStringified: false, // ✅ Correctly skipped stringify
finalPayloadLength: 446
}
[WORKOS DEBUG] Signature comparison: {
signaturesMatch: true // ✅ Now matches!
}
[WORKOS DEBUG] constructEvent after JSON.parse: {
webhookPayloadType: 'object', // ✅ Properly parsed for deserializer
webhookPayloadId: 'event_01K4JR2K6XTV2ETW2V3H9G52AG',
webhookPayloadEvent: 'user.created'
}
🛠️ Root Cause Analysis
-
Signature Bug: The signature is computed against
timestamp.payload, but whenpayloadis a string,JSON.stringify(payload)transforms{"id":"event_123"}→"{"id":"event_123"}"(adds quotes), making the computed signature different from the expected one. -
Deserialization Bug: The
deserializeEvent()function uses a switch statement onevent.eventproperty, but when passed a string instead of an object,event.eventis undefined, causing the function to return undefined.
✅ Proposed Fix
Option 1: Add clear documentation and more accurate error reporting when a string is passed in so that dev don't get sent down a rabbit hole of debugging.
Option 2:
Fix #1: signature-provider.js
// Current
payload = JSON.stringify(payload);
// Fixed
if (typeof payload === 'object') {
payload = JSON.stringify(payload);
}
Fix #2: webhooks.js
// Current
const webhookPayload = payload;
// Fixed
const webhookPayload = typeof payload === 'string' ? JSON.parse(payload) : payload;
@stefandevo if you ensure that the payload is parsed into a js object you should be fine.
Also, if the maintainers want me to create a PR for this, I would absolutely love to!!!