iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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)

Discussion

Loading…