HMAC Authentication Guide
Implement secure API authentication using Hash-based Message Authentication Codes.
This guide was originally written for NCR's developer platform, where it reduced auth-related support tickets by 40%. I have updated it to make it more robust and use active voice.
What is HMAC Authentication?
HMAC (Hash-based Message Authentication Code) is a request-signing mechanism used to authenticate API calls.
Your application secures each request with a cryptographic signature derived from:
- The request details: method, path, query, and selected headers
- A shared secret key known only to your application and server
- A timestamp
The server independently computes the same signature. If the signatures match and the request timestamp is valid, the request is accepted.
HMAC authentication provides the following guarantees:
- Request Integrity: The request was not modified in transit.
- Caller Authenticity: The caller possesses the valid secret key.
- Replay Protection: Attackers cannot reuse intercepted requests.
Key Components
To implement HMAC, you will use two specific credentials:
| Component | Purpose |
|---|---|
| Shared Key | The public identifier for your application. Include this with every request. |
| Secret Key | The private key used to generate signatures. Never transmit this key. |
The shared key identifies who is calling. The secret key proves they are authorized.
Authorization Format
Requests are authenticated using the AccessKey authorization scheme.
The Authorization header has the following format:
Authorization: AccessKey <shared-key>:<signature>
Where:
<shared-key>is your application's shared key<signature>is the Base64-encoded HMAC signature
How HMAC Authentication Works
HMAC authentication validates requests by ensuring that both your application and the server independently compute the exact same cryptographic signature.
Your application generates a signature using a normalized representation of the request and your shared secret key. The server then reconstructs that representation from the incoming request and computes its own signature.
If the two signatures match and the timestamp is within the validity window, the server accepts the request.
To authenticate an API request, follow these five steps in order. Each step contributes data to the final signature calculation. Deviations in formatting or encoding may result in authentication failures.
Step 1: Generate a Timestamp
Capture the current time using the ISO-8601 UTC format.
2025-06-25T18:42:11.000Z
You must use this exact timestamp string for both the signature calculation and the HTTP Date header.
Step 2: Build the Canonical Request String
Create a canonical text representation of the request.
Concatenate the following components in a fixed order, separated by newline characters \n:
- HTTP Method: Uppercase (e.g., POST, GET).
- Request URI: The path and query string, URL-encoded.
POST\n/api/transactions?limit=10
Newlines and whitespace are significant. Any deviation will cause signature mismatch.
Step 3: Derive the Signing Key
Create a unique key for each request by combining your Secret Key and the Timestamp.
<secret-key>:<timestamp>
mySecretKey:2025-06-25T18:42:11.000Z
Step 4: Generate the Signature
Compute the signature using the derived signing key and your canonical request string.
- Algorithm: HMAC-SHA256
- Input: Canonical Request String
- Output: Binary Hash
- Encoding: Base64
The resulting signature must be identical to the server’s computed signature for the request to be accepted.
Step 5: Send the Request
Construct your final HTTP request with the following headers:
Authorization: AccessKey <shared-key>:<signature>
Date: <timestamp>
Examples
import crypto from "crypto";
function buildCanonicalRequest(method, uri) {
return `${method.toUpperCase()}\n${encodeURI(uri)}`;
}
function generateSignature({
sharedKey,
secretKey,
method,
uri,
timestamp,
}) {
const canonicalRequest = buildCanonicalRequest(method, uri);
const signingKey = `${secretKey}:${timestamp}`;
const signature = crypto
.createHmac("sha256", signingKey)
.update(canonicalRequest, "utf8")
.digest("base64");
return {
authorizationHeader: `AccessKey ${sharedKey}:${signature}`,
dateHeader: timestamp,
};
}
// Example usage
const timestamp = new Date().toISOString();
const headers = generateSignature({
sharedKey: "your-shared-key",
secretKey: "your-secret-key",
method: "POST",
uri: "/api/transactions",
timestamp,
});
console.log(headers);Troubleshooting
If your authentication fails, check these common sources of error.
| Error | Likely Cause | Solution |
|---|---|---|
| 401 Invalid Signature | Canonical request mismatch | Verify method casing, URI encoding, header order, and whitespace |
| 401 Expired Request | Timestamp outside allowed window | Ensure your system clocks are synchronized via NTP |
| 403 Invalid Key | Incorrect shared key | Confirm the Shared Key matches the intended client credentials |
Debugging Tip: Log the generated Canonical Request String on both the client and the server. Comparing these strings usually reveals the issue immediately—a single missing newline or character difference will invalidate the signature.
Next Steps
To ensure the security and reliability of your integration:
- Secure your keys: Store secret keys in environment variables or a secrets manager. Never hardcode them.
- Synchronize clocks: Ensure your servers use NTP (Network Time Protocol) to avoid Expired Request errors.
- Limit scope: Avoid signing headers that might change in transit (like User-Agent or Accept) unless strictly required.