Debug Webhook HMAC Signature Mismatch
Compare the raw request body bytes used for HMAC signing against the bytes received, catching re-serialisation and encoding drift.
Task
Verify webhook signature validation by hashing the exact raw body and reconciling timestamp tolerance.
When to use this
A signed webhook fails validation only when the payload contains non-ASCII characters, pointing at an encoding mismatch.
Prerequisites
- The provider signing secret
- A way to store the raw request body
- A test endpoint that logs headers and body bytes
Steps
1. Store the raw body before any JSON parsing and compute the HMAC.
app.use('/webhook', express.raw({ type: '*/*' }));
app.post('/webhook', (req, res) => {
const mac = crypto.createHmac('sha256', secret).update(req.body).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(req.get('x-signature') || ''));
res.status(ok ? 200 : 401).end();
});Expected output: The signature verifies against the raw bytes, proving parsing does not alter the signed content.
2. Reject webhooks outside the timestamp tolerance.
const ts = Number(req.get('x-timestamp'));
if (Math.abs(Date.now() / 1000 - ts) > 300) return res.status(401).end();Expected output: Replayed webhooks older than five minutes are rejected.
Troubleshooting
Problem
Signature passes locally, fails in production
Solution
A proxy may re-encode the body; ensure the raw bytes reach the handler unchanged.
Problem
Non-ASCII payloads always fail
Solution
Compute the HMAC over the original byte buffer, not a re-serialised string.
Limitations
- Requires access to raw request bytes.
- Timestamp tolerance must balance replay protection and clock skew.
- Does not cover asymmetric signature schemes.
What this recipe does
Verify webhook signature validation by hashing the exact raw body and reconciling timestamp tolerance.
A signed webhook fails validation only when the payload contains non-ASCII characters, pointing at an encoding mismatch.
Prerequisites
- The provider signing secret
- A way to store the raw request body
- A test endpoint that logs headers and body bytes
Procedure
- Store the raw body before any JSON parsing and compute the HMAC.
app.use('/webhook', express.raw({ type: '*/*' }));
app.post('/webhook', (req, res) => {
const mac = crypto.createHmac('sha256', secret).update(req.body).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(req.get('x-signature') || ''));
res.status(ok ? 200 : 401).end();
});
Expected result: The signature verifies against the raw bytes, proving parsing does not alter the signed content.
- Reject webhooks outside the timestamp tolerance.
const ts = Number(req.get('x-timestamp'));
if (Math.abs(Date.now() / 1000 - ts) > 300) return res.status(401).end();
Expected result: Replayed webhooks older than five minutes are rejected.
How to confirm it worked
Run the commands above and confirm every expected result in order. If a step does not produce the stated output, use the troubleshooting notes before continuing. A passing run shows valid signatures pass, tampered bodies fail, and stale timestamps are rejected in every encoding case.
Troubleshooting
Signature passes locally, fails in production — A proxy may re-encode the body; ensure the raw bytes reach the handler unchanged.
Non-ASCII payloads always fail — Compute the HMAC over the original byte buffer, not a re-serialised string.
Limitations
- Requires access to raw request bytes.
- Timestamp tolerance must balance replay protection and clock skew.
- Does not cover asymmetric signature schemes.