const crypto = require('crypto');
function parseSignatureHeader(header) {
const parts = {};
for (const segment of String(header || '').split(',')) {
const [key, value] = segment.split('=');
if (key && value) parts[key.trim()] = value.trim();
}
return { timestamp: parts.t, signature: parts.v1 };
}
function verifyWebhook(secret, options = {}) {
const toleranceSeconds = options.toleranceSeconds || 300;
return function (req, res, next) {
const { timestamp, signature } = parseSignatureHeader(req.get('X-Webhook-Signature'));
if (!timestamp || !signature) {
return res.status(400).json({ error: 'missing_signature' });
}
const age = Math.floor(Date.now() / 1000) - Number(timestamp);
if (!Number.isFinite(age) || Math.abs(age) > toleranceSeconds) {
return res.status(400).json({ error: 'timestamp_out_of_tolerance' });
}
const rawBody = req.body; // Buffer supplied by express.raw
const signedPayload = `${timestamp}.${rawBody.toString('utf8')}`;
const expected = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex');
const expectedBuf = Buffer.from(expected, 'utf8');
const receivedBuf = Buffer.from(signature, 'utf8');
if (expectedBuf.length !== receivedBuf.length ||
!crypto.timingSafeEqual(expectedBuf, receivedBuf)) {
return res.status(401).json({ error: 'signature_mismatch' });
}
try {
req.webhookEvent = JSON.parse(rawBody.toString('utf8'));
} catch (err) {
return res.status(400).json({ error: 'invalid_json' });
}
next();
};
}
module.exports = { verifyWebhook, parseSignatureHeader };
const express = require('express');
const { verifyWebhook } = require('./verifyWebhook');
const router = express.Router();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
// Bounded in-memory dedupe; back this with Redis in production.
const seenEvents = new Map();
const DEDUPE_TTL_MS = 24 * 60 * 60 * 1000;
function alreadyProcessed(id) {
const seenAt = seenEvents.get(id);
if (seenAt && Date.now() - seenAt < DEDUPE_TTL_MS) return true;
seenEvents.set(id, Date.now());
return false;
}
router.post(
'/webhooks/payments',
express.raw({ type: 'application/json', limit: '256kb' }),
verifyWebhook(WEBHOOK_SECRET, { toleranceSeconds: 300 }),
async (req, res) => {
const event = req.webhookEvent;
if (alreadyProcessed(event.id)) {
return res.status(200).json({ status: 'duplicate_ignored' });
}
// Acknowledge fast, then process out of band.
res.status(200).json({ received: true });
setImmediate(() => {
try {
handleEvent(event);
} catch (err) {
console.error('webhook processing failed', event.id, err);
}
});
}
);
function handleEvent(event) {
switch (event.type) {
case 'payment.succeeded':
return fulfillOrder(event.data.orderId);
case 'payment.refunded':
return reverseOrder(event.data.orderId);
default:
console.log('unhandled webhook type', event.type);
}
}
module.exports = router;
const crypto = require('crypto');
function signPayload(secret, body, timestamp = Math.floor(Date.now() / 1000)) {
const rawBody = Buffer.isBuffer(body) ? body.toString('utf8') : String(body);
const signedPayload = `${timestamp}.${rawBody}`;
const signature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return {
header: `t=${timestamp},v1=${signature}`,
timestamp,
signature,
};
}
if (require.main === module) {
const body = JSON.stringify({ id: 'evt_123', type: 'payment.succeeded' });
const { header } = signPayload('whsec_test', body);
console.log('X-Webhook-Signature:', header);
}
module.exports = { signPayload };
Webhook endpoints are public HTTP routes that any caller can hit, so the receiver must prove each request genuinely originated from the sender. The common defense is an HMAC signature: the sender computes HMAC-SHA256(secret, payload) and includes it in a header, and the receiver recomputes the same digest and compares. This snippet implements a Stripe-style scheme where the signed content is timestamp.rawBody, which binds the signature to a moment in time and blocks replay attacks.
The critical detail in verifyWebhook middleware is that the HMAC is computed over the exact raw bytes of the request body, not a re-serialized object. JSON.stringify can reorder keys or change spacing and would produce a different digest, so express.raw is used at the route level to hand the middleware a Buffer. The header is parsed into t (timestamp) and v1 (signature), the expected digest is derived with crypto.createHmac, and comparison uses crypto.timingSafeEqual to avoid leaking information through response timing. Because timingSafeEqual throws on length mismatch, the buffers are guarded to equal length first.
The middleware also enforces a toleranceSeconds freshness window: a captured-and-replayed request older than five minutes is rejected even if its signature is valid. On success it attaches the parsed JSON to req.webhookEvent so downstream handlers work with a normal object.
In webhookRoutes, note the ordering — express.raw must run before any global express.json() parser consumes the stream, otherwise the raw bytes are gone. The handler in webhookRoutes layers idempotency on top of authenticity: seenEvents records processed event ids so a duplicate delivery (senders retry aggressively) is acknowledged with 200 but not processed twice.
The signPayload helper mirrors the verification logic and exists mainly for tests and for documenting the exact signing contract. A key trade-off is that HMAC only proves the caller knows the shared secret; it is not encryption, so the payload should still travel over TLS. Returning 200 quickly and doing heavy work asynchronously is the standard pattern, since most providers treat a slow or non-2xx response as a failed delivery and retry.
Related snips
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
timestamp = request.headers.fetch('X-Signature-Timestamp')
signature = request.headers.fetch('X-Signature')
payload = request.raw_post
data = "#{timestamp}.#{payload}"
expected = OpenSSL::HMAC.hexdigest('SHA256', ENV.fetch('WEBHOOK_SECRET'), data)
HMAC signed API requests for webhook and partner integrity
#!/usr/bin/env bash
set -euo pipefail
export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"
Secrets management with environment isolation and Vault
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";
const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";
JWT access + refresh token rotation (conceptual)
package files
import (
"context"
"time"
Presigned S3 upload URLs (AWS SDK v2)
module EmailNormalization
extend ActiveSupport::Concern
included do
attr_accessor :soft_warnings
Soft Validation: Normalize + Validate Email
Share this code
Here's the card — post it anywhere.