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.