Skip to content

Hash-Based Message Authentication Code(HMAC)

A keyed hash that proves a message came from someone holding a shared secret and was not changed; HMAC-SHA256 signs webhooks and API requests.

Also called: HMAC-SHA256 · Keyed hash · HMAC signature

Reviewed

Interactive lab

Hands on: use the controls to explore. Anything that plays on its own pauses while you are working inside it.

Sign a callback, then try to tamper with it

3 / 3

Incoming request

As signed

POST /webhook HTTP/1.1

X-Signature: sha256=c5bb9c2e18669c861febb6efaf7730783b28508c46dca7e413d9aa3b7b647492

83 bytes, exactly as the sender signed them.

Your server verifies

  1. Read the raw body and the headerUse the exact bytes received — 83 bytes, before any JSON parsing.
  2. Recompute the HMACHMAC-SHA256(secret, raw body)
  3. Compare the two digestsUse a constant-time comparison.
Shared signing keydemo-app-secret

X-Signature (received) vs HMAC-SHA256(secret, raw body) (computed)

Receivedsha256=c5bb9c2e18669c861febb6efaf7730783b28508c46dca7e413d9aa3b7b647492
Computedsha256=c5bb9c2e18669c861febb6efaf7730783b28508c46dca7e413d9aa3b7b647492
Match — signature validThe body is byte-for-byte what the sender signed. Process the event.
A payment callback signed with HMAC-SHA256. Step through the receiver's check, then change one character of the body or give the receiver the wrong key, and watch the comparison fail.

Match — signature valid. The body is byte-for-byte what the sender signed. Process the event.

01What is HMAC?

A hash-based message authentication code (HMAC) is a short tag computed from a message and a secret key with a cryptographic hash function such as SHA-256. Anyone holding the same key can recompute the tag and compare: if it matches, the message came from a key holder and was not altered on the way. Without the key, nobody can forge a valid tag or change the message undetected.

The construction, defined in RFC 2104, hashes the message twice with two padded versions of the key: HMAC(K, m) = H((K ⊕ opad) ∥ H((K ⊕ ipad) ∥ m)). The nesting is what makes HMAC safe where a naive hash of key plus message is not: it resists the length-extension attacks that affect SHA-256 used on its own. With SHA-256 the tag is 32 bytes, commonly sent as 64 lowercase hex characters or as Base64.

HMAC is the general primitive, and many security mechanisms in messaging are HMACs with a particular key and input. Meta's X-Hub-Signature-256 webhook header is an HMAC-SHA256 of the raw body keyed with the App Secret, and App Secret Proof (appsecret_proof) is an HMAC-SHA256 of an access token with the same key. Payment gateways, SMS providers and your own services use the idea to sign callbacks. Because both sides share one key, HMAC proves authenticity between two parties but, unlike a digital signature, cannot prove to a third party which of them made the tag.

Key takeaways

  • HMAC combines a secret key with a hash function to produce a tag that only key holders can create or check.
  • HMAC-SHA256 outputs 32 bytes, usually sent as 64 hex characters or as Base64.
  • A plain hash proves nothing about the sender; an HMAC proves the sender knew the shared secret.
  • Always compute over the exact raw bytes received and compare tags in constant time.
  • HMAC does not stop replays on its own: sign a timestamp or nonce and reject stale or repeated messages.

02 · How it works, step by step

How does HMAC work?

  1. Step 1

    Share a secret key

    Both parties hold the same random key (at least 32 bytes for HMAC-SHA256), exchanged out of band and kept with your Secrets Management tooling.

  2. Step 2

    The sender computes the tag

    It runs HMAC-SHA256 over the exact bytes it sends, often a timestamp joined to the body, and puts the tag in a header such as X-Signature or X-Hub-Signature-256.

  3. Step 3

    The receiver recomputes

    Using its copy of the key, it computes the HMAC over the raw bytes received, before any JSON parsing or re-serialising.

  4. Step 4

    Compare in constant time

    Use crypto.timingSafeEqual (Node.js) or hmac.compare_digest (Python) so the response time does not reveal how many leading characters matched.

  5. Step 5

    Check freshness

    If the signed data includes a timestamp, reject messages older than a few minutes and remember recent IDs, so a captured request cannot be replayed later.

03HMAC in code

