HMAC (Hash-Based Message Authentication Code) helps you verify that a request is genuine and unchanged. This guide explains a simple, correct way to use HMAC in modern APIs: build a stable request string, use SHA‑256 or SHA‑512, include a timestamp and nonce, and verify all of it on the server.
What Is HMAC and Why Use It?
HMAC mixes a shared secret with a hash function (such as SHA‑256) to create a signature for your message. It resists tampering and proves the sender had the secret.
- Integrity: Catches any change to the message.
- Authenticity: Only holders of the secret can produce the signature.
- Performance: Fast in browsers, servers, and edge runtimes.
When to Use HMAC and the Correct Pattern
Recommended Pattern
- Add a timestamp and nonce (UUID) to each request.
- Compute a body hash (SHA‑256 of the raw body bytes).
- Build a canonical string: method, path, sorted query, chosen headers, body hash.
- Compute HMAC(secret, canonical) with SHA‑256 or SHA‑512.
- Send the signature, timestamp, nonce, keyId in headers.
Common Mistakes
- Using SHA‑1 or MD5 for signing (deprecated).
- Signing unsorted JSON or query params (results change across clients).
- Missing replay protection (no timestamp/nonce or TTL window).
- Leaking secrets in client code or logs.
- Ignoring clock skew in distributed systems.
Browser Example (Web Crypto)
This example computes HMAC‑SHA‑256 with crypto.subtle and a canonical request string:
async function hmacSha256(secret, message) {
const enc = new TextEncoder();
const keyData = enc.encode(secret);
const key = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', key, enc.encode(message));
return Array.from(new Uint8Array(signature))
.map(function (b) { return b.toString(16).padStart(2, '0'); })
.join('');
}
function canonicalize(obj) {
const method = obj.method;
const path = obj.path;
const query = obj.query || {};
const headers = obj.headers || {};
const bodyHash = obj.bodyHash || '';
const qs = Object.keys(query)
.sort()
.map(function (k) {
return k + '=' + encodeURIComponent(query[k]);
})
.join('&');
const hs = Object.keys(headers)
.sort()
.map(function (k) {
return k.toLowerCase() + ':' + String(headers[k]).trim();
})
.join('\n');
return method.toUpperCase() + '\n' + path + '\n' + qs + '\n' + hs + '\n' + bodyHash;
}
// Usage
const secret = 'your-secret-key';
const nonce = crypto.randomUUID();
const timestamp = Math.floor(Date.now() / 1000);
const payload = { amount: 100, currency: 'USD' };
const bodyData = new TextEncoder().encode(JSON.stringify(payload));
const bodyHashBuffer = await crypto.subtle.digest('SHA-256', bodyData);
const bodyHashHex = Array.from(new Uint8Array(bodyHashBuffer))
.map(function (b) { return b.toString(16).padStart(2, '0'); })
.join('');
const canonical = canonicalize({
method: 'POST',
path: '/v1/payments',
query: { a: 1 },
headers: {
'x-nonce': nonce,
'x-timestamp': timestamp
},
bodyHash: bodyHashHex
});
const signature = await hmacSha256(secret, canonical);
console.log('Canonical String:' + canonical);
console.log('HMAC Signature: ' + signature);Tip: Use our UUID Generator for nonces and Password Generator for strong API secrets.
Server Verification Checklist
- Check the timestamp is inside a small window (e.g., ±300s).
- Reject requests if the nonce was used before (store for TTL).
- Rebuild the canonical string exactly like the client.
- Select the right key by keyId and compute HMAC.
- Verify with a constant‑time comparison.
- Log pass/fail for monitoring and incident response.
Rotate keys regularly and keep old keys for a short grace period to avoid breaking clients.
Implementation Notes and Best Practices
- Prefer SHA‑256 or SHA‑512 for HMAC; avoid SHA‑1.
- Canonicalize query, headers, and JSON consistently to avoid signature drift.
- Set a strict TTL window and handle clock skew with small leeway.
- Protect secrets: use server‑side storage, environment variables, and never echo secrets in logs.
- Version your signing scheme (e.g.,
x-signature-v) so you can evolve it safely. - Validate content‑type and compute body hashes on raw bytes to avoid normalization issues.
Canonical Request Layout Example
METHOD
PATH
key=a&x=1
content-type:application/json
x-key-id:demo
x-nonce:550e8400-e29b-41d4-a716-446655440000
x-timestamp:1731313131
BODY_SHA256_HEXSort keys and normalize header values to keep signatures deterministic.
Related Tools and Guides
Conclusion
HMAC is a practical way to protect APIs. Use SHA‑256 or SHA‑512, build a stable request string, and add replay protection with timestamps and nonces. These basics go a long way on browsers, servers, and edge.
Start with a clear signing spec, test with known vectors, and evolve safely with versioned headers.