Taylor McNeil

docs-as-portfolio v1.4

GET/guides/hmac-authentication

HMAC Authentication Guide

Implement secure API authentication using Hash-based Message Authentication Codes.

Context

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:

  1. Request Integrity: The request was not modified in transit.
  2. Caller Authenticity: The caller possesses the valid secret key.
  3. Replay Protection: Attackers cannot reuse intercepted requests.

Key Components

To implement HMAC, you will use two specific credentials:

ComponentPurpose
Shared KeyThe public identifier for your application. Include this with every request.
Secret KeyThe 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:

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.

Example

2025-06-25T18:42:11.000Z

Warning

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:

  1. HTTP Method: Uppercase (e.g., POST, GET).
  2. Request URI: The path and query string, URL-encoded.
Example Canonical String

POST\n/api/transactions?limit=10

Note

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.

Format

<secret-key>:<timestamp>

Example

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 Header

Authorization: AccessKey <shared-key>:<signature>

Date Header

Date: <timestamp>

Examples

hmac.js
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.

ErrorLikely CauseSolution
401 Invalid SignatureCanonical request mismatchVerify method casing, URI encoding, header order, and whitespace
401 Expired RequestTimestamp outside allowed windowEnsure your system clocks are synchronized via NTP
403 Invalid KeyIncorrect shared keyConfirm the Shared Key matches the intended client credentials
Tip

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.