HMAC authentication is a way to prove that a request to your API hasn’t been tampered with and that it originated from a party you trust, even if you’re communicating over an insecure channel like HTTP.

Imagine you’re sending a secret message, but you want the recipient to be absolutely sure it’s from you and hasn’t been altered in transit. You and the recipient have a shared secret key, like a special handshake. HMAC uses this shared secret to create a unique "fingerprint" for your message. This fingerprint, called a Message Authentication Code (MAC), is sent along with the message. The recipient can then use the same shared secret and the message to independently generate their own fingerprint. If their fingerprint matches yours, they know two things: the message is authentic (it came from you) and it’s intact (it wasn’t changed).

Let’s see this in action with a simple Python example. We’ll use the hmac and hashlib modules.

import hmac
import hashlib
import time
import json

# This is your shared secret key. Keep this VERY secure.
# In a real application, this would be stored securely, not hardcoded.
SECRET_KEY = b'my-super-secret-key-that-no-one-else-knows'

def create_hmac_signature(request_body, timestamp):
    """Creates an HMAC-SHA256 signature for the request."""
    message = f"{timestamp}{request_body}".encode('utf-8')
    signature = hmac.new(SECRET_KEY, message, hashlib.sha256).hexdigest()
    return signature

def verify_hmac_signature(received_signature, request_body, timestamp):
    """Verifies the HMAC signature of a received request."""
    expected_signature = create_hmac_signature(request_body, timestamp)
    return hmac.compare_digest(received_signature, expected_signature)

# --- Simulate an API request ---

# The data being sent in the request body
data_to_send = {
    "user_id": 123,
    "action": "update_profile",
    "payload": {"email": "new.email@example.com"}
}
request_body_str = json.dumps(data_to_send, sort_keys=True) # Ensure consistent body for signature

# Get the current timestamp. A common practice is to use seconds since epoch.
# This timestamp is crucial for preventing replay attacks.
current_timestamp = str(int(time.time()))

# Generate the signature
signature = create_hmac_signature(request_body_str, current_timestamp)

print(f"Original Request Body: {request_body_str}")
print(f"Timestamp: {current_timestamp}")
print(f"Generated HMAC Signature: {signature}")

# --- Simulate API receiving the request ---

# In a real API, these would be coming from the request headers/body
received_signature = signature
received_timestamp = current_timestamp
received_body_str = request_body_str

print("\n--- Verifying Request ---")
is_valid = verify_hmac_signature(received_signature, received_body_str, received_timestamp)

if is_valid:
    print("HMAC signature is valid. Request is authentic and unaltered.")
    # Process the request...
else:
    print("HMAC signature is invalid. Request may be tampered with or from an unauthorized source.")

# --- Simulate a tampered request ---
print("\n--- Verifying Tampered Request ---")
tampered_body_str = json.dumps({"user_id": 456, "action": "delete_account"}, sort_keys=True)
tampered_timestamp = str(int(time.time())) # Use a new timestamp for this example
tampered_signature = create_hmac_signature(tampered_body_str, tampered_timestamp)

is_valid_tampered = verify_hmac_signature(tampered_signature, tampered_body_str, tampered_timestamp)

if is_valid_tampered:
    print("HMAC signature is valid. Request is authentic and unaltered.")
else:
    print("HMAC signature is invalid. Request may be tampered with or from an unauthorized source.")

# --- Simulate a replay attack (using an old timestamp and signature) ---
print("\n--- Verifying Replay Attack ---")
# Using the original valid signature and timestamp, but a new body
replay_body_str = json.dumps({"user_id": 123, "action": "view_data"}, sort_keys=True)
# IMPORTANT: For a true replay attack, the attacker would re-use the OLD timestamp and signature.
# Here, we're demonstrating that *if* the timestamp is old, the API should reject it.
# A proper implementation checks if the timestamp is within an acceptable window.

