patterntypescriptexpressMajor
Webhook Design: Reliable Delivery and Verification
Viewed 0 times
webhookHMACsignature verificationreplay attackevent deliverytimingSafeEqual
Problem
Webhooks sent without signing can be spoofed. Webhooks without retry logic lose events on transient failures. Receivers that process inline cause timeouts and missed events.
Solution
Sign webhooks with HMAC-SHA256 and implement retry logic with exponential backoff.
// Sender: sign the payload
import crypto from 'node:crypto';
function signPayload(payload: string, secret: string): string {
return crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
}
async function sendWebhook(url: string, event: unknown, secret: string) {
const payload = JSON.stringify(event);
const signature = signPayload(payload, secret);
const timestamp = Date.now();
await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': `sha256=${signature}`,
'X-Webhook-Timestamp': String(timestamp),
},
body: payload,
});
}
// Receiver: verify and acknowledge immediately
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-webhook-signature'];
const expected = `sha256=${signPayload(req.body.toString(), process.env.WEBHOOK_SECRET!)}`;
if (!crypto.timingSafeEqual(Buffer.from(sig as string), Buffer.from(expected))) {
return res.status(401).end();
}
res.status(200).end(); // Acknowledge immediately
processEventAsync(JSON.parse(req.body)); // Process in background
});Why
HMAC signatures prevent spoofed webhooks. Acknowledging immediately (before processing) prevents the sender from timing out and retrying, causing duplicate processing.
Gotchas
- Use crypto.timingSafeEqual for signature comparison to prevent timing attacks.
- Include a timestamp in the signature to prevent replay attacks — reject payloads older than 5 minutes.
- Parse raw body (Buffer) for signature verification — JSON.parse changes whitespace and invalidates signatures.
Revisions (0)
No revisions yet.