<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 "> Authentication

Authentication

Anyone on the internet can POST to your webhook URL. You must prove the request came from the legitimate sender before acting on it. There is no single standard; each platform picks an approach:

Method Used by How it works
HMAC signature headerGitHub, Stripe, ShopifySender signs the raw body with a shared secret, puts the hash in a header. You recompute and compare. Most common and most secure.
Bearer / API tokenmany simpler servicesA secret token in the Authorization header. Simple, but can't prove body integrity.
Basic Autholder systems, some CI platformsUsername + password encoded in the Authorization header.
mTLS (mutual TLS)financial / enterpriseBoth sides present certificates. Very strong but complex to set up.
IP allowlistsupplementary guardOnly accept requests from the sender's published IP ranges. Easy but fragile if their IPs change.

How to use it

HMAC verification is the pattern worth knowing by heart. The sender computes HMAC(secret, rawBody) and sends it in a header like x-webhook-signature. You recompute it over the raw request body (not the parsed-and-re-stringified JSON) and compare using a constant-time function to prevent timing attacks:

1 · Sender signs the raw body raw body shared secret HMAC-SHA256 signature a1b2c3… POST /webhooks · body + header x-webhook-signature: a1b2c3… 2 · Receiver recomputes and compares raw body (recv) shared secret HMAC-SHA256 timingSafeEqual() constant-time compare received sig match → 200 differ → 401
The sender hashes the raw body with the shared secret and sends the result in a header. The receiver hashes the body the same way and compares the two with a constant-time check.
import crypto from "node:crypto";

function verifySignature(rawBody, signatureHeader, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  // Constant-time comparison — never use === for signatures
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signatureHeader)
  );
}

app.post("/webhooks/receive", (req, res) => {
  if (!verifySignature(req.rawBody, req.headers["x-webhook-signature"], SECRET)) {
    return res.status(401).end(); // not from who it claims to be
  }
  res.status(200).end(); // acknowledge fast
  enqueue(req.body);     // process async
});

Exercise

Node challenge · runs in your browser

Verify a webhook signature

Finish verifySignature so it accepts a webhook signed with the shared secret and rejects anything tampered with. The starter signs an empty string and compares with ===.

  • Sign the raw body, not an empty string.
  • Compare with crypto.timingSafeEqual so the check is constant-time.

node:crypto is available in this sandbox, so the tests run real HMACs.

Check your understanding

Check your understanding

Why is an HMAC signature stronger than a bearer token for webhooks?