HMAC Request Signing
Secure API requests with HMAC-SHA256 signatures to ensure integrity and authenticity.
Overview
HMAC (Hash-based Message Authentication Code) is the industry standard for signing API requests. By combining a shared secret with the request payload and a cryptographic hash, both sender and receiver can verify message integrity and authenticity without transmitting the secret over the wire.
When to Use
Use this resource when:
- Authenticating service-to-service API calls
- Ensuring webhook payloads have not been tampered with
- Implementing API key authentication without OAuth complexity
- Verifying request integrity across untrusted networks
Solution
HMAC-SHA256 Signing (Node.js)
const crypto = require('crypto');
function signRequest(method, path, body, timestamp, secret) {
const payload = method.toUpperCase() + path + timestamp + JSON.stringify(body);
return crypto.createHmac('sha256', secret).update(payload).digest('hex');
}
function verifyRequest(method, path, body, timestamp, signature, secret) {
const expected = signRequest(method, path, body, timestamp, secret);
// Constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex')
);
}
Client-Server Example (Python)
import hmac
import hashlib
import time
def sign_request(method: str, path: str, body: bytes, secret: str) -> str:
timestamp = str(int(time.time()))
message = f"{method.upper()}{path}{timestamp}{body.decode()}"
signature = hmac.new(
secret.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
return signature, timestamp
# Client
signature, ts = sign_request("POST", "/api/orders", b'{"id":1}', "my-secret")
headers = {"X-Signature": signature, "X-Timestamp": ts}
# Server
def verify(signature: str, timestamp: str, method, path, body, secret):
# Reject old requests (replay protection)
if abs(int(time.time()) - int(timestamp)) > 300:
return False
expected, _ = sign_request(method, path, body, secret)
return hmac.compare_digest(signature, expected)
Explanation
HMAC security relies on three properties:
- Secret key: Never transmitted; shared out-of-band during onboarding
- Message coverage: The signature must cover method, path, timestamp, and body
- Replay protection: Timestamp windows prevent attackers from reusing old requests
Why not plain SHA-256? SHA-256 without HMAC is vulnerable to length-extension attacks. HMAC uses two nested hash passes that prevent this.
Variants
| Algorithm | Hash | Strength | Notes |
|---|---|---|---|
| HMAC-SHA256 | SHA-256 | 128-bit | Recommended default |
| HMAC-SHA384 | SHA-384 | 192-bit | Higher security margin |
| HMAC-SHA512 | SHA-512 | 256-bit | Slower; use for high-security contexts |
| HMAC-Blake3 | Blake3 | 256-bit | Fast; modern alternative |
What Works
- Include timestamp: Reject requests older than 5 minutes to prevent replay attacks
- Sign the entire request: Method + path + timestamp + body (sorted headers if included)
- Use constant-time comparison: timingSafeEqual prevents timing attacks
- Rotate secrets regularly: Use key versioning (v1, v2) in the signature header
- Never log the secret: Log signatures and keys, never the raw secret
Common Mistakes
- Signing only the body: An attacker can replay a valid body with a different endpoint
- Missing replay protection: Without timestamps, intercepted requests are valid forever
- Using MD5 or SHA-1: Cryptographically broken; use SHA-256 minimum
- String comparison instead of timingSafeEqual: Vulnerable to timing attacks
- Storing secrets in environment variables without encryption: Use a secret manager
Advanced Solutions
Java HMAC-SHA256 signing with key rotation
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class HmacSigner {
private final Map<String, String> secrets = new HashMap<>();
public HmacSigner() {
secrets.put("v1", "old-secret-key");
secrets.put("v2", "current-secret-key");
}
public String sign(String method, String path, String body, String timestamp, String keyVersion)
throws Exception {
String payload = method.toUpperCase() + path + timestamp + body;
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(
secrets.get(keyVersion).getBytes(StandardCharsets.UTF_8),
"HmacSHA256"
);
mac.init(keySpec);
byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
return keyVersion + ":" + Base64.getEncoder().encodeToString(hash);
}
public boolean verify(String method, String path, String body, String timestamp, String signature)
throws Exception {
// Parse key version from signature: "v2:base64hash"
String[] parts = signature.split(":", 2);
if (parts.length != 2) return false;
String keyVersion = parts[0];
String receivedHash = parts[1];
if (!secrets.containsKey(keyVersion)) return false;
String expected = sign(method, path, body, timestamp, keyVersion);
String expectedHash = expected.split(":", 2)[1];
// Constant-time comparison
return MessageDigest.isEqual(
receivedHash.getBytes(StandardCharsets.UTF_8),
expectedHash.getBytes(StandardCharsets.UTF_8)
);
}
}
// Usage
HmacSigner signer = new HmacSigner();
String sig = signer.sign("POST", "/api/orders", "{\"id\":1}", "1690000000", "v2");
// Headers: X-Signature: v2:base64hash, X-Timestamp: 1690000000
Webhook signature verification with raw body
Many APIs (Stripe, GitHub, Slack) sign webhooks with HMAC. You must verify using the raw request body before any JSON parsing:
const crypto = require('crypto');
/**
* Verify a Stripe-style webhook signature.
* Stripe uses: t=<timestamp>,v1=<signature>
*/
function verifyWebhook(rawBody, signatureHeader, secret) {
const parts = signatureHeader.split(',');
const timestamp = parts.find(p => p.startsWith('t='))?.split('=')[1];
const signatures = parts
.filter(p => p.startsWith('v1='))
.map(p => p.split('=')[1]);
if (!timestamp || signatures.length === 0) return false;
// Reject old timestamps (5 minute window)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (age > 300 || age < -300) return false;
// Compute expected signature: HMAC-SHA256(timestamp + rawBody)
const payload = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
// Check against all provided signatures (Stripe may send multiple)
return signatures.some(sig =>
crypto.timingSafeEqual(
Buffer.from(sig, 'hex'),
Buffer.from(expected, 'hex')
)
);
}
// Express middleware: must use raw body
const express = require('express');
const app = express();
// IMPORTANT: use raw body for signature verification
app.use('/webhooks', express.raw({ type: 'application/json' }));
app.post('/webhooks/stripe', (req, res) => {
const rawBody = req.body.toString('utf8');
const sig = req.headers['stripe-signature'];
if (!verifyWebhook(rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET)) {
return res.status(400).send('Invalid signature');
}
const event = JSON.parse(rawBody);
console.log('Verified webhook:', event.type);
res.status(200).send('OK');
});
Nonce-based replay prevention
Timestamps alone allow a 5-minute replay window. Add a nonce cache to reject duplicate requests within that window:
import hmac
import hashlib
import time
from collections import OrderedDict
from typing import Optional
class NonceCache:
"""LRU cache for tracking used nonces within the timestamp window."""
def __init__(self, max_size: int = 10000, ttl_seconds: int = 300):
self._cache: OrderedDict[str, float] = OrderedDict()
self._max_size = max_size
self._ttl = ttl_seconds
def check_and_add(self, nonce: str) -> bool:
"""Returns True if nonce is new (acceptable), False if duplicate."""
now = time.time()
self._evict_expired(now)
if nonce in self._cache:
return False # Duplicate nonce
self._cache[nonce] = now
if len(self._cache) > self._max_size:
self._cache.popitem(last=False)
return True
def _evict_expired(self, now: float):
expired = [
k for k, ts in self._cache.items()
if now - ts > self._ttl
]
for k in expired:
self._cache.pop(k, None)
class HmacVerifier:
"""HMAC verifier with nonce-based replay prevention."""
def __init__(self, secret: str, max_skew_seconds: int = 300):
self.secret = secret.encode()
self.max_skew = max_skew_seconds
self.nonces = NonceCache(max_size=10000, ttl_seconds=max_skew_seconds)
def verify(
self,
method: str,
path: str,
body: str,
timestamp: str,
nonce: str,
signature: str,
) -> bool:
# Check timestamp window
try:
ts = int(timestamp)
except ValueError:
return False
if abs(int(time.time()) - ts) > self.max_skew:
return False
# Check nonce uniqueness
if not self.nonces.check_and_add(nonce):
return False
# Verify signature
message = f"{method.upper()}{path}{timestamp}{nonce}{body}"
expected = hmac.new(
self.secret,
message.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
# Server-side usage
verifier = HmacVerifier("my-secret-key")
# Request arrives with headers:
# X-Timestamp: 1690000000
# X-Nonce: a1b2c3d4-e5f6-7890-abcd-ef1234567890
# X-Signature: hexhash
is_valid = verifier.verify(
method="POST",
path="/api/orders",
body='{"id":1,"item":"widget"}',
timestamp="1690000000",
nonce="a1b2c3d4-e5f6-7890-abcd-ef1234567890",
signature="abc123...",
)
Key rotation strategy
import hmac
import hashlib
import time
from typing import Optional
class KeyRotator:
"""Manages HMAC key rotation with overlap period for zero downtime."""
def __init__(self):
self.keys: dict[str, dict] = {}
self.active_version: Optional[str] = None
def add_key(self, version: str, secret: str, activate: bool = True):
self.keys[version] = {
"secret": secret,
"created": time.time(),
}
if activate:
self.active_version = version
def deactivate_key(self, version: str):
self.keys.pop(version, None)
if self.active_version == version:
# Activate the most recent remaining key
if self.keys:
self.active_version = max(self.keys.keys())
else:
self.active_version = None
def get_signing_key(self) -> tuple[str, str]:
"""Returns (version, secret) for signing new requests."""
if not self.active_version:
raise RuntimeError("No active signing key")
return self.active_version, self.keys[self.active_version]["secret"]
def get_verification_keys(self) -> list[tuple[str, str]]:
"""Returns all valid keys for verifying incoming requests."""
return [(v, info["secret"]) for v, info in self.keys.items()]
def sign(self, method: str, path: str, body: str, timestamp: str) -> str:
version, secret = self.get_signing_key()
message = f"{method.upper()}{path}{timestamp}{body}"
sig = hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest()
return f"{version}:{sig}"
def verify(self, method: str, path: str, body: str, timestamp: str, signature: str) -> bool:
parts = signature.split(":", 1)
if len(parts) != 2:
return False
version, received_sig = parts
for v, secret in self.get_verification_keys():
if v != version:
continue
message = f"{method.upper()}{path}{timestamp}{body}"
expected = hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest()
if hmac.compare_digest(received_sig, expected):
return True
return False
# Rotation workflow:
# 1. Add new key (v2) — both v1 and v2 are valid for verification
rotator = KeyRotator()
rotator.add_key("v1", "old-secret", activate=False)
rotator.add_key("v2", "new-secret", activate=True)
# 2. Deploy: new requests signed with v2, old v1 requests still verify
# 3. After all old requests expire (timestamp window), deactivate v1
# rotator.deactivate_key("v1") Frequently Asked Questions
Is HMAC better than JWT for service-to-service auth?
HMAC is simpler and stateless for internal services. JWT is better when you need identity claims and third-party verification. For a full API security overview, see the API security checklist.
How do I handle clock skew between services?
Allow a 5-minute window and synchronize with NTP. Reject requests outside the window.
Can I use the same secret for multiple clients?
No. Each client should have a unique secret so you can revoke one without affecting others.
Related Resources
API Security Checklist — Authentication to Encryption
A thorough security checklist for APIs: authentication, authorization, input validation, rate limiting, encryption, logging, and deployment hardening.
GuideSecurity Best Practices Guide
A thorough guide to application security: authentication, authorization, input validation, secrets management, and common vulnerability prevention.
GuideWeb Application Security (OWASP Top 10)
A developer-focused walkthrough of the OWASP Top 10 — injection, XSS, broken access control, SSRF and the rest — with prevention code and a threat-modeling workflow.
RecipeWebSocket Authentication and Security Patterns
How to authenticate WebSocket connections, implement token validation, and handle authorization for real-time messaging in production
RecipeProtect Web Forms Against CSRF Attacks
How to prevent Cross-Site Request Forgery attacks using synchronizer tokens, SameSite cookies, and double-submit cookie patterns.
RecipePassword Hashing in Production
Securely hash and verify passwords using bcrypt, scrypt, and Argon2 with what works.