Hashes, passwords, and MACs
Authenticate messages with a MAC
Use HMAC or a high-level message-authentication API when two trusted parties share a secret and must detect forgery or modification.
A plain hash does not prove who created a message. Anyone can calculate another hash after modifying it.
A message authentication code fixes that by combining the message with a secret key. Without the key, an attacker cannot produce a valid tag for modified bytes. HMAC is the standard construction, and HMAC-SHA-256 is what most webhook signatures use.
import { createHmac } from 'node:crypto'
const secret = process.env.WEBHOOK_SECRET
const tag = createHmac('sha256', secret).update(rawBody).digest('hex')
// e3b1c44298fc1c14...
The sender computes the tag and ships it in a header. The receiver recomputes it over the received bytes and compares.
Verify the exact bytes
Verify the exact bytes you received. Here is the failure that bites almost every webhook integration: the sender signs the raw JSON body, but the receiver parses and serializes it before verification. Whitespace and key order change the bytes, so authentic requests fail. Capture the raw request body before any JSON middleware touches it, and feed that to the HMAC.
Compare in constant time
Never compare MACs with == or ===. String comparison returns at the first differing character, so response timing leaks how many leading characters matched, and an attacker can build a valid tag byte by byte. Use a constant-time library comparison:
import { timingSafeEqual } from 'node:crypto'
const expected = createHmac('sha256', secret).update(rawBody).digest()
const received = Buffer.from(signatureHeader, 'hex')
const valid = expected.length === received.length &&
timingSafeEqual(expected, received)
timingSafeEqual throws when lengths differ, so check the length first.
What a MAC does not do
An HMAC proves that a shared-secret holder created the exact bytes. It does not prevent replay: yesterday’s validly signed request verifies again today. Include context such as version, timestamp, and message type in the authenticated data, and reject stale timestamps or repeated event identifiers.
It also gives no non-repudiation. Every verifier holding the secret can also forge messages, so a valid tag cannot prove to a third party which side produced it. Keep keys separate by purpose: the webhook secret authenticates webhooks and nothing else.
Practice
Create an HMAC for the exact raw bytes of a small webhook and save a successful verification. Change one byte and verify with the library constant-time comparison. Replay the original valid request and show why a timestamp or event identifier is still required.
Lesson completed