workos-node icon indicating copy to clipboard operation
workos-node copied to clipboard

Cloudflare Worker cannot verify webhooks with SDK

Open stefandevo opened this issue 4 months ago • 4 comments

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?

stefandevo avatar Aug 26 '25 18:08 stefandevo

https://github.com/workos/workos-node/issues/964

stefandevo avatar Aug 26 '25 18:08 stefandevo

@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

  1. Signature Bug: The signature is computed against timestamp.payload, but when payload is a string, JSON.stringify(payload) transforms {"id":"event_123"}"{"id":"event_123"}" (adds quotes), making the computed signature different from the expected one.

  2. Deserialization Bug: The deserializeEvent() function uses a switch statement on event.event property, but when passed a string instead of an object, event.event is 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;

Connorbelez avatar Sep 07 '25 20:09 Connorbelez

@stefandevo if you ensure that the payload is parsed into a js object you should be fine.

Connorbelez avatar Sep 07 '25 20:09 Connorbelez

Also, if the maintainers want me to create a PR for this, I would absolutely love to!!!

Connorbelez avatar Sep 07 '25 20:09 Connorbelez