and which method really fits your case
Choosing the wrong authentication method costs more than just security, it costs developer time on every single integration. API keys, bearer tokens, OAuth2 and HMAC signed requests each solve a different problem, and knowing the difference lets you make the right call before the first commit.
Table of Contents
- 1. The API authentication problem
- 2. API keys: simple, but limited
- 3. Bearer tokens and JWT: stateless authentication
- 4. OAuth2 flows: which one for what
- 5. Authorization code + PKCE: the secure browser flow
- 6. HMAC signed requests: integrity and replay protection
- 7. mTLS: mutual certificate authentication
- 8. Token security: rotation, revocation and scope
- 9. Methods compared directly
- 10. Summary
- 11. FAQ
1. The API authentication problem
API authentication solves a fundamental problem: how does an API make sure that an incoming request actually comes from an authorized client, and not from an attacker who intercepted a connection, stole a token or replayed a request? The answer depends heavily on the use case. Internal microservice communication has different requirements than a public developer API, and an SPA accessing user data has different requirements than a backend-to-backend webhook.
The most common wrong decision: using API keys for every scenario because they are the easiest to implement. API keys have no built-in expiry, no scope separation, cannot be rotated without effort and offer no replay protection. In practice that leads to API keys that stay valid for years, end up in repositories and only get rotated manually after the next security incident. The following sections show which authentication method is right for which situation, and what each method actually implements.
2. API keys: simple, but limited
API keys are static secrets transmitted in a request header (X-API-Key or Authorization: ApiKey) or as a query parameter. They are easy to issue and to validate, but they have structural weaknesses. Without expiry they must be rotated manually. Without scope binding they always grant full access. Without a signature, an intercepted API key immediately lets an attacker construct a valid request. Still, they are the right tool in certain scenarios: internal tools, development environments, very simple APIs with a small, known set of consumers.
The correct implementation of API keys: they are stored server-side as a bcrypt hash, never in plain text. On issuance the client receives the plaintext value exactly once, after that it is no longer retrievable. API keys should be cryptographically random (at least 256 bits of entropy), follow a prefix scheme (mk_live_..., mk_test_...) to prevent mixing up environments, and never appear in full in logs (only the first and last 4 characters). For production scenarios with third-party developers or user delegation, API keys are not sufficient, that is where OAuth2 comes in.
# API-Key patterns, correct usage
# In Authorization header (preferred over query param)
curl -H "Authorization: ApiKey mk_live_a3f9c8b2d7e4f1..." \
https://api.mironsoft.de/v1/products
# X-API-Key header (common alternative)
curl -H "X-API-Key: mk_live_a3f9c8b2d7e4f1..." \
https://api.mironsoft.de/v1/products
# NEVER in query string, appears in server logs and browser history
# BAD: GET /products?api_key=mk_live_a3f9c8b2d7e4f1
# Rate limiting by API-Key (response headers)
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1748476800
Retry-After: 3600
# 429 Too Many Requests when limit exceeded
HTTP/1.1 429 Too Many Requests
Retry-After: 3600
Content-Type: application/problem+json
{ "type": "https://mironsoft.de/errors/rate-limit-exceeded",
"title": "Rate limit exceeded", "status": 429 }
3. Bearer tokens and JWT: stateless authentication
Bearer tokens per RFC 6750 are transmitted in the Authorization: Bearer <token> header. The name "bearer" means: whoever holds the token is authorized, without any further identity check. That makes them portable and stateless, but also vulnerable to token theft. JSON Web Tokens (JWT, RFC 7519) are the most common bearer token format for APIs. A JWT carries claims, structured data such as user ID, roles, scopes and expiry, directly in the signed token payload, so the server does not have to look the token up in a database.
The signature of a JWT is created with a private key (RS256, ES256) or a symmetric secret (HS256). The receiving server validates the signature with the public key or the shared secret. Critical validations: the exp claim (expiry), the iss claim (issuer), the aud claim (intended recipient) and the alg header (never accept none). JWTs are not encrypted, all claims in the payload are base64-decodable. Sensitive data (passwords, credit card numbers) does not belong in JWT payloads. For encrypted payloads there is JWE (JSON Web Encryption).
4. OAuth2 flows: which one for what
OAuth2 (RFC 6749) is not an authentication protocol, it is an authorization framework, an important distinction that often gets blurred in practice. OAuth2 governs how a client can access a user's resources on a resource server on the user's behalf, without ever knowing the user's password. The four most important flows for REST APIs are: authorization code (for web apps and SPAs with user delegation), client credentials (for machine-to-machine without user involvement), device code (for devices without a browser) and implicit (deprecated, do not use anymore).
The client credentials flow is the standard for M2M communication: the client authenticates with the authorization server using a client ID and client secret, receives an access token with the requested scopes and uses that token for API requests. The token has a short lifetime (15 minutes to 1 hour), and the client automatically requests a new one when it expires. No refresh token needed, the client can fetch a new token at any time. This flow suits every server-to-server integration: microservices, cron jobs, webhooks, data imports.
# OAuth2 Client Credentials Flow
# Step 1: Request access token
curl -X POST https://auth.mironsoft.de/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=service-importer" \
-d "client_secret=cs_live_secret..." \
-d "scope=products:read orders:write"
# Response
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5...",
"token_type": "Bearer",
"expires_in": 900,
"scope": "products:read orders:write"
}
# Step 2: Use token in API requests
curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5..." \
https://api.mironsoft.de/v1/orders
# Token introspection (RFC 7662), validate opaque tokens
curl -X POST https://auth.mironsoft.de/oauth2/introspect \
-H "Authorization: Basic base64(client_id:client_secret)" \
-d "token=eyJhbGciOiJSUzI1NiIsInR5..."
5. Authorization code + PKCE: the secure browser flow
For SPAs and mobile apps, the authorization code flow with PKCE (Proof Key for Code Exchange, RFC 7636) is the only secure OAuth2 flow. The problem: an SPA cannot store a client secret securely, JavaScript code is readable by every user. PKCE solves that without needing a client secret at all. The client generates a cryptographically random code_verifier, derives the code_challenge from it (SHA-256 hash, base64url encoded) and sends that challenge with the authorization request. The authorization server stores the challenge. On the token request, the client sends the original code_verifier. The server verifies that hash(code_verifier) matches the stored challenge, only the original client can redeem the token.
PKCE has been mandatory for every client since OAuth2.1 (in draft), not just for public clients. The implicit flow has been fully removed since OAuth2.1, it has no refresh tokens, returns access tokens directly in the URL fragment (which show up in browser history and server logs) and offers no PKCE protection. Every new SPA integration should use authorization code + PKCE, even if the authorization server still offers implicit. OpenID Connect (OIDC) builds on top of OAuth2 and adds authentication: an id_token (JWT) with user identity claims, a standardized /userinfo endpoint and discovery via /.well-known/openid-configuration.
6. HMAC signed requests: integrity and replay protection
HMAC signed requests (e.g. AWS Signature Version 4, webhook signatures) solve a problem bearer tokens do not solve: request integrity and replay protection. With a bearer token, an attacker who intercepts the connection can reuse the same token for arbitrary requests. With HMAC signatures, the signature is computed over the entire request (method, path, query, relevant headers, body hash) plus a timestamp. The server validates the signature and the timestamp, requests older than 5 minutes get rejected. The attacker cannot tamper with the request or replay it.
This pattern is essential for webhook callbacks. The webhook sender (e.g. Stripe, GitHub) signs the request body with a shared secret and HMAC-SHA-256 and transmits the signature in a header (X-Stripe-Signature, X-Hub-Signature-256). The receiver computes the signature itself and compares it with hmac.compare_digest (constant-time comparison, to prevent timing attacks). Without this validation, any HTTP client could call the webhook URL and trigger actions. HMAC signed requests are also the right mechanism for partner APIs where the request body must not be tamperable.
# HMAC Signed Request, webhook validation pattern
# Sender: sign request body with shared secret
TIMESTAMP=$(date +%s)
PAYLOAD='{"event":"order.created","orderId":"4711"}'
SECRET="whsec_live_secret..."
SIGNATURE=$(echo -n "${TIMESTAMP}.${PAYLOAD}" | \
openssl dgst -sha256 -hmac "$SECRET" -binary | \
xxd -p -c 256)
curl -X POST https://partner.example.com/webhooks/mironsoft \
-H "Content-Type: application/json" \
-H "X-Mironsoft-Timestamp: $TIMESTAMP" \
-H "X-Mironsoft-Signature: v1=$SIGNATURE" \
-d "$PAYLOAD"
# Receiver: validate signature (pseudocode logic)
# 1. Extract timestamp from header
# 2. Reject if abs(now - timestamp) > 300 seconds (replay protection)
# 3. Compute: expected = HMAC-SHA256(secret, "${timestamp}.${body}")
# 4. Compare: hmac.compare_digest(expected, received), constant time!
# 5. Store processed event IDs to prevent duplicate processing
7. mTLS: mutual certificate authentication
Mutual TLS (mTLS) extends the normal TLS handshake with mutual authentication: not only does the server present a certificate, the client does too. The server validates the client certificate against a CA (Certificate Authority), only clients with a valid, CA-signed certificate get access. This is the strongest authentication method for microservice communication in zero-trust networks: no secrets transmitted, no token theft risk, cryptographically verified identity at the network level.
mTLS is implemented transparently in Kubernetes environments with service meshes (Istio, Linkerd), every pod gets a short-lived certificate from the mesh CA, and mTLS between services is active automatically, without any code changes. For external APIs, mTLS is more effort because clients have to manage certificates. It is particularly well suited to highly secure B2B integrations (finance, government) and to APIs that cannot use token-based authentication for compliance reasons. Combining mTLS with bearer tokens is possible: mTLS authenticates the network identity (which service), the bearer token authorizes the business action (with which permissions).
8. Token security: rotation, revocation and scope
Token security is more than choosing the right format. The most important practices: short lifetimes for access tokens (15 to 60 minutes), longer but rotated refresh tokens (7 to 30 days, one-time-use rotation per RFC 6819). Refresh token rotation means: every use of a refresh token produces a new refresh token and invalidates the old one. If a refresh token is stolen, the attack is detected at the latest at the next refresh by the legitimate client, the token has already been consumed, so the server can invalidate the entire session.
Scope design is critical for the principle of least privilege. Instead of a catch-all scope (api:full_access), granular scopes get defined: orders:read, orders:write, customers:read. Clients request only the scopes they need for their current operation. Token revocation per RFC 7009 allows immediate invalidation of access and refresh tokens, essential during security incidents. JWKS (JSON Web Key Sets) under /.well-known/jwks.json let resource servers automatically fetch and rotate public keys for JWT signature validation, without any configuration changes.
9. Methods compared directly
Choosing an authentication method is not a one-size-fits-all decision. Different scenarios require different methods, and sometimes several combined.
| Scenario | Recommended method | Replay protection | Effort |
|---|---|---|---|
| Internal dev tools | API key | No | Minimal |
| M2M / microservices | OAuth2 client credentials | Via short expiry | Medium |
| SPA / mobile app | OAuth2 auth code + PKCE | Via short expiry | Medium |
| Webhook receiver | HMAC signed requests | Yes (timestamp) | Low |
| Zero-trust / B2B | mTLS (+bearer) | Yes (TLS) | High |
The most common wrong combination: deploying OAuth2 for internal microservice communication inside a private Kubernetes cluster with no zero-trust requirement creates overhead without a proportional security gain. In that context, mTLS through the service mesh is enough, or, if there is no mesh, a shared JWT with a short lifetime. The table shows recommendations, not absolute rules, the concrete use case, compliance requirements and existing infrastructure always factor into the final decision.
Mironsoft
API security, OAuth2 implementation and token strategies
API authentication that actually reduces attack surface?
We analyze existing authentication implementations, identify insecure patterns and implement the right method, OAuth2, HMAC signed requests or mTLS, for your specific use case.
Security audit
Analysis of existing token strategies, scope design and rotation mechanisms
OAuth2 setup
Authorization server, client credentials, PKCE flow and refresh token rotation
Webhook security
Implementing HMAC signature validation, replay protection and idempotency handling
10. Summary
Choosing the right API authentication method is an architecture decision with long-term consequences. API keys are simple, but without expiry, scope and replay protection they are only suitable for non-critical internal tools. Bearer tokens with JWT enable stateless, scalable authentication, but JWT claims are not encrypted and short lifetimes are mandatory. OAuth2 client credentials is the standard for M2M communication, authorization code + PKCE for user delegation in SPAs. HMAC signed requests are essential for webhook receivers and partner APIs where request integrity matters. mTLS is the strongest method for zero-trust networks.
Combine methods instead of replacing them: OAuth2 for authorization and mTLS for network identity is not redundancy, it is defense in depth. Token security is not a one-time setup, refresh token rotation, scope minimization and regular key rotation are ongoing operational tasks. Building these principles into your API architecture means building APIs that are not fully compromised by a single credential leak.
API Authentication: The Essentials at a Glance
API Keys
Store as a bcrypt hash, show the plaintext value only once, use a prefix scheme (mk_live_), never log in full.
OAuth2 & JWT
Access token 15-60 min, rotating refresh token, granular scopes, reject alg:none, validate aud/iss.
HMAC Signed Requests
Timestamp in the payload, max 5 minutes old, constant-time comparison, store processed event IDs.
Scope & Rotation
Least privilege: request only the scopes you need. Refresh token rotation. JWKS for automatic key rollover.
11. FAQ: API Authentication
1Authentication vs. authorization, what is the difference?
id_token.2Do I need OAuth2 for small APIs?
3OAuth2 vs. OpenID Connect?
id_token and a /userinfo endpoint.4Why avoid the implicit flow?
5How to prevent replay attacks?
6Store API keys securely in a database?
7When is mTLS worth it?
8How does refresh token rotation work?
9What belongs in a JWT payload?
10Validate webhook signatures correctly?
hmac.compare_digest (constant time). Store the event ID against duplicates.