Using HMAC for Message Authentication
Learn how to use HMAC for API request signing, webhook verification, and secure message authentication.
What is HMAC?
HMAC (Hash-based Message Authentication Code) is a mechanism for verifying both the integrity and authenticity of a message. It combines a cryptographic hash function with a secret key to produce a signature that proves:
- -Integrity: The message hasn't been modified
- -Authenticity: The message came from someone who knows the secret key
Unlike regular hashing, HMAC requires a secret key. Without the key, an attacker cannot create a valid HMAC even if they know the message and the hash algorithm. This makes HMAC perfect for API authentication.
How HMAC Works
HMAC(key, message) = H((key ⊕ opad) || H((key ⊕ ipad) || message))
- H = hash function (SHA-256, SHA-512, etc.)
- ⊕ = XOR operation
- || = concatenation
- opad = outer padding (0x5c repeated)
- ipad = inner padding (0x36 repeated)
Simplified Explanation
HMAC essentially hashes the message twice with the key mixed in using XOR operations. This prevents length extension attacks and ensures the key is properly integrated into the hash.
You don't need to implement this yourself - use built-in HMAC functions in your programming language.
Common Use Cases
1. API Request Signing
Sign API requests to prove they came from an authorized client. The server verifies the signature before processing.
2. Webhook Verification
Services send webhooks with HMAC signatures so you can verify the webhook actually came from them.
3. Session Tokens
Create tamper-proof session tokens by including an HMAC of the session data.
4. Message Integrity
Ensure messages between systems haven't been tampered with during transmission.
Implementation Examples
import hmac
import hashlib
secret_key = b"your-secret-key"
message = b"Hello, World!"
# Generate HMAC
signature = hmac.new(secret_key, message, hashlib.sha256).hexdigest()
print(f"HMAC: {signature}")
# Verify HMAC
def verify_hmac(message, signature, key):
expected = hmac.new(key, message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
# Always use compare_digest to prevent timing attacks
is_valid = verify_hmac(message, signature, secret_key)
print(f"Valid: {is_valid}") const crypto = require('crypto');
const secretKey = 'your-secret-key';
const message = 'Hello, World!';
// Generate HMAC
const signature = crypto
.createHmac('sha256', secretKey)
.update(message)
.digest('hex');
console.log(`HMAC: ${signature}`);
// Verify HMAC
function verifyHmac(message, signature, key) {
const expected = crypto
.createHmac('sha256', key)
.update(message)
.digest('hex');
// Use timingSafeEqual to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
const isValid = verifyHmac(message, signature, secretKey);
console.log(`Valid: ${isValid}`); $secretKey = 'your-secret-key';
$message = 'Hello, World!';
// Generate HMAC
$signature = hash_hmac('sha256', $message, $secretKey);
echo "HMAC: $signature\n";
// Verify HMAC
function verifyHmac($message, $signature, $key) {
$expected = hash_hmac('sha256', $message, $key);
// Use hash_equals to prevent timing attacks
return hash_equals($expected, $signature);
}
$isValid = verifyHmac($message, $signature, $secretKey);
echo "Valid: " . ($isValid ? 'true' : 'false'); API Request Signing Example
Here's how to sign API requests to prevent tampering and replay attacks:
import hmac
import hashlib
import time
import requests
API_KEY = "your-api-key"
SECRET_KEY = "your-secret-key"
def sign_request(method, path, body=""):
timestamp = str(int(time.time()))
# Create signature string
message = f"{method}\n{path}\n{timestamp}\n{body}"
# Generate HMAC
signature = hmac.new(
SECRET_KEY.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
return {
'X-API-Key': API_KEY,
'X-Timestamp': timestamp,
'X-Signature': signature
}
# Make signed request
headers = sign_request('POST', '/api/users', '{"name":"Alice"}')
response = requests.post(
'https://api.example.com/api/users',
headers=headers,
json={"name": "Alice"}
) const crypto = require('crypto');
function verifyRequest(req) {
const apiKey = req.headers['x-api-key'];
const timestamp = req.headers['x-timestamp'];
const signature = req.headers['x-signature'];
// Check timestamp (prevent replay attacks)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) { // 5 minutes
return false; // Request too old
}
// Look up secret key for this API key
const secretKey = getSecretKeyForApiKey(apiKey);
if (!secretKey) return false;
// Reconstruct message
const body = JSON.stringify(req.body);
const message = `${req.method}\n${req.path}\n${timestamp}\n${body}`;
// Verify signature
const expected = crypto
.createHmac('sha256', secretKey)
.update(message)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
// Middleware
app.use((req, res, next) => {
if (!verifyRequest(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}
next();
}); Webhook Verification Example
Verify webhooks from services like Stripe or GitHub:
const crypto = require('crypto');
function verifyStripeWebhook(payload, signature, secret) {
// Stripe sends signature in format: t=timestamp,v1=signature
const elements = signature.split(',');
const timestamp = elements.find(e => e.startsWith('t=')).split('=')[1];
const sig = elements.find(e => e.startsWith('v1=')).split('=')[1];
// Create signed payload
const signedPayload = `${timestamp}.${payload}`;
// Compute expected signature
const expected = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Verify
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(sig)
);
}
// Express route
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['stripe-signature'];
const payload = req.body.toString();
if (!verifyStripeWebhook(payload, signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process webhook
const event = JSON.parse(payload);
console.log('Verified webhook:', event.type);
res.json({received: true});
}); Security Best Practices
Generate random keys with at least 256 bits of entropy. Don't use passwords or predictable values.
Always use hmac.compare_digest() (Python),
crypto.timingSafeEqual() (Node.js), or
hash_equals() (PHP) to prevent timing attacks.
Add timestamps to prevent replay attacks. Reject requests older than 5-15 minutes.
HMAC-SHA-256 is the standard. Avoid HMAC-MD5 and HMAC-SHA-1.
Store keys in environment variables or secret management systems. Never commit them to version control.
HMAC protects integrity, not confidentiality. Always use HTTPS to encrypt the entire request.
Common Mistakes
Never use signature == expected.
This is vulnerable to timing attacks. Use constant-time comparison functions.
Don't use short keys, passwords, or predictable values. Generate cryptographically random keys.
Without timestamps or nonces, attackers can replay valid requests. Always include replay protection.
Sign the entire request (method, path, headers, body). Attackers can modify unsigned parts.
Try It Yourself
Experiment with HMAC using our Hash Calculator:
- 1. Go to the Hash Calculator
- 2. Scroll to the HMAC section
- 3. Enter a message:
Hello, World! - 4. Enter a key:
my-secret-key - 5. Copy the HMAC signature
- 6. Change the message slightly and notice the signature changes completely
- 7. Change it back - the signature matches again
Official Resources
Standards & Specifications
- → FIPS 198-1: HMAC Specification (NIST)
- → RFC 2104: HMAC Keyed-Hashing (IETF)
- → RFC 4868: HMAC-SHA-256 with IPsec (IETF)
Security Best Practices
- → OWASP Authentication Cheat Sheet (OWASP)
- → NIST SP 800-107: Hash Algorithm Recommendations (NIST)
Implementation Documentation
- → Python hmac Module (Python.org)
- → Node.js Crypto: HMAC (Node.js)
- → PHP hash_hmac Function (PHP.net)