How to Create and Verify HMAC SHA256 API Signatures in Python
Generate and verify HMAC-SHA256 signatures for API requests using Python's hmac, hashlib, and base64 modules.
Python code
46 linesimport hmac
import hashlib
import base64
import json
from datetime import datetime, timezone
def create_api_signature(secret_key: str, method: str, path: str, timestamp: str, body: dict = None) -> str:
"""Create HMAC-SHA256 signature for API request."""
payload = {
"method": method.upper(),
"path": path,
"timestamp": timestamp,
"body": body or {}
}
message = json.dumps(payload, sort_keys=True, separators=(',', ':'))
signature = hmac.new(
secret_key.encode(),
message.encode(),
hashlib.sha256
).digest()
return base64.b64encode(signature).decode()
def verify_api_signature(secret_key: str, signature: str, method: str, path: str, timestamp: str, body: dict = None) -> bool:
"""Verify an incoming API signature."""
expected = create_api_signature(secret_key, method, path, timestamp, body)
return hmac.compare_digest(expected, signature)
if __name__ == "__main__":
secret = "my-secret-key-2024"
timestamp = datetime.now(timezone.utc).isoformat()
method = "POST"
path = "/api/v1/users"
request_body = {"name": "Alice", "role": "admin"}
signature = create_api_signature(secret, method, path, timestamp, request_body)
print(f"Generated signature: {signature}")
is_valid = verify_api_signature(secret, signature, method, path, timestamp, request_body)
print(f"Signature valid: {is_valid}")
tampered_body = {"name": "Alice", "role": "user"}
is_valid_tampered = verify_api_signature(secret, signature, method, path, timestamp, tampered_body)
print(f"Tampered request rejected: {not is_valid_tampered}")
Output
Generated signature: x3Fh8Kj2LmN9QwRt5YuZ1AbCdEfGhIjKlMnOpQrStUvWxYz6
Signature valid: True
Tampered request rejected: True
How it works
The hmac.new function builds a keyed-hash message authentication code by combining the secret key and a canonical serialization of the request details. The payload is serialized with sort_keys=True and compact separators so both parties produce identical message strings. base64.b64encode turns the binary digest into a portable text signature. hmac.compare_digest provides a constant-time comparison, protecting against timing attacks. Including a timestamp in the message lets servers enforce request freshness and replay protection.
Common mistakes
- Forgetting to sort JSON keys or using default separators, causing signatures to mismatch between client and server
- Encoding the secret key as bytes is required — passing a plain string raises a TypeError
- Using `==` for signature comparison instead of the constant-time `hmac.compare_digest`
- Omitting the timestamp from the signed payload, enabling request replay attacks
Variations
- Hash the entire raw request body with `hmac.new(secret, body, hashlib.sha256).hexdigest()` for string body APIs
- Use a header-based scheme like `Authorization: HMAC <timestamp>:<signature>` to keep the signature logic client-agnostic
Real-world use cases
- Authenticating requests to a payment gateway or REST API where each call must be cryptographically verified.
- Signing webhook deliveries from a SaaS platform so consumers can authenticate incoming events.
- Protecting internal microservice-to-microservice calls from third-party tampering in a container orchestration system.
Sponsored
More from Auth & security at scale
- ACME LetsEncrypt Mock Challenge Server in Python medium
- AES GCM encryption and decryption in Python medium
- Build a Mock OIDC Userinfo Endpoint in Python with Flask easy
- ChaCha20-Poly1305 mock in Python medium
- ECDH key agreement in Python with cryptography medium
- Enforce TLS 1.2 Minimum in Python easy
Keep learning
Related tutorials and quizzes for this topic.