How to Verify a Webhook Signature With HMAC SHA-256
Never trust a webhook until its X-Hub-Signature-256 header matches an HMAC SHA-256 you compute over the raw body with your secret.
Compute HMAC-SHA256(secret, rawBody) and compare it to the X-Hub-Signature-256 header using a constant-time comparison. Verify on the raw bytes before any JSON parsing.
Steps
- Capture the raw request body before parsing it as JSON.
- Compute
sha256=+ HMAC of the raw body keyed by the shared secret. - Compare to
X-Hub-Signature-256withcrypto.timingSafeEqual. - Reject with 401 on any mismatch, missing header, or missing secret.
Handler
handler.js
const crypto = require('crypto');
function verify(secret, rawBody, header) {
const expected =
'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(header || '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Gotchas
- Hash the raw body, not a re-serialized object; re-encoding changes byte order and whitespace.
- Use
timingSafeEqual, never===, to avoid a timing side channel. - Length-check before the compare, since
timingSafeEqualthrows on unequal lengths.