Webhook Security
Hash-based Message Authentication Code (HMAC)
| Complexity |
|
| Pros |
|
| Caveats |
|
| Examples |
Hash-based Message Authentication Code (HMAC) is, by far, the most popular authentication and message security method used on webhook requests, including 65% of the webhooks we studied. In this method, the webhook provider and listener use a secret key to sign and validate webhook requests.
On webhook requests, the provider signs the webhook message using the secret key plus a hashing algorithm — typically
HMAC-SHA256, encodes the resulting signature inbase64orhex, and then includes the signature in the webhook request as a header.The webhook listener receives the request and repeats the same steps — signs and encodes the webhook message using the secret key — and compares the resulting signature with the value sent in the request header. If the result matches, the request is considered legitimate.
const signatureHeader = 'Signature-Header' const signatureAlgorithm = 'sha256' const encodeFormat = 'hex' const hmacSecret = process.env.WEBHOOK_SECRET app.post('/webhook', (req, res) => { // Create digest with payload + hmac secret const hashPayload = req.rawBody const hmac = crypto.createHmac(signatureAlgorithm, hmacSecret) const digest = Buffer.from(signatureAlgorithm + '=' + hmac.update(hashPayload).digest(encodeFormat), 'utf8') // Get hash sent by the provider const providerSig = Buffer.from(req.get(signatureHeader) || '', 'utf8') // Compare digest signature with signature sent by provider if (providerSig.length !== digest.length || !crypto.timingSafeEqual(digest, providerSig)) { res.status(401).send('Unauthorized') }else{ // Webhook Authenticated // process and respond... res.json({ message: "Success" }) } })
Request Signature Validation
HMAC vs. Shared Secrets
HMAC offers the following advantages over Basic Authentication and other shared secrets:
- Authentication + message integrity: Assuming the secret key is known only by the webhook provider and the listener, the HMAC process verifies that the message comes from the webhook provider (authenticity) and its contents are the same as they were at the time of sending (integrity).
- The secret key remains secret: In HMAC, secret keys are not sent with the webhook request — only the signatures created with it — reducing the risk of stolen keys.
Common verification mistakes
Most failed HMAC verifications — and most insecure ones — come from a few mistakes on the consumer side:
Verifying a re-serialized body. The signature covers the exact bytes the provider sent. Frameworks that parse JSON before your handler runs, followed by
JSON.stringify, change whitespace and key order. Capture the raw body (for example,express.raw()) and verify before parsing. Stripe, Slack, GitHub, Shopify, and Linear all call this out in their docs.Comparing with
==. A regular string comparison returns as soon as a character differs, which can leak the expected signature through response timing. Use a constant-time function such ascrypto.timingSafeEqualin Node.js orhmac.compare_digestin Python. GitHub's docs say to "never use a plain==operator".Using the wrong encoding. Providers split between
hexandbase64, and some add a prefix likesha256=orv1,. The webhook directory lists the encoding for each provider.Signing the wrong content. Not every provider signs only the body:
Provider Signed content GitHub request_bodyStripe timestamp.request_bodySlack and Zoom v0:timestamp:request_bodyStandard Webhooks webhook_id.timestamp.request_bodySquare notification_url+request_bodyTwilio full_url+ sortedPOSTparametersWhen the URL is part of the signature, a proxy or load balancer that changes the scheme, host, or port will break verification.
Ignoring the timestamp. If the provider signs a timestamp, check it — otherwise a captured request can be replayed indefinitely.
HMAC is only as good as its implementation
Like any other security control, HMAC is only as good as its implementation. In our research, we saw many examples of webhook providers with unnecessary complexity, lack of features, and complex documentation that made their solutions tough to implement and keep safe. Good webhook implementations will typically:
- Use strong hash algorithms such as
sha256andsha512— providers still usingsha1, such as Twilio, are noted in the directory - Add sensitive headers to the hash digest:
... const clientIdHeader = 'clientid' ... app.post('/webhook', (req, res) => { // Create digest from the request payload and the clientid header const hashPayload = req.rawBody+'.'+req.get(clientIdHeader) const hmac = crypto.createHmac(signatureAlgorithm, hmacSecret) const digest = Buffer.from(signatureAlgorithm + '=' + hmac.update(hashPayload).digest(encodeFormat), 'utf8') ... }) - Leverage HMAC signatures to implement replay prevention, versioning, key rotation, and unique event IDs
- Follow an existing convention such as Standard Webhooks rather than inventing a new signature format
- Provide great documentation and features for better operations