If you're building an API, one of the first hard decisions you'll face is: how should clients prove who they are? The three terms that come up constantly — OAuth 2.0, JWT, and API keys — get thrown around interchangeably, but they aren't really competing options. They solve different problems, and understanding that difference is the key to choosing correctly.
This post goes deep on what each one actually is, how they work internally, where they fail in the real world, and which to reach for depending on your architecture.
The Core Confusion: These Aren't Apples to Apples
Before comparing them, it helps to clarify what category each thing belongs to:
- API Keys are a credential — a static secret string that identifies a client.
- OAuth 2.0 is a protocol — a framework for delegated authorization (granting access without sharing passwords).
- JWT (JSON Web Token) is a token format — a way of encoding claims (data) in a compact, signed, verifiable structure.
In practice, OAuth 2.0 often uses JWTs as the access token format. So the real comparison isn't three equal alternatives — it's more like: "a simple static secret" vs. "a delegation protocol" vs. "a token encoding standard that the protocol might use." Keeping this distinction in mind makes the rest of the decision much easier.
It also helps to separate two concepts that get conflated constantly:
- Authentication — proving who is making the request (a user, a service, a device).
- Authorization — determining what that identity is allowed to do.
API keys mostly handle authentication of a client application. JWTs can carry both authentication (identity claims) and authorization (scopes/roles) information. OAuth 2.0 is fundamentally an authorization framework — it's about granting scoped access, though it's frequently paired with OpenID Connect (OIDC) to also handle authentication.
API Keys: Simple, Static, Limited
An API key is typically a long random string issued to a client
(a developer, service, or application) that gets sent with every request,
usually in a header like
Authorization: Bearer <key> or
X-API-Key: <key>.
How it actually works under the hood
- A developer registers for API access; the server generates a cryptographically random string (often 32+ bytes, base64 or hex encoded) and stores a hash of it (never the raw key) in a database, associated with the client's account, rate limits, and permissions.
- The client stores the raw key and attaches it to every request.
- The server hashes the incoming key on each request and compares it against the stored hash — a simple lookup, no cryptographic verification of a signature involved.
GET /v1/orders HTTP/1.1
Host: api.example.com
Authorization: Bearer sk_live_51Hc9F2CZ...
Strengths
- Extremely simple to implement on both client and server.
- Good for machine-to-machine or server-to-server communication where there's no "user" to authenticate — just a system identifying itself.
- Easy to rate-limit, track usage per key, and revoke individually.
- No cryptographic library dependency needed for verification — just a database lookup.
Weaknesses
- No built-in expiration — a leaked key can be used indefinitely until manually revoked.
- No standard way to encode identity, scope, or permissions — that logic has to be built and maintained separately in your own database schema.
- Doesn't represent a user — it represents a client application, so it's a poor fit for anything involving individual user permissions or consent.
- If sent insecurely (URL query params, committed to a public git repo, logged in plaintext, embedded in client-side JavaScript), it's trivially easy to leak.
- Doesn't scale well to fine-grained permissions — you either build a whole permissions system around a flat string, or you issue dozens of keys per client.
Best practices if you go this route
- Prefix keys by environment and purpose (
sk_live_,sk_test_) so leaked keys are identifiable and rotation is easier. - Store only a hash of the key server-side, never the plaintext.
- Support key rotation without downtime by allowing two active keys per client during a rotation window.
- Log key usage patterns and alert on anomalies such as sudden request spikes or requests from a new geographic region.
- Bind keys to IP allowlists where feasible for extra defense in depth.
Best for: internal services, server-to-server integrations, simple third-party API access (e.g., a weather API, a payment gateway SDK, analytics ingestion) where you're authenticating an application, not a person.
JWT: A Token Format, Not an Auth System
A JSON Web Token is a compact, URL-safe, digitally signed token consisting of three base64url-encoded parts separated by dots: a header, a payload (claims), and a signature.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Decoded, that's:
Header
Declares the token type and signing algorithm:
{
"alg": "HS256",
"typ": "JWT"
}
Payload
The claims (data), which can include registered claims like
sub (subject/user ID), exp (expiration),
iat (issued at), iss (issuer),
aud (audience), plus any custom claims your application needs
(roles, permissions, tenant ID):
{
"sub": "1234567890",
"name": "John Doe",
"role": "admin",
"exp": 1735689600
}
Signature
The signature is computed over the header and payload using a secret (HMAC, e.g. HS256) or a private key (RSA/ECDSA, e.g. RS256/ES256), which lets any party with the corresponding secret or public key verify the token hasn't been tampered with.
Symmetric vs. asymmetric signing
This distinction matters a lot in practice:
- HS256 (symmetric): the same secret signs and verifies. Simple, but every service that needs to verify tokens also needs the ability to create valid tokens.
- RS256 / ES256 (asymmetric): a private key signs, and a public key verifies. This is the better choice in distributed systems — your auth server holds the private key, and downstream services can verify tokens using the public key without being able to forge one.
Strengths
- Self-contained — carries user ID, roles, expiration, and custom claims directly in the token.
- Stateless verification — servers don't need a database lookup to validate the token's authenticity.
- Widely supported — libraries exist in essentially every language.
- Naturally supports expiration through the
expclaim. - Extensible — custom claims can pass tenant IDs, permission sets, or feature flags.
Weaknesses and real vulnerabilities
- No easy revocation. Once issued, a JWT is valid until it expires unless you add a denylist.
- Payload is readable, not encrypted. Base64url encoding is not encryption. Never put passwords, secrets, or sensitive PII in a JWT payload.
alg: noneattacks. Accepted algorithms should always be explicitly whitelisted server-side.- Algorithm confusion (RS256 → HS256). The expected algorithm should be explicitly pinned per key or issuer.
- Token bloat. Too many claims increase request size and can hit header size limits.
Best practices
- Keep expiry short (5–15 minutes for access tokens is common) and pair with a refresh token flow.
- Prefer asymmetric algorithms (RS256/ES256) when multiple services need to verify tokens independently.
- Explicitly specify and validate the algorithm server-side.
- Validate
issandaudclaims. - Never store sensitive data in the payload.
Best for: representing an authenticated session or identity after login — passing user identity and permissions between microservices, or as the access token format within an OAuth 2.0 flow.
OAuth 2.0: Delegated Authorization
OAuth 2.0 solves a different problem entirely: how does a user grant a third-party application limited access to their resources on another service, without handing over their password?
Think of "Sign in with Google" or a scheduling app that needs read access to your calendar. OAuth defines four roles:
- Resource Owner — the user who owns the data.
- Client — the application requesting access.
- Authorization Server — issues tokens after authenticating the user and getting consent.
- Resource Server — the API that holds the protected data and validates tokens.
The main grant types (flows), and when each applies
1. Authorization Code Flow (with PKCE)
The gold standard for anything with a user and a browser or app involved.
- Client redirects the user to the authorization server's login/consent screen.
- User authenticates and approves the requested scopes.
- Authorization server redirects back to the client with a short-lived authorization code.
- Client exchanges that code plus a PKCE code verifier for an access token and refresh token.
PKCE (Proof Key for Code Exchange) adds a dynamically generated secret per login attempt, which prevents an intercepted authorization code from being usable by anyone other than the client that initiated the flow.
2. Client Credentials Flow
This is for machine-to-machine communication with no user involved at all. A service authenticates directly with its own client ID and secret and receives an access token representing the application itself, not a user.
3. Device Authorization Flow
This is designed for devices without a good browser or keyboard input, such as smart TVs and CLI tools.
4. Implicit Flow (deprecated)
Used historically to return tokens directly in the redirect URL fragment without a code exchange step. It is now discouraged; authorization code plus PKCE has replaced it even for SPAs.
5. Resource Owner Password Credentials (deprecated)
This flow collects the user's username and password directly and trades them for a token. It defeats the purpose of OAuth and should be avoided.
Token types in OAuth 2.0
- Access Token — short-lived, sent with API requests, often (but not required to be) a JWT.
- Refresh Token — long-lived, stored securely and used to obtain a new access token without requiring the user to log in again.
Strengths
- Purpose-built for delegated, scoped access.
- Supports refresh tokens, allowing access tokens to remain short-lived.
- Industry standard with mature libraries and identity providers.
- Scopes and consent screens provide visibility and control over shared access.
- Extensible via OpenID Connect (OIDC) for standardized authentication.
Weaknesses and common misconfigurations
- More complex to implement correctly.
- Open redirect vulnerabilities can occur when redirect URIs are not strictly validated.
- Missing PKCE on public clients can expose authorization codes.
- Overly broad scopes increase the damage from a compromised token.
- Insecure token storage can expose tokens to XSS.
- Can be overkill for simple service-to-service authentication.
Best for: scenarios involving user consent and delegated access, including third-party integrations, login with external identity providers, and multi-service ecosystems where users grant applications specific permissions.
Side-by-Side Comparison
| Dimension | API Key | JWT | OAuth 2.0 |
|---|---|---|---|
| What it is | Static credential | Token format | Authorization protocol |
| Represents | A client/application | An identity + claims | A delegated grant of access |
| Expiration | Usually none (manual revocation) | Built-in (exp claim) |
Access token short-lived, refresh token long-lived |
| Revocation | Immediate (delete from DB) | Hard without added infrastructure | Refresh token revocation is straightforward; access token revocation before expiry is hard |
| Involves end-user consent | No | Not inherently | Yes (except client credentials grant) |
| Verification cost | DB lookup | Cryptographic signature check | Code exchange + token verification |
| Best suited for | Simple service auth, third-party API access | Session/identity representation, service-to-service claims passing | User-delegated, scoped, revocable access |
| Implementation complexity | Low | Medium | High |
| Typical lifespan | Indefinite | Minutes to hours | Minutes (access) / days-weeks (refresh) |
How They Fit Together in a Real System
A very common real-world setup actually combines all three:
- A user logs in via an OAuth 2.0 authorization code flow with PKCE if it's a mobile app or SPA.
- The authorization server issues a JWT as the access token, encoding the user's identity, roles, and granted scopes, signed with RS256.
- Each downstream microservice verifies that JWT locally using the auth server's public key.
- A separate internal batch job or third-party integration authenticates using a simple API key or the OAuth client credentials grant.
So the question usually isn't "which one should I use forever," but "which layer of my system am I authenticating, and what does that layer actually need?"
A Practical Decision Guide
| Situation | Best Fit |
|---|---|
| Authenticating a backend service calling another backend service | API key or OAuth client credentials grant |
| A public-facing API for external developers | API key or OAuth if scoped, user-linked access matters |
| Users logging into your app with sessions/permissions | JWT as the session/access token |
| Users granting a third-party app access to their data | OAuth 2.0 (authorization code + PKCE) |
| Mobile or SPA apps needing secure, short-lived tokens | OAuth 2.0 with JWT access tokens + rotating refresh tokens |
| CLI tool or smart TV app | OAuth 2.0 device authorization flow |
| Internal tooling with a handful of trusted clients | API key is often enough |
| Multi-tenant SaaS platform with per-tenant permissions | OAuth 2.0 + JWT carrying tenant/role claims |
Security Considerations Worth Remembering
- Always use HTTPS/TLS. None of these methods are safe over plain HTTP.
- Prefer short-lived tokens with refresh mechanisms over long-lived static credentials wherever possible.
- Never store secrets or sensitive data in a JWT payload.
- Rotate and scope API keys with the minimum permissions needed.
- Validate JWT signatures and algorithms explicitly on the server.
- Use PKCE with OAuth 2.0 for public clients such as mobile apps and SPAs.
- Rotate refresh tokens on use and detect reuse of already-rotated tokens.
- Audit OAuth redirect URI allowlists regularly.
- Log and monitor authentication failures and unusual token usage patterns.
Frequently Asked Questions
Can I use a JWT instead of an API key?
Yes, and many APIs do. A self-issued, long-lived JWT can serve a similar role to an API key while adding built-in expiration and embedded metadata. The trade-off is that you now need key-management infrastructure for signing and verification.
Does OAuth 2.0 replace API keys entirely?
Not necessarily. OAuth's client credentials grant is a reasonable replacement for API keys in service-to-service scenarios, but for very simple use cases, a plain API key can involve less operational overhead when it is issued, stored and rotated properly.
Is OAuth 2.0 the same as OpenID Connect (OIDC)?
No. OAuth 2.0 handles authorization (delegated access). OIDC is an identity layer
built on top of OAuth 2.0 that standardizes authentication, adding the ID token
and a /userinfo endpoint.
The Bottom Line
API keys, JWTs, and OAuth 2.0 aren't rivals — they're tools that operate at different layers of the authentication and authorization stack. API keys identify a client. JWTs carry verifiable claims about an identity. OAuth 2.0 orchestrates how access gets delegated and consented to.
The right choice comes down to one question: are you authenticating a machine, a session, or a user granting permission to another party? Answer that, and the "which one" question mostly answers itself. In most non-trivial systems, the real answer ends up being some combination of all three, each handling the layer it's actually good at.