javascript 123 lines · 3 tabs

Verify Stripe-Style Webhook HMAC Signatures with a Timestamped Scheme in Express

Shared by codesnips Sep 2026
3 tabs
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 };
3 files · javascript Explain with highlit

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

Share this code

Here's the card — post it anywhere.

Verify Stripe-Style Webhook HMAC Signatures with a Timestamped Scheme in Express — share card
Link copied