Security

What is HMAC? Message Authentication for Developers

Understanding HMAC (Hash-based Message Authentication Code): how it works, why it's more secure than plain hashes, and implementing it in your APIs.

HandyUtils January 7, 2026 5 min read

When you need to verify that a message hasn't been tampered with AND came from a trusted source, plain hashing isn't enough. That's where HMAC comes in.

The Problem with Plain Hashes

Suppose you send a message with its hash:

Message: {"amount": 100, "recipient": "Alice"}
Hash: sha256(message) = a1b2c3...

An attacker could:

  1. Intercept the message
  2. Modify it to {"amount": 10000, "recipient": "Mallory"}
  3. Compute the new hash
  4. Send both to the recipient

The hash verifies integrity but not authenticity—anyone can compute it.

What is HMAC?

HMAC (Hash-based Message Authentication Code) combines a cryptographic hash with a secret key. Only parties who know the key can create or verify the HMAC.

HMAC(key, message) = hash(key XOR opad || hash(key XOR ipad || message))

Or more simply:

HMAC = Hash(Secret Key + Message)  (with proper padding)

The secret key makes all the difference:

Message: {"amount": 100, "recipient": "Alice"}
Secret Key: "my-secret-key"
HMAC: HMAC-SHA256(key, message) = f4e5d6...

Without the key, an attacker can't forge a valid HMAC.

How HMAC Works

HMAC uses two passes of the hash function:

  1. Inner hash: Combine key with inner padding (ipad), hash with message
  2. Outer hash: Combine key with outer padding (opad), hash with inner result
ipad = 0x36 repeated (block size times)
opad = 0x5c repeated (block size times)

inner_hash = H((key XOR ipad) || message)
HMAC = H((key XOR opad) || inner_hash)

This construction:

  • Prevents length extension attacks
  • Provides better security properties than simple hash(key + message)
  • Is proven secure if the underlying hash function is secure

HMAC vs Hash Comparison

Property Plain Hash HMAC
Input Message only Key + Message
Verifies integrity
Verifies authenticity
Anyone can compute ✗ (need key)
Prevents tampering

Common HMAC Algorithms

HMAC can use any cryptographic hash:

Algorithm Output Size Use Case
HMAC-MD5 128 bits Legacy only
HMAC-SHA1 160 bits Legacy, some webhooks
HMAC-SHA256 256 bits Recommended
HMAC-SHA512 512 bits Extra security

Recommendation: Use HMAC-SHA256 for new projects.

HMAC in API Authentication

Many APIs use HMAC for request signing:

1. Client prepares request

POST /api/transfer
Body: {"amount": 100, "to": "Alice"}
Timestamp: 1705312800

2. Client creates signature

const message = timestamp + method + path + body;
const signature = hmacSha256(secretKey, message);

3. Client sends with headers

POST /api/transfer HTTP/1.1
X-Timestamp: 1705312800
X-Signature: a1b2c3d4e5f6...
Content-Type: application/json

{"amount": 100, "to": "Alice"}

4. Server verifies

const expectedSig = hmacSha256(secretKey, message);
if (signature !== expectedSig) {
    return 401 Unauthorized;
}

This pattern is used by AWS, Stripe, GitHub webhooks, and many more.

Implementing HMAC Signatures

JavaScript (Node.js)

const crypto = require('crypto');

function createHmacSignature(secret, message) {
    return crypto
        .createHmac('sha256', secret)
        .update(message)
        .digest('hex');
}

function verifyHmacSignature(secret, message, signature) {
    const expected = createHmacSignature(secret, message);
    return crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(expected)
    );
}

Python

import hmac
import hashlib

def create_hmac_signature(secret, message):
    return hmac.new(
        secret.encode(),
        message.encode(),
        hashlib.sha256
    ).hexdigest()

def verify_hmac_signature(secret, message, signature):
    expected = create_hmac_signature(secret, message)
    return hmac.compare_digest(signature, expected)

C# / .NET

using System.Security.Cryptography;
using System.Text;

string CreateHmacSignature(string secret, string message)
{
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
    return Convert.ToHexString(hash).ToLower();
}

Timing Attacks and Constant-Time Comparison

Never use regular string comparison for signatures!

// VULNERABLE - timing attack possible
if (signature === expectedSignature) { ... }

// SAFE - constant time comparison
if (crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) { ... }

Why? Regular comparison returns early when it finds a mismatch. An attacker can measure response times to gradually discover the correct signature, byte by byte.

Constant-time comparison always takes the same time regardless of where differences occur.

HMAC Best Practices

1. Use strong, random keys

// Generate a secure key
const key = crypto.randomBytes(32).toString('hex');

At minimum, use 256 bits (32 bytes) for HMAC-SHA256.

2. Include timestamp in signed message

Prevents replay attacks:

const message = timestamp + request;
// Reject if timestamp is too old
if (Date.now() - timestamp > 300000) reject();

3. Sign all relevant request parts

Include method, path, headers, and body in the signature:

const message = `${method}:${path}:${headers}:${body}`;

4. Use constant-time comparison

Always use timingSafeEqual() or hmac.compare_digest().

5. Keep keys secret

  • Store securely (environment variables, secret managers)
  • Rotate periodically
  • Use different keys for different purposes

6. Include version in signature

Allows algorithm upgrades:

X-Signature-Version: hmac-sha256-v1

Common HMAC Mistakes

1. Using plain hash instead of HMAC

// WRONG - no authentication
const hash = sha256(message);

// RIGHT
const hmac = hmacSha256(secret, message);

2. Weak or hardcoded keys

// WRONG
const key = "password123";

// RIGHT
const key = process.env.HMAC_SECRET; // Random 256-bit key

3. Not including all data

// WRONG - body can be modified
const sig = hmac(key, timestamp);

// RIGHT
const sig = hmac(key, timestamp + method + path + body);

4. String comparison for verification

# WRONG - timing attack
if signature == expected: ...

# RIGHT
if hmac.compare_digest(signature, expected): ...

Real-World Examples

GitHub Webhooks

X-Hub-Signature-256: sha256=a1b2c3...

Verify:
expected = 'sha256=' + hmac_sha256(secret, body)

Stripe Webhooks

Stripe-Signature: t=1705312800,v1=a1b2c3...

Verify:
payload = timestamp + '.' + body
expected = hmac_sha256(secret, payload)

AWS Signature V4

Uses HMAC-SHA256 in a multi-step process:

  1. Create canonical request
  2. Create string to sign
  3. Calculate signing key (derived via HMAC chain)
  4. Calculate signature

Summary

HMAC provides authentication that plain hashes can't:

  • Hash: Verifies integrity (message wasn't corrupted)
  • HMAC: Verifies integrity AND authenticity (message came from trusted source)

Key points:

  • Use HMAC-SHA256 for new projects
  • Keep keys secret and strong (256+ bits)
  • Always use constant-time comparison
  • Include timestamp to prevent replay attacks
  • Sign all relevant request data

Need to generate HMAC signatures? Try our HMAC Generator!

Related Topics
hmac authentication message authentication api security hash
Share this article

Continue Reading

Security
Understanding Hash Functions: MD5, SHA-1, SHA-256 Explained

A developer's guide to cryptographic hash functions: what they are, how they work, and choosing the right hash for your use case.

Security
Password Security: Hashing, Salting, and Best Practices

How to properly store passwords: why hashing alone isn't enough, what salting does, and modern password security recommendations.

Security
JWT Tokens Decoded: Structure, Security, and Best Practices

Understanding JSON Web Tokens: the three parts of a JWT, how verification works, and security considerations for token-based authentication.