What Is HMAC? How It Works and When to Use It
If you've ever verified a GitHub webhook, signed an AWS API request, or looked inside a JWT token, you've used HMAC — whether you knew it or not. HMAC is one of the most widely deployed authentication mechanisms in software systems, yet it's often confused with encryption or plain hashing.
This guide explains exactly what HMAC is, why it exists, how the algorithm works internally, and how to implement it correctly.
What Is HMAC?
HMAC stands for Hash-based Message Authentication Code. It's a mechanism for verifying two things simultaneously:
- Integrity — the message has not been altered in transit
- Authenticity — the message was created by someone who knows the secret key
HMAC is defined in RFC 2104 (1997) and uses a cryptographic hash function (typically SHA-256 or SHA-512) combined with a secret key to produce a fixed-size authentication tag.
The critical distinction: HMAC is not encryption. HMAC does not hide the content of a message — it only proves the message is authentic and unmodified. The message itself is still sent in plaintext (or over TLS). HMAC is a signature, not a cipher.
Why HMAC Exists: The Problem with Plain Hashes
To understand why HMAC was invented, consider what happens if you try to use a plain hash for authentication.
Suppose you want to prove to a server that a message came from you and wasn't modified. You could send the message alongside its SHA-256 hash:
message = "transfer $100 to Alice"
hash = SHA256("transfer $100 to Alice")
= a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3
The problem: An attacker who intercepts the message can also compute SHA-256 — it's a public algorithm with no secret. They can modify the message to "transfer $1000 to Bob", compute a new SHA-256 hash, and send both along. The server can't tell the difference.
A hash alone proves integrity only if the attacker can't also compute hashes — which they can. You need a secret that only you and the server know. That's what HMAC adds.
With HMAC:
message = "transfer $100 to Alice"
hmac = HMAC-SHA256(secret_key, "transfer $100 to Alice")
= 4a0e9c7d8b6f2e1a3d5c9b0e7f4a2d8c1b6e3f0a9d7c5b2e8a1f4d7c3b0e6f9a
An attacker cannot forge this tag without knowing secret_key. Even if they modify the message, they cannot produce a valid HMAC without the key. The server verifies the HMAC against the received message using its copy of the secret key — if they match, the message is authentic.
How HMAC Works Internally
HMAC is defined by this formula (RFC 2104):
HMAC(K, m) = H((K' ⊕ opad) || H((K' ⊕ ipad) || m))
Where:
H— the underlying hash function (e.g., SHA-256)K— the secret keyK'— the key padded to the hash's block size (64 bytes for SHA-256)ipad— inner padding:0x36repeated to fill the blockopad— outer padding:0x5Crepeated to fill the block⊕— XOR operation||— concatenation
In plain English, HMAC works like this:
- Pad the key to the block size of the hash function (64 bytes for SHA-256)
- Inner hash: XOR the padded key with
ipad, concatenate the message, and hash the result - Outer hash: XOR the padded key with
opad, concatenate the inner hash output, and hash again
The two-layer structure is deliberate. It prevents certain attacks against simpler constructions like H(K || m) (length extension attacks). HMAC's double-hash design is why it remains secure even with hash functions that have length-extension vulnerabilities.
You don't need to implement this yourself — every cryptographic library provides HMAC. But understanding the structure helps you reason about security properties.
HMAC vs Hash vs Digital Signature
These three mechanisms are often confused. Here's the key difference:
| Mechanism | Key Required | Verifiable By | Use Case |
|---|---|---|---|
| SHA-256 hash | No | Anyone | Data integrity only (checksum) |
| HMAC-SHA256 | Yes — shared secret | Anyone with the same key | Mutual authentication, API signing, webhooks |
| RSA / ECDSA signature | Yes — asymmetric key pair | Anyone with the public key | Public authentication, TLS, JWT RS256 |
Hash (no key): Proves data wasn't accidentally corrupted. Doesn't prove who sent it.
HMAC (shared secret key): Proves data came from someone who knows the secret. Both sender and receiver must have the same key. Suitable for server-to-server communication where you control both sides.
Digital signature (asymmetric key): Proves data came from someone with the private key, verifiable by anyone with the public key. No need to share secrets. Used in TLS certificates, code signing, and JWT RS256.
When to use HMAC vs a signature:
- If both parties are in your control (e.g., your server calling your other server, webhook delivery between services): HMAC
- If you need third parties to verify authenticity without sharing a secret (e.g., TLS, user-facing JWTs in a multi-service system): digital signatures
Common Uses of HMAC
Webhook Signature Verification
When a service delivers webhooks to your endpoint, it needs a way to prove the payload is genuine (not forged by a third party). The standard approach is HMAC.
GitHub webhooks: GitHub signs every webhook payload with HMAC-SHA256 using your webhook secret. The signature appears in the X-Hub-Signature-256 header:
X-Hub-Signature-256: sha256=3dca279e731c97c38e3019a075d02bae6f9c69980f2a7a33e38571df2a1a4e37
Stripe webhooks: Stripe sends the Stripe-Signature header containing a timestamp and HMAC-SHA256 signature. They include the timestamp in the signed payload to prevent replay attacks.
AWS Request Signing
AWS Signature Version 4 uses HMAC-SHA256 to authenticate every API request. AWS derives a signing key from your secret key, the date, region, service, and a constant — and uses that key to sign a canonical representation of the HTTP request.
The signing key is derived through multiple HMAC operations:
kDate = HMAC-SHA256("AWS4" + SecretKey, Date)
kRegion = HMAC-SHA256(kDate, Region)
kService = HMAC-SHA256(kRegion, Service)
kSigning = HMAC-SHA256(kService, "aws4_request")
signature = HMAC-SHA256(kSigning, StringToSign)
This hierarchy means that even if someone captures a signed request, they can't derive your secret key from the HMAC output.
JWT Signatures (HS256, HS384, HS512)
When a JWT uses the HS256 algorithm, its signature is an HMAC-SHA256 over the base64url-encoded header and payload:
signature = HMAC-SHA256(secret, base64url(header) + "." + base64url(payload))
The server that verifies the JWT must know the same secret. This is why HS256 JWTs are appropriate for systems where the same server both issues and verifies tokens — but RSA (RS256) is better when multiple services need to verify tokens without sharing a secret.
Cookie and Session Integrity
Frameworks like Django and Rails sign session cookies with HMAC to prevent users from tampering with their session data. The server signs the cookie value with HMAC using a SECRET_KEY. On each request, the HMAC is verified — if it doesn't match, the cookie was tampered with and is rejected.
API Request Authentication
Many APIs use HMAC for request authentication. The client signs a canonical string (method + path + timestamp + body hash) with HMAC-SHA256. The server independently computes the same HMAC and compares.
This pattern is used by payment processors, trading platforms, and any API where extra authentication security beyond OAuth is needed.
HMAC in Practice: Code Examples
Python
import hmac
import hashlib
message = b"transfer $100 to Alice"
secret_key = b"my-secret-key"
# Compute HMAC-SHA256
signature = hmac.new(secret_key, message, hashlib.sha256).hexdigest()
print(signature)
# 4a0e9c7d8b6f2e1a3d5c9b0e7f4a2d8c1b6e3f0a9d7c5b2e8a1f4d7c3b0e6f9a
# Verify (constant-time comparison to prevent timing attacks)
expected = b"4a0e9c7d..."
if hmac.compare_digest(signature.encode(), expected):
print("Valid!")
Note the use of hmac.compare_digest() instead of ==. This is critical — a plain equality check is vulnerable to timing attacks where an attacker can infer how many characters matched by measuring response time.
Node.js
const crypto = require('crypto');
const message = 'transfer $100 to Alice';
const secretKey = 'my-secret-key';
// Compute HMAC-SHA256
const signature = crypto
.createHmac('sha256', secretKey)
.update(message)
.digest('hex');
console.log(signature);
// Verify (constant-time comparison)
const expected = '4a0e9c7d...';
const valid = crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex')
);
Bash (using OpenSSL)
# Compute HMAC-SHA256
echo -n "transfer \$100 to Alice" | \
openssl dgst -sha256 -hmac "my-secret-key"
# HMAC-SHA256(stdin)= 4a0e9c7d...
Go
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func computeHMAC(message, key string) string {
mac := hmac.New(sha256.New, []byte(key))
mac.Write([]byte(message))
return hex.EncodeToString(mac.Sum(nil))
}
func verifyHMAC(message, key, expected string) bool {
actual := computeHMAC(message, key)
return hmac.Equal([]byte(actual), []byte(expected))
}
Webhook Signature Verification: Step by Step
Here's the full flow for verifying a GitHub webhook:
1. Set a webhook secret in GitHub — a random string you store on your server.
2. On each webhook delivery, GitHub sends:
POST /webhook HTTP/1.1
X-Hub-Signature-256: sha256=3dca279e731c97c38e3019a075d02bae6f9c69980f2a7a33e38571df2a1a4e37
Content-Type: application/json
{"action": "opened", "number": 1, ...}
3. Your server verifies:
import hmac
import hashlib
from flask import request, abort
WEBHOOK_SECRET = b"your-webhook-secret"
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Hub-Signature-256', '')
# Recompute the expected HMAC
expected = 'sha256=' + hmac.new(
WEBHOOK_SECRET,
request.data,
hashlib.sha256
).hexdigest()
# Constant-time comparison
if not hmac.compare_digest(signature, expected):
abort(401, "Invalid signature")
# Process the webhook
payload = request.json
...
The key steps:
- Get the raw request body (before any JSON parsing — parse after verification)
- Recompute HMAC-SHA256 using your copy of the secret
- Compare with the provided signature using constant-time comparison
- Only process the payload if verification succeeds
Replay attack prevention: For high-security applications, also verify a timestamp included in the signed payload (Stripe does this). If the timestamp is more than 5 minutes old, reject it — even if the signature is valid.
HMAC Security Considerations
Key length matters
The HMAC key should be at least as long as the hash output — 32 bytes (256 bits) for HMAC-SHA256. A shorter key reduces the effective security of the HMAC. Generate keys using a cryptographically secure random number generator:
import secrets
key = secrets.token_bytes(32) # 256-bit key
const key = crypto.randomBytes(32);
Always use constant-time comparison
Never compare HMAC values with == or ===. String comparison in most languages short-circuits on the first non-matching character, which leaks timing information. An attacker can measure response times to infer how many characters of their forged HMAC matched — and gradually craft a valid forgery.
Use hmac.compare_digest() in Python, crypto.timingSafeEqual() in Node.js, subtle.timingSafeEqual() in Go.
Don't reuse keys across contexts
If the same key is used for signing webhook payloads and signing session cookies, a valid HMAC from one context could potentially be used in another. Use separate keys for each purpose, or include a context string in the HMAC computation:
# Include context in the signed message
payload = f"webhook:v1:{raw_body}"
hmac.new(key, payload.encode(), hashlib.sha256)
HMAC-SHA256 is preferred over HMAC-MD5 or HMAC-SHA1
While HMAC-MD5 and HMAC-SHA1 are not as directly broken as MD5/SHA-1 alone (the HMAC construction provides some protection), best practice is to use HMAC-SHA256 for all new implementations. Some legacy protocols (like OAuth 1.0a) use HMAC-SHA1 — for these, migration is preferable but HMAC-SHA1 is not immediately catastrophic.
Generate HMAC Values Online
Use our HMAC Generator to compute HMAC values for any algorithm (HMAC-MD5, HMAC-SHA1, HMAC-SHA256, HMAC-SHA512) without installing OpenSSL or writing code. The HMAC-SHA256 Generator is pre-configured for the most common use case.
All computation happens in your browser — your keys and messages are never sent to any server.
Summary
- HMAC produces a message authentication code — it proves both integrity and authenticity
- It requires a shared secret key — without the key, you cannot produce or verify the HMAC
- HMAC is not encryption — the message itself remains readable; HMAC only authenticates it
- The algorithm: two layers of hashing with the key XORed into each layer — prevents length-extension attacks
- HMAC-SHA256 is the correct choice for new implementations — used by GitHub webhooks, AWS Signature V4, JWT HS256
- Always use constant-time comparison when verifying HMACs —
==is vulnerable to timing attacks - Generate secret keys with a CSPRNG and use at least 256 bits
- Don't reuse the same key across different authentication contexts