import crypto from "node:crypto";
const key = Buffer.from(process.env.CALLBACK_SIGNING_KEY, "base64"); // 32+ random bytes
// Sender: sign "timestamp.body"
export function sign(rawBody, ts = Math.floor(Date.now() / 1000)) {
const tag = crypto.createHmac("sha256", key).update(`${ts}.${rawBody}`).digest("hex");
return { "X-Timestamp": String(ts), "X-Signature": `sha256=${tag}` };
}
// Receiver: reject stale requests, recompute over the raw bytes, compare in constant time
export function verify(rawBody, headers, maxAgeSec = 300) {
const ts = Number(headers["x-timestamp"]);
if (!Number.isInteger(ts) || Math.abs(Date.now() / 1000 - ts) > maxAgeSec) return false;
const expected = Buffer.from(
"sha256=" + crypto.createHmac("sha256", key).update(`${ts}.${rawBody}`).digest("hex"),
);
const given = Buffer.from(headers["x-signature"] ?? "");
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}

Signing the timestamp with the body means an attacker cannot refresh an old request without breaking the tag.

04HMAC: key facts and limits

Standards
RFC 2104 (1997); NIST FIPS 198-1
HMAC-SHA256 tag
32 bytes = 64 hex characters
Recommended key
At least 32 random bytes for HMAC-SHA256
SHA-256 block size
64 bytes; longer keys are hashed first
Inner and outer pads
ipad = 0x36, opad = 0x5c, repeated
Meta's uses
X-Hub-Signature-256 and appsecret_proof

05 · Common mistakes, and the fix

Common mistakes with HMAC

  • Hashing parsed and re-serialised JSON

    Whitespace, key order and Unicode escaping change when JSON is stringified again, so the tag never matches. Capture the raw body bytes and verify those.

  • Comparing tags with ==

    An ordinary string comparison stops at the first differing character and leaks timing information. Use a constant-time comparison function.

  • Using a plain hash as a signature

    SHA-256(body) can be recomputed by anyone, and SHA-256(secret + body) is open to length extension. Use the HMAC construction, not a home-made one.

  • Weak or shared keys

    A short, guessable or widely shared key can be brute-forced offline or reused. Generate 32+ random bytes per integration and rotate them.

  • Assuming a valid tag means a fresh request

    An attacker can resend a captured, correctly signed request. Sign a timestamp or nonce and reject old or repeated ones; see Replay Attack.

06Why does HMAC matter?

Webhook and callback URLs are public: anyone who finds yours can POST to it. Without a signature check, a forged "payment succeeded" or "message delivered" event goes straight into your database. HMAC is the cheap, standard way to make every request prove where it came from, at a few microseconds of hashing per request, and getting the details right (raw bytes, constant-time comparison, key handling, freshness) is what separates a real check from a decorative one.

07 · Tips from the bench

Best practices for HMAC

  • Handle prefixes such as sha256= the same way on both sides, or compare the full header string.
  • During key rotation, accept tags made with either the old or the new key until every sender has switched.
  • Log signature failures with the request ID, never with the key or the full tag.
  • Prefer HMAC-SHA256 over HMAC-SHA1 for new integrations.

08Example: A replayed callback that bounced

A Jaipur travel agency's booking engine receives payment callbacks and then sends a WhatsApp confirmation template. Each callback carries X-Timestamp and an HMAC-SHA256 of timestamp.body, keyed with a secret shared with the payment gateway. During a security test, a tester replays last week's genuine callback with its valid signature; the verifier rejects it because the timestamp is days old. A second test changes amount_paise from 149900 to 14990 and fails the HMAC comparison outright. Only fresh, untouched callbacks ever trigger a confirmation.

09HMAC FAQs

What is the difference between HMAC and a hash?+
A hash needs no key, so anyone can compute it; an HMAC needs a secret key. A hash shows data was not corrupted, while an HMAC also proves the sender knew the secret.
Is HMAC the same as a digital signature?+
No. HMAC uses one shared secret, so either party could have made the tag. Digital signatures sign with a private key and verify with a public key, which proves to anyone who signed.
Should an HMAC signature be hex or Base64?+
Either works, as long as both sides agree. Meta's X-Hub-Signature-256 uses lowercase hex with a sha256= prefix; many other APIs send Base64.
Is HMAC-SHA1 still secure?+
HMAC-SHA1 is not broken the way plain SHA-1 collision resistance is, but it is a legacy choice. Use HMAC-SHA256 for anything new.

From definition to your first message

InfiQ is an official Meta Business Partner. Start a 7-day free trial — no card needed — and see this running on your own number.