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 guide to the OWASP Top 10: injection, broken access control, XSS, insecure design, and how to prevent each vulnerability with code examples.
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.