What is HMAC?
Definition
HMAC (hash-based message authentication code) is a method for producing a short authentication tag by running a message and a secret key, known only to sender and receiver, through a hash function such as SHA-256. The receiver repeats the calculation and compares results; a match shows the message was not altered and was produced by someone holding the key. Webhook signatures, API request signing and JWT's HS256 algorithm all use HMAC.
Also known as: Hash-based Message Authentication Code, HMAC-SHA256, keyed-hash message authentication code, HMAC signature

Why a plain hash is not enough
Sending a message together with its hash does not protect it. Anyone who changes the message can recompute the hash, because SHA-256 is available to everyone. For verification to mean anything, the calculation has to include something an attacker does not know: a secret key.
The obvious approach, prefixing the key and hashing (SHA256(key + message)), is flawed with Merkle–Damgård hashes such as SHA-256. They are vulnerable to length extension: without knowing the key, an attacker can take a valid tag and compute a valid tag for the same message with extra data appended. HMAC avoids this with two nested hash passes. The definition in RFC 2104 is, roughly:
HMAC(K, m) = H( (K ⊕ opad) ‖ H( (K ⊕ ipad) ‖ m ) )
ipad = byte 0x36 repeated, opad = byte 0x5C repeatedNobody implements this by hand in practice. Every mainstream standard library ships an HMAC function, and you should use it rather than rolling your own.
What HMAC does and does not give you
| Technique | Integrity | Origin authentication | Confidentiality | Non-repudiation |
|---|---|---|---|---|
| Plain hash | Against accidental corruption only | No | No | No |
| HMAC | Yes | Yes, between parties sharing the key | No | No |
| Digital signature (RSA, ECDSA) | Yes | Yes | No | Yes |
| Encryption | Depends on the scheme | Depends on the scheme | Yes | No |
Two points are often confused. HMAC does not hide anything: a signed webhook body is perfectly readable, and confidentiality comes from TLS. And because both sides hold the same key, the receiver can produce valid tags too, so HMAC cannot prove to a third party that the sender, and only the sender, created a message. That property requires a public-key signature.
Verifying a webhook signature, step by step
Most payment, shipping and code-hosting providers sign webhook deliveries with HMAC-SHA256. Formats differ, but correct verification always follows the same steps:
- Capture the raw body. The tag was computed over the exact bytes on the wire. Parse and re-serialise the JSON and key order or whitespace changes, and the tag no longer matches. You may need to switch off your framework's automatic body parsing for this route.
- Build the signed string exactly as documented. Some providers sign the body alone; others sign a timestamp joined to the body (for example
timestamp + "." + body). - Compute the expected tag and compare in constant time.
- Check freshness. Rejecting deliveries older than a few minutes limits replay of a captured, validly signed request. Recording event IDs and ignoring repeats serves the same goal.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the unparsed request body as a Buffer
function isValidSignature(rawBody, receivedSig, secret) {
const expected = "sha256=" + createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(receivedSig ?? "");
// timingSafeEqual throws if the lengths differ
return a.length === b.length && timingSafeEqual(a, b);
}The sha256= prefix is GitHub's format; other providers use different prefixes, encodings (hex or Base64) and header names.
The case for constant-time comparison
An ordinary string comparison (===) stops at the first differing character, so a guess that is wrong in the first byte is rejected fractionally faster than one that is right for the first ten. The difference is far too small to see in a single request, but across very many requests it can emerge statistically and, in principle, let an attacker recover a valid tag byte by byte. Functions such as Node.js's crypto.timingSafeEqual take the same time wherever the mismatch occurs. Other languages have equivalents: hmac.compare_digest in Python, hash_equals in PHP.
The explicit length check in the example is needed because Node's function throws on inputs of different lengths. The only thing it reveals is the tag length, which is public anyway.
Choosing, storing and rotating keys
- Length and randomness: RFC 2104 strongly discourages keys shorter than the hash output (32 bytes for SHA-256). Generate keys with a cryptographically secure random generator; a human-chosen password is not a key.
- Storage: keep the key out of the repository, in an environment variable or a secrets manager.
- Separation: a distinct key per integration and per environment limits the blast radius of a leak.
- Rotation: accepting tags made with either the old or the new key for a short overlap allows a switch without dropped deliveries.
HMAC turns up well beyond webhooks. The HS256 algorithm in JWT is HMAC-SHA256, cloud providers' API request-signing schemes use it, and the one-time codes generated by authenticator apps (HOTP/TOTP) are built on HMAC as well.

