Authentication vs authorization
The two get conflated constantly. Authentication is the process of verifying identity — proving the request comes from a particular caller. Authorization is the process of deciding whether that caller is allowed to do what they're asking. The same credential answers the first question; your application logic answers the second.
Almost every "auth bug" in production is actually an authorization bug — the caller was correctly identified, but the system let them do something they shouldn't. Get authentication right, then design authorization separately on top of it.
The four common credential types
Four mechanisms cover the vast majority of API authentication in 2026: API keys, OAuth 2.0 access tokens, JSON Web Tokens, and mutual TLS. Each fits a particular use case.
API keys
An API key is an opaque string the server issues to a caller, which the caller sends with every request. The simplest possible scheme.
What they're for. Server-to-server integrations where one organization owns both the client and the server, or where the API is consumed by trusted partners. The key identifies the calling system, not an end user.
How to send them. In the Authorization header, with a custom scheme:
Authorization: Bearer sk_live_abc123def456...
Not in query parameters — query parameters end up in server access logs, browser history, and HTTP Referer headers. Not in custom headers like X-API-Key — they work but break the convention that secrets live in Authorization.
What goes wrong. API keys leak. They get committed to public source repositories, embedded in client-side JavaScript, posted in support tickets. Defenses: prefix keys (sk_live_...) so secret-scanners can detect them; rotate them on a schedule; let customers create multiple keys with different scopes so a leak doesn't compromise everything; revoke instantly when leakage is detected.
What they don't do. API keys identify the integration, not the end user. If your API needs to act on behalf of specific users (read this user's data, post on this user's behalf), API keys aren't enough. That's where OAuth comes in.
OAuth 2.0
OAuth 2.0 is the framework for delegated access — a user grants a third-party application permission to act on their behalf, without giving that application the user's password. The application receives an access token (and usually a refresh token) and uses the access token in subsequent API requests, the same way it would use an API key.
What it's for. Any time a third party needs to access user-specific data through your API. "Sign in with X" buttons, integrations between SaaS products, mobile apps acting on behalf of their users.
The grant types worth knowing. OAuth 2.0 defines several flows; only a few are still recommended:
- Authorization Code with PKCE. The default for any client that runs in a browser, mobile app, or any environment where the client secret can't be kept secret. The user is redirected to your authorization server, logs in, approves the access, and is redirected back to the application with an authorization code; the application exchanges the code (plus a PKCE verifier) for an access token. PKCE prevents code-interception attacks on public clients.
- Client Credentials. For server-to-server calls where the application authenticates as itself (no user involved). Functionally similar to an API key, but with the standardized OAuth token-issuance flow.
- Refresh tokens. Issued alongside access tokens. The application uses the refresh token to get a new access token without user interaction when the current one expires.
The grants you should not use anymore: Implicit Flow (deprecated; use Authorization Code with PKCE) and Resource Owner Password Credentials (deprecated; defeats the entire point of OAuth).
Scopes. Each token carries a set of scopes that limit what the application can do. Design scopes around capabilities the user understands, not around your API endpoints. read:orders and write:orders are good scopes; get_v1_orders is not. Fewer, well-named scopes are better than dozens of granular ones nobody can reason about.
JSON Web Tokens (JWT)
A JWT is a token format, not an authentication mechanism. It's a way to encode claims (statements about an identity) in a compact, signed string that the receiver can verify without calling back to the issuer. They're commonly used as the access token format in OAuth 2.0, and as session tokens in their own right.
What they're for. Cases where you want self-contained tokens — the receiver can verify the token by checking the signature, without making a network call to a session store. This makes them ideal for stateless backends and for tokens that travel across service boundaries within a microservices architecture.
The structure. Three base64-encoded parts separated by dots: header.payload.signature. The header says what algorithm signed it. The payload is a JSON object of claims (subject, expiry, issuer, custom claims). The signature is computed over the header and payload using the algorithm in the header.
What goes wrong. A few classic JWT failure modes that have caused real incidents:
alg: "none"attack. Some libraries accept tokens whose header says{"alg": "none"}, treating them as valid without checking a signature. Reject any token whose algorithm doesn't match what your application expects, regardless of what the header says.- Algorithm confusion. A token signed with HS256 (symmetric, using a shared secret) can be forged if the verifier accepts RS256 (asymmetric) and uses the public key as the HMAC secret. Pin the algorithm in your verifier.
- No expiry, or no expiry check. Tokens with no
expclaim, or libraries that don't enforce it, become permanent credentials when they leak. - Using JWTs for revocable sessions. A signed JWT cannot be revoked before it expires. If you need revocation (logout, password change, security event), either use short expiry plus refresh tokens, or maintain a revocation list — both of which negate the "stateless" advantage of JWTs in the first place.
When not to use JWT. If your tokens are user sessions and you need server-side revocation, opaque tokens with a session lookup are simpler and more secure. Use JWTs when the receiver-side verification cost matters more than the revocation semantics.
Mutual TLS (mTLS)
In mTLS, both the client and the server present X.509 certificates during the TLS handshake. The server verifies the client's certificate against a trusted CA. The client is identified by the certificate, not by anything in the HTTP request itself.
What it's for. Service-to-service authentication inside a controlled environment — a service mesh, a private network, an enterprise integration. Excellent security properties (no shared secrets to leak), but heavy operationally: certificate provisioning, distribution, rotation, revocation. Outside its sweet spot, the operational cost is hard to justify.
Where credentials go
The decision is small but consequential: in the Authorization header, in a custom header, in a query parameter, or in a cookie?
Authorizationheader — default. The protocol-defined location for credentials. Not logged by most servers (when configured correctly), not in URLs, not in the document.- Custom header (
X-Api-Key) — second-best. Functionally fine but breaks the convention. - Query parameter — avoid. Query strings end up in access logs, browser history, server error reports, and the HTTP
Refererheader sent to other sites. The only time it's defensible is for one-time short-lived tokens (signed download URLs, WebSocket upgrade tokens) where the leakage window is acceptable. - Cookies — only for browser session auth. When the API is consumed by a browser app on the same origin (or with proper CORS), HTTP-only secure cookies are the right choice for session tokens — they're protected from XSS by being inaccessible to JavaScript. CSRF becomes a concern; mitigate with the
SameSite=Strictattribute or with double-submit tokens.
Token expiry and refresh
The trade-off is between security and user experience. Short expiry means a leaked token has a short useful life; long expiry means fewer interruptions.
The standard pattern is short-lived access tokens (15 minutes to a few hours) plus longer-lived refresh tokens (days to weeks, sometimes months). The application uses the access token until it expires, then exchanges the refresh token for a new access token without bothering the user. If the refresh token is compromised, revoking it cuts off access at the next refresh attempt.
Refresh-token rotation strengthens this: each time a refresh token is used, the server invalidates it and issues a new one. If a refresh token is replayed (used twice), the server detects the second use as an attack and revokes the entire token chain. This bounds the damage from a leaked refresh token to the window between the legitimate use and the attacker's use.
Failed authentication: 401 vs 403
Two distinct failures, two distinct status codes:
- 401 Unauthorized. The request had no credentials, or the credentials were invalid (wrong key, expired token, bad signature). The client should re-authenticate before retrying.
- 403 Forbidden. The credentials were valid, but the authenticated identity isn't allowed to do what they're asking. The client should not retry with different credentials — the answer won't change.
Returning 401 for both — common — sends clients into pointless re-authentication loops on permission errors. Returning 403 for both leaks information about which credentials are valid. Get the distinction right.
What to log, what not to log
Log every authentication failure with enough detail to investigate (timestamp, source IP, attempted credential prefix, requested resource). Log every credential issuance and revocation. Do not log full credentials — ever. A bug in your logging pipeline becomes a credential dump otherwise. Truncate or hash credentials before they reach any persistent log.
Rate-limit authentication attempts per source IP and per identity, separately. The first defends against credential-stuffing attacks; the second defends against targeted attacks on a known user. See API Rate Limiting Strategies for the algorithms.
Common mistakes
- Sending API keys in URLs. They will end up in logs. Use the
Authorizationheader. - One credential type for everything. Server-to-server should not use the same credentials as user-delegated access. Mixing them couples revocation to the wrong scope.
- JWT for everything. JWTs are not always the right token format. Opaque session tokens are simpler and more revocable for user sessions.
- No expiry on tokens. Permanent credentials become permanent vulnerabilities the moment they leak.
- Implicit flow or password grant. Both are deprecated for good reasons. Authorization Code with PKCE for browsers and mobile; Client Credentials for server-to-server.
- Confusing 401 and 403. Costs you debug time and confuses clients.
- Storing tokens in localStorage. Vulnerable to XSS. Browser session tokens belong in HTTP-only cookies.
- Trusting JWT claims you didn't sign. Verify the signature first, then trust the claims. Decoding without verifying is the single most common JWT bug.
Where to go next
For practical implementation patterns, see the authentication guide. For the broader security picture, see API security. For how authentication interacts with rate limiting, see API Rate Limiting Strategies.