HMAC Request Signing: How to Prevent Replay Attacks in Node.js
Learn how to implement HMAC request signing to prevent replay attacks. Secure your Node.js API with cryptographic signatures and timestamp validation.
When you're building sensitive integrations, standard TLS isn't always enough to stop a determined attacker. I recently spent about two days refactoring a payment gateway integration because our API was vulnerable to replay attacks—where a malicious actor intercepts a valid request and fires it again to trigger duplicate transactions.
If you aren't verifying the integrity and freshness of your incoming requests, you're leaving the door open for these kinds of exploits. Implementing HMAC request signing is one of the most effective API security best practices to ensure that requests are authentic and haven't been modified in transit.
Why HMAC Request Signing Matters
HMAC (Hash-based Message Authentication Code) works by using a shared secret key to generate a signature for the request body, headers, and a timestamp. Because the attacker doesn't have your secret key, they can't modify the request payload without breaking the signature.
When you prevent replay attacks using this method, you include a timestamp in the signed data. Your server then rejects any request where the timestamp is older than, say, 60 seconds. Even if an attacker captures a valid, signed request, they can't reuse it once that tiny window closes.
A First Attempt That Failed
We initially tried just checking for a unique request-id in our database. It worked until our traffic spiked to around 1,500 requests per minute. The database lookups for every single request added roughly 280ms of latency, which was unacceptable. We realized we needed a stateless way to verify requests, which led us to HMAC.
Implementing HMAC in Node.js
To implement this, you need a shared secret between your client and server. The client should sign the request, and your Node.js server will verify it using the same secret.
Here’s a basic implementation pattern using the built-in crypto module:
JAVASCRIPTconst crypto = require(CE9178">'crypto'); function verifySignature(req, secret) { const signature = req.headers[CE9178">'x-signature']; const timestamp = req.headers[CE9178">'x-timestamp']; // 1. Check if the request is too old(e.g., 60 seconds) if (Math.abs(Date.now() - parseInt(timestamp)) > 60000) { return false; } // 2. Re-create the signature using the shared secret const dataToSign = CE9178">`${timestamp}.${JSON.stringify(req.body)}`; const expectedSignature = crypto .createHmac(CE9178">'sha256', secret) .update(dataToSign) .digest(CE9178">'hex'); // 3. Constant-time comparison to prevent timing attacks return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expectedSignature) ); }
Critical Security Considerations
When implementing this, keep these three things in mind:
- Never expose the secret: Store your HMAC secret in a secure environment variable or a dedicated secret manager. Never commit it to your repository.
- Use
timingSafeEqual: Always usecrypto.timingSafeEqualinstead of a standard===string comparison. Standard comparisons returnfalseearly if they find a mismatch, which can allow an attacker to guess your signature byte-by-byte by measuring response times. - Use specific headers: Don't just sign the body. Sign the
timestamp, theURL path, and any relevant headers. This prevents an attacker from taking a signature from aGETrequest and using it on aPOSTrequest.
Choosing Your Authentication Strategy
If you're unsure which method fits your current stack, here is how HMAC compares to other common approaches.
| Method | Best For | Security Level | Complexity |
|---|---|---|---|
| Basic Auth | Internal scripts | Low | Very Low |
| JWT | User sessions | Medium | Medium |
| HMAC | Server-to-server | High | Medium |
| OAuth2 DPoP | Modern SPAs | High | High |
For more depth on hardening your flows, I've previously written about OAuth2 DPoP Implementation: Preventing Token Theft and Replay, which is the standard for browser-based apps. If you're dealing with bulk data, also check out my guide on JSON-RPC API security: Preventing Batching Attacks and Request Smuggling.
Frequently Asked Questions
What if the client clock is skewed? Clock skew is a common issue. I recommend a 60-second window, which is usually enough to account for minor drift while remaining tight enough to block replay attacks. If you find errors, check your NTP sync.
Does HMAC protect against man-in-the-middle (MITM)? HMAC provides message integrity, but it doesn't provide encryption. You must always wrap your API in TLS (HTTPS). HMAC ensures the data wasn't tampered with, while TLS ensures it wasn't read by a third party.
Can I use HMAC for user-facing logins? Generally, no. HMAC is best for server-to-server communication where both sides can hold a secret. For users, stick to established standards like OAuth2 or OpenID Connect. If you need help hardening your infrastructure, I provide VPS Server Setup, Deployment & Hardening to ensure your environment is configured correctly from the start.
Next time, I’d like to experiment with rotating the HMAC secrets automatically every 24 hours to further minimize the impact of a potential key leak. It adds complexity to the handshake, but it’s a standard I'm seeing more often in high-security environments.