HMAC Authentication — Signing API Requests with Secret Keys
HMAC (Hash-based Message Authentication Code) signs API requests with a shared secret. Learn how HMAC-SHA256 works, how to implement request signing in Node.js and Python, and...
HMAC signs a message with a secret key to prove both authenticity (sender knows the key) and integrity (message wasn’t modified). It’s the mechanism behind AWS Signature Version 4, webhook verification, and many API authentication schemes.
Use the Hash Generator to generate HMAC values for testing.
How HMAC works
HMAC(key, message) = Hash(key_outer || Hash(key_inner || message))
Where:
- Hash = SHA-256 (or SHA-1, SHA-512)
- key_inner = key XOR 0x36 (ipad)
- key_outer = key XOR 0x5C (opad)
- || = concatenation
This double-hash construction prevents length-extension attacks that would affect plain SHA-256 on hash(key + message).
Node.js HMAC
import crypto from 'crypto';
// Create HMAC:
function createHmac(message, secret, algorithm = 'sha256') {
return crypto.createHmac(algorithm, secret)
.update(message)
.digest('hex');
}
// Verify HMAC (constant-time comparison to prevent timing attacks):
function verifyHmac(message, secret, expected) {
const computed = createHmac(message, secret);
const computedBuf = Buffer.from(computed, 'hex');
const expectedBuf = Buffer.from(expected, 'hex');
if (computedBuf.length !== expectedBuf.length) return false;
// timingSafeEqual prevents timing attacks:
return crypto.timingSafeEqual(computedBuf, expectedBuf);
}
// Example:
const secret = 'my-super-secret-key';
const message = '{"user_id":123,"action":"purchase","amount":99.99}';
const signature = createHmac(message, secret);
// 'a3f4b2c1...'
// Verify:
verifyHmac(message, secret, signature) // true
verifyHmac(message, secret, 'wrong') // false
Python HMAC
import hmac
import hashlib
def create_hmac(message: str | bytes, secret: str | bytes) -> str:
if isinstance(message, str): message = message.encode()
if isinstance(secret, str): secret = secret.encode()
return hmac.new(secret, message, hashlib.sha256).hexdigest()
def verify_hmac(message: str | bytes, secret: str | bytes, expected: str) -> bool:
computed = create_hmac(message, secret)
# hmac.compare_digest prevents timing attacks:
return hmac.compare_digest(computed, expected)
# Example:
secret = 'my-super-secret-key'
message = '{"user_id":123}'
sig = create_hmac(message, secret)
verify_hmac(message, secret, sig) # True
Webhook signature verification
GitHub, Stripe, and many services use HMAC to sign webhooks:
// GitHub webhook verification:
app.post('/webhook/github', express.raw({ type: '*/*' }), (req, res) => {
const secret = process.env.GITHUB_WEBHOOK_SECRET;
const signature = req.headers['x-hub-signature-256'];
if (!signature) {
return res.status(401).send('No signature');
}
const computed = `sha256=${createHmac(req.body, secret)}`;
if (!crypto.timingSafeEqual(Buffer.from(computed), Buffer.from(signature))) {
return res.status(401).send('Invalid signature');
}
// Process webhook:
const payload = JSON.parse(req.body);
console.log('Verified webhook:', payload.action);
res.sendStatus(200);
});
// Stripe webhook verification:
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
app.post('/webhook/stripe', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
if (event.type === 'payment_intent.succeeded') {
const paymentIntent = event.data.object;
console.log('Payment succeeded:', paymentIntent.amount);
}
res.json({ received: true });
});
Signing API requests
// Example: sign API requests with timestamp to prevent replay attacks:
function signRequest(method, path, body, secret) {
const timestamp = Math.floor(Date.now() / 1000);
const message = `${timestamp}\n${method}\n${path}\n${body}`;
const signature = createHmac(message, secret);
return {
'X-Timestamp': timestamp,
'X-Signature': signature,
};
}
// Verify on server (reject if timestamp is more than 5 minutes old):
function verifyRequest(req, secret) {
const timestamp = parseInt(req.headers['x-timestamp']);
const signature = req.headers['x-signature'];
if (Math.abs(Date.now() / 1000 - timestamp) > 300) {
return false; // Replay attack
}
const message = `${timestamp}\n${req.method}\n${req.path}\n${req.body}`;
return verifyHmac(message, secret, signature);
}
HMAC vs JWT
| HMAC | JWT | |
|---|---|---|
| Purpose | Message authentication | Authentication + claims |
| Contains data | No (just a signature) | Yes (payload is visible) |
| Stateless | Depends on use | Yes |
| Common use | Webhooks, API signing | Auth tokens |
Related tools
- Hash Generator — compute HMAC and hash values
- SHA-256 Hash Guide — SHA-256 explained
- JWT Authentication Flow — JWT vs HMAC tokens
Related posts
- MD5 Is Dead. Use These Instead. — MD5 was broken in 2004 and is trivially cracked for passwords. Here's what to us…
- bcrypt Password Hashing — Why You Should Use bcrypt and How to Implement It — bcrypt is the standard password hashing algorithm for web applications. Learn wh…
- File Integrity Verification with Checksums — SHA-256 and MD5 — Verify file integrity using SHA-256 and MD5 checksums. Learn how to generate and…
- Hash Functions Comparison — MD5, SHA-1, SHA-256, bcrypt, Argon2 — Hash functions have different speed, output size, and security properties. MD5 a…
- JWT Authentication Flow — Login, Token Storage, Refresh Tokens — JWT authentication involves issuing tokens on login, sending them with requests,…
Related tool
Generate MD5, SHA-1, SHA-256, and SHA-512 hashes client-side.
Written by Mian Ali Khalid. Part of the Dev Productivity pillar.