What Is HMAC?
HMAC (Hash-based Message Authentication Code) combines a cryptographic hash function with a secret key to produce a message authentication code. It provides two guarantees:
- Integrity: The message hasn't been tampered with — any change to the payload changes the HMAC.
- Authentication: The sender knows the secret key — only parties with the key can produce a valid HMAC.
HMAC(key, message) = H((key XOR opad) || H((key XOR ipad) || message))
// H = hash function (SHA-256 in HMAC-SHA256)
// Two nested hash operations prevent length-extension attacks
HMAC in JavaScript
// Web Crypto API (browser + Node.js 15+)
async function hmacSha256(key, message) {
const encoder = new TextEncoder();
const cryptoKey = await crypto.subtle.importKey(
"raw",
encoder.encode(key),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signature = await crypto.subtle.sign("HMAC", cryptoKey, encoder.encode(message));
return Array.from(new Uint8Array(signature))
.map(b => b.toString(16).padStart(2, "0"))
.join("");
}
// Node.js built-in crypto module
import { createHmac } from "crypto";
const hmac = createHmac("sha256", secret).update(payload).digest("hex");
HMAC in Python
import hmac
import hashlib
signature = hmac.new(
key=secret.encode(),
msg=payload.encode(),
digestmod=hashlib.sha256
).hexdigest()
Verifying Webhook Payloads
Stripe, GitHub, Shopify, and most other platforms sign webhook payloads with HMAC-SHA256. Here is a generic verification pattern:
// Express webhook handler
import { createHmac, timingSafeEqual } from "crypto";
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-signature-sha256"];
const expected = createHmac("sha256", process.env.WEBHOOK_SECRET)
.update(req.body) // raw body, not parsed JSON
.digest("hex");
// Use timingSafeEqual to prevent timing attacks
const sigBuffer = Buffer.from(signature, "hex");
const expBuffer = Buffer.from(expected, "hex");
if (sigBuffer.length !== expBuffer.length ||
!timingSafeEqual(sigBuffer, expBuffer)) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body);
// Process event...
res.status(200).send("OK");
});
Platform-Specific Formats
// GitHub: "sha256=<hex>"
const [alg, sig] = req.headers["x-hub-signature-256"].split("=");
// alg === "sha256"
// Stripe: "t=timestamp,v1=<hex>"
// Verify: HMAC("sha256", secret, timestamp + "." + payload)
// Check timestamp to prevent replay attacks
// Shopify: base64-encoded HMAC in X-Shopify-Hmac-Sha256
const decoded = Buffer.from(req.headers["x-shopify-hmac-sha256"], "base64").toString("hex");
Timing-Safe Comparison
Never compare HMACs with === — JavaScript string comparison short-circuits as soon as it finds a mismatch, leaking timing information that helps attackers guess the correct signature byte by byte. Always use crypto.timingSafeEqual() (Node.js) or hmac.compare_digest() (Python).
Generate HMAC Signatures Instantly
Use ToolsVito's HMAC Generator to compute HMAC-SHA256 (and other variants) from a key and message in your browser — useful for testing webhook integrations.