← Recipes

阅读中文版本

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.

~30 minwebhooksecuritydebugging

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

  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 result: The signature verifies against the raw bytes, proving parsing does not alter the signed content.

  1. 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.