Authentication is the mechanism that answers "who is making this request?" before any business logic runs. Get it wrong and every other security measure downstream is moot. In 2026, three patterns cover the vast majority of API authentication needs: API keys, JSON Web Tokens (JWTs), and OAuth 2.0. Each has a specific home.
What changed in 2026
- Passkeys and WebAuthn replaced passwords at the user-facing layer, but API-to-API authentication still relies on the same key/token patterns.
- OAuth 2.1 consolidated the spec — it dropped implicit flow and the resource-owner password credentials flow entirely, and mandated PKCE for all authorization code flows.
- Short-lived tokens became the standard — 15-minute access tokens with refresh token rotation are now the expected default, not the exception.
- Secrets managers (AWS Secrets Manager, HashiCorp Vault, Infisical) are cheap enough that storing credentials in
.env files is considered poor practice for production.
API keys
An API key is a high-entropy random string issued by the server and presented by the client on every request:
GET /v1/data
Authorization: Bearer sk_live_xG7q2...
On the server:
import secrets
def generate_api_key() -> str:
return "sk_live_" + secrets.token_urlsafe(32)
Store a hashed version (SHA-256) in the database — never the raw key. Verify by hashing the incoming value and comparing.
Use API keys when: both parties are servers you control, the use case is simple (one service calling another), and you want low implementation overhead.
Limitations: no built-in expiry, no user context, revocation requires a DB lookup to mark the key invalid.
JSON Web Tokens (JWTs)
A JWT is a signed (and optionally encrypted) payload of claims:
eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTc0ODkwMDAwMH0.<sig>
The server validates the signature with its public key — no database lookup needed. This makes JWTs attractive for high-throughput APIs.
import jwt # PyJWT
from datetime import datetime, timedelta, UTC
def issue_token(user_id: str, secret: str) -> str:
payload = {
"sub": user_id,
"exp": datetime.now(UTC) + timedelta(minutes=15),
"iat": datetime.now(UTC),
}
return jwt.encode(payload, secret, algorithm="HS256")
Use JWTs when: you need stateless auth across multiple services, you want to embed claims (roles, tenant ID) to avoid extra DB calls, and you can live with short expiry times.
Limitations: revocation before expiry is painful — you need a blocklist, which reintroduces state. Keep access tokens short-lived (15 min); use refresh tokens for sessions.
OAuth 2.0 (authorization code + PKCE)
OAuth 2.0 is not an authentication protocol — it is an authorization delegation protocol. Use it when a user grants your app access to their data held by a third party (Google, GitHub, Stripe).
1. App generates code_verifier and code_challenge (S256 hash)
2. Redirect user to auth server with code_challenge
3. User authenticates and approves scopes
4. Auth server returns code
5. App exchanges code + code_verifier for access_token + refresh_token
6. Access API with access_token
PKCE (Proof Key for Code Exchange) is mandatory in OAuth 2.1 for all clients — it prevents authorization code interception attacks even in public (non-confidential) clients.
Use OAuth 2.0 when: a user is delegating access, you are building a third-party integration, or you need scoped permissions.
Strategy comparison
| Strategy |
Use case |
Stateless |
Revocable |
Complexity |
| API key |
Server-to-server |
Yes (with hashing) |
Yes (DB flag) |
Low |
| JWT (short-lived) |
Service auth / sessions |
Yes |
Hard (need blocklist) |
Medium |
| JWT + refresh token |
User sessions |
Access: yes |
Yes (invalidate refresh) |
Medium |
| OAuth 2.0 + PKCE |
User-delegated access |
Depends on IdP |
Yes |
High |
How to pick
- Machine-to-machine, you control both sides? API key with rotation policy.
- User sessions across your own services? JWT access token (15 min) + refresh token in HttpOnly cookie.
- User granting your app access to a third-party service? OAuth 2.0 authorization code with PKCE. Do not roll your own.
- Internal microservices on a zero-trust network? mTLS or service-mesh identity (SPIFFE/SPIRE) — both are first-class in 2026 service meshes.
Common mistakes
Long-lived access tokens. A leaked JWT that expires in 24 hours is a 24-hour breach window. 15 minutes hurts nothing significant.
Storing tokens in localStorage. XSS can read it. Use HttpOnly cookies for refresh tokens; keep access tokens in memory.
Not verifying the alg header. Early JWT libraries accepted alg: none. Always pin the algorithm server-side.
Broad API key scopes. Issue keys with the minimum required permissions. A billing key should not be able to delete records.
No key rotation. Schedule rotation. Treat key rotation like dependency upgrades — regular, automated, and boring.
What to skip
- Implicit flow — removed from OAuth 2.1; PKCE replaces it.
- Resource-owner password credentials flow — also removed; if you need to exchange a username/password, use a proper token endpoint or a first-party app flow.
- Rolling your own JWT signature verification — use a well-maintained library (python-jose, go-jwt, jsonwebtoken).
FAQ
Should I use JWT or sessions?
For SPAs and mobile apps, JWT in memory + HttpOnly refresh cookie is the 2026 default. For server-rendered apps, traditional sessions with a signed cookie are simpler and equally secure.
How do I revoke a JWT?
Maintain a short blocklist of revoked JTI (JWT ID) values in Redis. Only active tokens need to be in the blocklist — they expire naturally.
What is the difference between authentication and authorization?
Authentication is "who are you?" — covered here. Authorization is "what are you allowed to do?" — handled by policies evaluated after identity is established.
Is an API key really secure?
With HTTPS, high entropy, short rotation windows, and hashed storage, yes. The risk is in how keys are managed, not the pattern itself.
Where to go next