HMAC
HMAC takes a key and a message and produces a fixed-size MAC. It proves the message wasn’t tampered AND came from someone holding the key. The canonical use: webhook signatures, API request signing, password reset tokens.
Sign + verify a webhook
EXAMPLE
import { createHmac, timingSafeEqual } from 'node:crypto';
// 1) SIGN — server-side, before sending
function sign(secret, body, ts = Date.now()) {
const payload = `${ts}.${body}`;
const mac = createHmac('sha256', secret).update(payload).digest('hex');
return { signature: `t=${ts},v1=${mac}`, payload };
}
// 2) VERIFY — receiver-side, constant time
function verify(secret, body, header, maxSkewMs = 5 * 60 * 1000) {
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
const ts = Number(parts.t);
const sig = parts.v1;
if (!ts || !sig) return false;
if (Math.abs(Date.now() - ts) > maxSkewMs) return false; // replay protection
const expected = createHmac('sha256', secret).update(`${ts}.${body}`).digest('hex');
const a = Buffer.from(sig, 'hex');
const b = Buffer.from(expected, 'hex');
return a.length === b.length && timingSafeEqual(a, b);
}
// 3) USE — sender
const body = JSON.stringify({ event: 'order.paid', id: 'abc' });
const { signature } = sign(SECRET, body);
await fetch(WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Signature': signature },
body,
});
// 4) USE — receiver (Express)
app.post('/webhooks/orders', express.raw({ type: 'application/json' }), (req, res) => {
const body = req.body.toString('utf8'); // raw, exact bytes
if (!verify(SECRET, body, req.headers['x-signature'])) {
return res.status(401).end();
}
const event = JSON.parse(body);
/* … */
res.json({ ok: true });
});
// 5) Other production HMAC uses
// - Password reset tokens (HMAC(user_id + expiry, server_key))
// - URL signing (HMAC the canonical request)
// - JWT (HS256 = HMAC-SHA-256 over header.payload)
// - AWS Sig v4 (HMAC chain)
Why it matters
Three rules: timingSafeEqual, raw body (not parsed JSON) for verification, and a timestamp + max-skew window so replays are bounded. Skip any one and the signature is decorative.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import { createHmac, timingSafeEqual } from 'crypto';
const expected = createHmac('sha256', secret).update(payload).digest();
const got = Buffer.from(headerSig, 'hex');
if (got.length !== expected.length || !timingSafeEqual(got, expected)) {
return res.status(401).end();
}
Try it Yourself »
Exercise
Constant-time compare in Node.
crypto.
(a, b)
camelCase.
Discussion
Loading…