# Let's simulate an old timestamp. If the API checks for freshness, this will fail.
old_timestamp = str(int(time.time()) - 600) # 10 minutes ago

# We'll use the original signature for demonstration, but it won't match the new body + old timestamp
# A real attacker would also try to forge a signature for the new body and old timestamp,
# which would fail if they don't have the secret key.
# The primary defense against replay is the timestamp freshness check.

# Let's simulate what happens if the API *only* checks the HMAC and *doesn't* check the timestamp freshness.
# This is why timestamp checking is VITAL.
print("Simulating API check WITHOUT timestamp freshness:")
is_valid_replay_no_ts_check = verify_hmac_signature(signature, replay_body_str, received_timestamp) # Using original timestamp
if is_valid_replay_no_ts_check:
    print("HMAC signature is valid. Request is authentic and unaltered (BUT timestamp is old!).")
else:
    print("HMAC signature is invalid. Request may be tampered with or from an unauthorized source.")

print("\nSimulating API check WITH timestamp freshness (e.g., within 5 minutes):")
# In a real API, you'd have a server-side clock and a tolerance window.
server_time = int(time.time())
request_time = int(received_timestamp)
time_tolerance_seconds = 300 # 5 minutes

if abs(server_time - request_time) <= time_tolerance_seconds:
    print("Timestamp is fresh. Proceeding to HMAC verification...")
    is_valid_replay_with_ts_check = verify_hmac_signature(signature, replay_body_str, received_timestamp)
    if is_valid_replay_with_ts_check:
        print("HMAC signature is valid. Request is authentic and unaltered.")
    else:
        print("HMAC signature is invalid. Request may be tampered with or from an unauthorized source.")
else:
    print(f"Timestamp is too old (difference: {abs(server_time - request_time)}s). Request rejected.")

The core idea is that you have a shared secret key. When a client makes a request, it takes the request body, concatenates it with a timestamp, and then hashes this combined string using HMAC with the shared secret key. This hash is the signature. The client sends the original request body, the timestamp, and the signature to your API.

Your API receives these three pieces of information. It then performs the exact same HMAC calculation: it takes the received request_body, concatenates it with the received_timestamp, and hashes it using its own copy of the SECRET_KEY. It then compares the signature it just calculated with the received_signature. If they match, the request is considered authentic and unmodified.

Crucially, HMAC alone doesn’t prevent replay attacks. A replay attack is when an attacker intercepts a valid request and re-sends it later. To combat this, the timestamp is essential. Your API should check that the received_timestamp is recent (within an acceptable time window, e.g., 5 minutes). If the timestamp is too old, the request is rejected, even if the HMAC signature is valid. This ensures that only current requests are processed.

The hmac.compare_digest function is used for comparing the signatures. This is important because it performs a constant-time comparison, which helps prevent timing attacks. A simple == comparison could be vulnerable if an attacker could measure the time it takes for the comparison to fail, thereby guessing parts of the signature.

When implementing this, you need to decide on a few things:

  1. The Hash Algorithm: SHA-256 is a common and secure choice, as demonstrated.
  2. The Secret Key: This must be strong, unique per client (if you have multiple clients), and kept secret on both the client and server sides. Never hardcode secrets in client-side code.
  3. Timestamp Format and Tolerance: Decide on a consistent format for timestamps (like seconds since epoch) and a reasonable time window for acceptance to prevent replay attacks.
  4. Where to put the signature and timestamp: Typically, these are sent in HTTP headers (e.g., X-Signature, X-Timestamp).

The most surprising thing about HMAC authentication is that it doesn’t inherently provide encryption. The request body itself is sent in plain text (unless you use HTTPS). HMAC only guarantees the integrity and authenticity of the message, not its confidentiality. An eavesdropper could still read the contents of the request if it’s not sent over TLS/SSL.

The next concept you’ll want to explore is how to manage and rotate these secret keys securely, especially as your API scales to support multiple clients.

Want structured learning?

Take the full Cryptography course →