API Authentication Methods
Introduction
API authentication verifies the identity of clients calling your API. Choosing the right authentication method depends on the threat model, client type, and operational requirements. This guide covers the four dominant approaches and their appropriate use cases.
API Keys
API keys are the simplest form of API authentication. A static token is issued to each client and included in every request.
from fastapi import FastAPI, HTTPException, Depends
from fastapi.security import APIKeyHeader
app = FastAPI()
api_key_header = APIKeyHeader(name="X-API-Key")
API_KEYS = {
"sk-live-a1b2c3d4": {"client": "payment-service", "scopes": ["read:transactions"]},
"sk-test-e5f6g7h8": {"client": "test-client", "scopes": ["read:test"]},
}
def validate_api_key(api_key: str = Depends(api_key_header)):
if api_key not in API_KEYS:
raise HTTPException(status_code=403, detail="Invalid API key")
return API_KEYS[api_key]
@app.get("/api/transactions")
def get_transactions(client=Depends(validate_api_key)):
if "read:transactions" not in client["scopes"]:
raise HTTPException(status_code=403, detail="Insufficient permissions")
return {"transactions": [...]}
**Pros**: Simple, fast, easy to revoke. **Cons**: Static keys can leak, no identity delegation, limited granularity.
OAuth2 Client Credentials
The OAuth2 client credentials grant is designed for server-to-server communication where the client is the resource owner.
import requests
from authlib.integrations.requests_client import OAuth2Session
# Client configuration
client = OAuth2Session(
client_id='my-service',
client_secret='my-secret',
scope='read:orders write:orders'
)
# Obtain access token
token = client.fetch_token(
url='https://auth.example.com/oauth/token',
grant_type='client_credentials'
)
# Use token for API calls
response = client.get(
'https://api.example.com/orders',
headers={'Accept': 'application/json'}
)
Server-side token validation:
import jwt
from jwt import PyJWKClient
JWKS_URL = "https://auth.example.com/.well-known/jwks.json"
def validate_bearer_token(token: str):
jwks_client = PyJWKClient(JWKS_URL)
signing_key = jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience="https://api.example.com",
issuer="https://auth.example.com",
options={"verify_exp": True}
)
# Validate scopes
token_scopes = payload.get("scope", "").split()
required_scopes = {"read:orders"}
if not required_scopes.issubset(set(token_scopes)):
raise PermissionError("Insufficient scope")
return payload
Mutual TLS (mTLS)
mTLS extends TLS so that both client and server present certificates, establishing mutual authentication at the transport layer.
# Generate client certificate
openssl req -newkey rsa:2048 -nodes \
-keyout client-key.pem \
-out client-csr.pem \
-subj "/CN=payment-service.production.internal"
# Sign with internal CA
openssl x509 -req -in client-csr.pem \
-CA ca-cert.pem -CAkey ca-key.pem \
-CAcreateserial -out client-cert.pem \
-days 365 -sha256
# Configure server for mTLS (Nginx)
server {
listen 443 ssl;
ssl_certificate /etc/nginx/server-cert.pem;
ssl_certificate_key /etc/nginx/server-key.pem;
ssl_client_certificate /etc/nginx/ca-cert.pem;
ssl_verify_client on;
ssl_verify_depth 2;
location /api/ {
# Extract client certificate info
proxy_set_header X-Client-CN $ssl_client_s_dn;
proxy_set_header X-Client-Verify $ssl_client_verify;
proxy_pass http://backend;
}
}
# Flask app reading mTLS client info
from flask import Flask, request
app = Flask(__name__)
@app.route('/api/data')
def api_data():
client_cn = request.headers.get('X-Client-CN')
client_verify = request.headers.get('X-Client-Verify')
if client_verify != 'SUCCESS':
return {"error": "TLS verification failed"}, 403
# Authorize based on client certificate CN
allowed_clients = {
'payment-service.production.internal': ['read:transactions'],
'order-service.production.internal': ['read:write:orders'],
}
if client_cn not in allowed_clients:
return {"error": "Unauthorized client"}, 403
return {"data": "sensitive data"}
HMAC Signing
HMAC signing creates request-specific signatures that prevent tampering and replay attacks.
import hmac
import hashlib
import time
from typing import Dict
class HMACAuthClient:
def __init__(self, api_key: str, api_secret: str):
self.api_key = api_key
self.api_secret = api_secret.encode()
def sign_request(self, method: str, path: str, body: bytes = b'') -> Dict[str, str]:
timestamp = str(int(time.time()))
nonce = secrets.token_hex(8)
# Build message to sign
message = f"{method}\n{path}\n{timestamp}\n{nonce}\n".encode() + body
signature = hmac.new(
self.api_secret,
message,
hashlib.sha256
).hexdigest()
return {
'X-API-Key': self.api_key,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
}
class HMACAuthServer:
def __init__(self):
self.secrets = {"client-1": "supersecret123"}
self.nonce_store = set()
def verify_request(self, method, path, headers, body):
api_key = headers.get('X-API-Key')
timestamp = headers.get('X-Timestamp')
nonce = headers.get('X-Nonce')
signature = headers.get('X-Signature')
# Replay protection
if int(time.time()) - int(timestamp) > 300:
return False # Expired
if nonce in self.nonce_store:
return False # Replay
self.nonce_store.add(nonce)
# Verify signature
secret = self.secrets.get(api_key)
if not secret:
return False
message = f"{method}\n{path}\n{timestamp}\n{nonce}\n".encode() + body
expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
Choosing the Right Method
| Method | Best For | Security Level | Complexity | |--------|----------|---------------|------------| | API Keys | Simple internal services, quick integration | Low-Medium | Very Low | | OAuth2 Client Credentials | Third-party integrations, scoped access | High | Medium | | mTLS | Service mesh, zero-trust, internal microservices | Very High | High | | HMAC Signing | Financial APIs, requests that must be non-repudiable | High | Medium |
Conclusion
No single API authentication method fits all use cases. Use API keys for low-risk internal tools, OAuth2 client credentials for third-party integrations with scope requirements, mTLS for service mesh and zero-trust architectures, and HMAC signing when request integrity and non-repudiation are critical. Always combine authentication with TLS encryption.