Stripe and Shopify Webhook HMAC-SHA256 Verification: Why JSON Body Parsers Break Raw-Body Signatures
The most frequent cause of 401 Unauthorized webhook failures in Node and TypeScript backends is verifying the HMAC-SHA256 signature against JSON.stringify(req.body) instead of the exact raw UTF-8 byte stream transmitted
The most frequent cause of 401 Unauthorized webhook failures in Node and TypeScript backends is verifying the HMAC-SHA256 signature against JSON.stringify(req.body) instead of the exact raw UTF-8 byte stream transmitted by Stripe or Shopify.
Whenever a global JSON body parser deserializes and re-serializes a payload, subtle differences in key spacing, escaped characters, or scientific notation produce a completely different SHA-256 digest.
import crypto from 'node:crypto';
export function verifyStripeWebhookRawHmac({ rawBodyBuffer, sigHeader, secret, nowSec, toleranceSec = 300 }) {
const parts = Object.fromEntries(
String(sigHeader || '').split(',').map((kv) => kv.trim().split('='))
);
const ts = Number(parts.t);
if (!Number.isInteger(ts) || Math.abs(nowSec - ts) > toleranceSec) return false;
const signedPayload = `${ts}.${rawBodyBuffer.toString('utf8')}`;
const expected = crypto.createHmac('sha256', secret).update(signedPayload, 'utf8').digest();
const provided = Buffer.from(String(parts.v1 || ''), 'hex');
return expected.length === provided.length && crypto.timingSafeEqual(expected, provided);
}
- Engineering deep-dive: Stripe & Shopify Webhook HMAC Raw-Body Pitfalls
- Interactive sandbox: Free Stripe & Shopify Webhook HMAC Signature Tester
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes โ full credit and traffic to the original publisher.