OAuth 2.0 vs JWT vs API Keys
OAuth 2.0, JWT, and API keys are frequently compared as if they were interchangeable authentication mechanisms. They are not. OAuth 2.0 is an authorization framework, JWT is a token format, and an API key is a credential. A production system may use one of them, combine several of them, or use completely different mechanisms depending on the trust model.
The correct choice depends on who is calling the API, whether access is delegated, how credentials are rotated, how quickly permissions must be revoked, and whether services need to validate credentials without contacting a central authorization server on every request.
The important engineering question is therefore not which mechanism is universally better. It is which credential and authorization model produces the right security, latency, scalability, revocation, and operational characteristics for a particular API.
Table of Contents
- OAuth 2.0, JWT, and API Keys Are Different Things
- API Keys
- JWT
- OAuth 2.0
- OAuth 2.0 vs JWT vs API Keys
- Choosing the Right Approach
- Failure Scenarios and Security Trade-Offs
- Production Design Example
- Ready-to-Use Example
- Common Mistakes
- Production Checklist
- Conclusion
- More Articles to Read
OAuth 2.0, JWT, and API Keys Are Different Things
Comparing these technologies directly can be misleading because they operate at different layers.
OAuth 2.0
|
| authorization framework
| defines how access is delegated
v
Access Token
|
| token may be JWT
| or opaque
v
Protected API
JWT
|
| token representation
| carries signed claims
v
Can represent:
access tokens
identity assertions
other signed data
API Key
|
| direct credential
v
Identifies client or integration
OAuth 2.0 can issue JWT access tokens, but it does not require JWT. An OAuth access token can be opaque and require server-side introspection.
Similarly, JWT does not automatically mean OAuth. An application can issue its own JWT-based session or service credential without implementing OAuth 2.0.
An API key is simpler. The client presents a secret value that identifies or authenticates an integration:
Client
|
| X-API-Key: secret-value
v
API
|
v
Key Lookup
|
v
Client Identity
The first architectural decision should therefore be the required trust model rather than the token syntax.
| Technology | What It Is | Main Purpose |
|---|---|---|
| OAuth 2.0 | Authorization framework | Delegated and controlled API access |
| JWT | Signed token format | Carry verifiable claims |
| API key | Credential | Identify or authenticate API clients |
API Keys
API keys are useful when a system needs a simple credential for a known machine or integration. They are common for server-to-server integrations where complex user delegation is unnecessary.
A typical request might contain:
GET /v1/shipments
X-API-Key: k_live_...
The server stores a representation of the key and resolves it to an integration identity:
API Key
|
v
Credential Store
|
v
Integration
|
+--> account_id = 82
+--> scopes = shipments:read
+--> status = active
Keys should generally be treated like passwords. Storing only a cryptographic hash or otherwise protected representation reduces the impact of credential-store exposure.
Advantages:
- simple to implement;
- easy for API clients to understand;
- low validation overhead;
- works well for stable server-to-server integrations;
- easy to associate with quotas and usage reporting.
Disadvantages:
- often long-lived;
- clients may accidentally commit them to source repositories;
- rotation can be operationally difficult;
- a key often provides direct access until revoked;
- poorly designed systems give all keys identical permissions.
API keys become considerably safer when each integration receives its own key and permission set:
Integration A
key=A
scopes=
shipments:read
Integration B
key=B
scopes=
shipments:read
shipments:create
Integration C
key=C
scopes=
tracking:read
This allows individual revocation without affecting every customer.
Key rotation should support overlapping credentials:
Existing key
|
+-------------------+
| |
v v
still valid new key issued
|
v
client migrates
|
v
old key revoked
Immediately invalidating the old key before clients deploy the new one can create unnecessary outages.
When to use:
- trusted server-to-server integrations;
- internal automation;
- simple partner APIs;
- workloads where delegated user access is unnecessary.
When not to use:
- browser applications where the credential cannot remain secret;
- complex delegated user authorization;
- systems requiring short-lived user sessions;
- cases requiring rich dynamic identity claims in every request.
JWT
A JSON Web Token can carry signed claims that the API validates without looking up central session state for every request.
Conceptually, the payload might contain:
{
"sub": "user-481",
"iss": "https://identity.example.com",
"aud": "shipment-api",
"exp": 1787605200,
"account_id": "82",
"scope": "shipments:read shipments:create"
}
The signature protects the claims against unauthorized modification. It does not normally make the claims secret. JWT contents should therefore be treated as readable by whoever possesses the token unless encryption is explicitly used.
JWT is attractive in distributed architectures because APIs can validate tokens locally:
Identity Provider
|
| issues signed JWT
v
Client
|
+------------+------------+
| | |
v v v
Service A Service B Service C
| | |
local verify local verify local verify
This removes a central session lookup from the normal request path.
JWT Validation
Production validation must do more than verify a signature.
At minimum, the API should validate relevant properties such as:
signature
issuer
audience
expiration
not-before
allowed algorithm
required claims
Conceptually:
JWT
|
v
Parse Header
|
v
Is algorithm allowed?
|
v
Find trusted key
|
v
Verify signature
|
v
Validate issuer
|
v
Validate audience
|
v
Validate time claims
|
v
Create trusted identity
The API should not simply decode a token and trust its payload.
A token signed by a trusted provider but intended for another application should also be rejected when the audience does not match.
Advantages:
- local validation is fast;
- avoids centralized session lookup for every request;
- scales naturally across many API instances;
- can carry identity and authorization context;
- works well across distributed services.
Disadvantages:
- immediate revocation is more complicated;
- large tokens increase request bandwidth;
- claims can become stale before expiration;
- key rotation must be coordinated;
- developers sometimes store sensitive information in readable payloads.
JWT Revocation and Expiration
Stateless validation produces an important trade-off: once a valid token has been issued, an API can continue accepting it until expiration even if permissions have changed centrally.
Token issued
|
| permissions = read/write
|
v
Permissions revoked centrally
|
v
Old token still valid
|
v
Token eventually expires
Common strategies include:
- short-lived access tokens;
- refresh tokens for obtaining new access tokens;
- revocation lists for high-risk cases;
- token-version checks;
- central introspection when immediate control is required.
Every additional centralized check reduces some of the scalability advantage of stateless tokens.
The trade-off can be represented as:
More local validation
|
+--> lower latency
+--> higher availability
+--> slower revocation
More centralized validation
|
+--> faster revocation
+--> fresher policy
+--> extra latency
+--> central dependency
OAuth 2.0
OAuth 2.0 provides a framework for obtaining access tokens that allow a client to access protected resources. Its main value is separating several roles that are often incorrectly collapsed into one credential.
Resource Owner
|
v
Client Application
|
v
Authorization Server
|
| access token
v
Client Application
|
v
Resource Server / API
The authorization server manages credential issuance and authorization flows. The resource server validates the resulting access token and enforces permissions.
This architecture allows APIs to avoid directly handling a user's primary authentication credential.
Delegated Access
Delegation is one of the main reasons OAuth exists.
Consider a logistics customer authorizing a third-party analytics application to read shipments.
Without delegated authorization, the customer might need to give that application a username and password or a broad API credential.
OAuth provides a different model:
User
|
| authorizes
v
Analytics App
|
| receives limited token
v
Shipment API
Allowed:
shipments:read
Not allowed:
shipments:create
users:manage
billing:manage
The client receives limited access rather than the user's primary credential.
Authorization can also be revoked independently from the user's account password.
Machine-to-Machine Access
OAuth can also support machine identities.
Partner Backend
|
| client credentials
v
Authorization Server
|
| short-lived access token
v
Partner Backend
|
v
Shipment API
This differs from a static API key because the long-lived credential is used to obtain short-lived access tokens rather than being presented directly to the API on every request.
The architecture can therefore reduce exposure:
Long-lived client secret
|
| used only with
v
Authorization Server
|
v
Short-lived access token
|
v
API
Advantages:
- supports delegated access;
- supports scoped permissions;
- supports short-lived access tokens;
- separates authorization server from resource APIs;
- works for both user and machine access patterns.
Disadvantages:
- significantly more protocol complexity than API keys;
- authorization server becomes critical infrastructure;
- incorrect flow selection can introduce vulnerabilities;
- token refresh and revocation require lifecycle management;
- clients need more sophisticated implementation.
OAuth is valuable when the access model requires it. Using it for a small trusted internal script with one stable backend may create unnecessary complexity.
OAuth 2.0 vs JWT vs API Keys
The comparison becomes clearer when each technology is evaluated according to the problem it solves rather than treating them as direct substitutes.
| Property | OAuth 2.0 | JWT | API Key |
|---|---|---|---|
| Type | Authorization framework | Token format | Credential |
| Delegated user access | Strong fit | Not by itself | Poor fit |
| Machine-to-machine use | Strong fit | Possible | Strong fit for simple cases |
| Local API validation | Depends on token type | Yes | Usually requires lookup |
| Short-lived credentials | Common | Common | Less common |
| Immediate revocation | Depends on architecture | Difficult when stateless | Easy after lookup/store update |
| Permission scopes | Native concept | Can carry them | Can be associated server-side |
| Implementation complexity | High | Medium | Low |
| Credential rotation | Built into broader lifecycle | Signing keys and tokens | Must be designed explicitly |
The most important observation is that combinations are common:
OAuth 2.0
+
JWT access token
OAuth 2.0
+
opaque access token
API key
+
server-side scopes
JWT
+
custom authentication system
Choosing the Right Approach
The best approach follows from the client and trust relationship.
Public browser or mobile application.
A browser or mobile app cannot safely preserve a permanent secret in the same way as a backend server. User-oriented authorization flows with short-lived access tokens are usually more appropriate.
Third-party application acting for a user.
User
|
| grants limited access
v
Third-Party App
|
v
OAuth authorization
This is a strong OAuth use case because delegated authorization is the actual problem.
Trusted customer backend integration.
Two approaches may be reasonable:
Simple requirements
|
v
Scoped API key
More advanced security lifecycle
|
v
OAuth machine credentials
|
v
short-lived access token
The second option adds infrastructure but can improve credential lifecycle and scope management.
Internal microservices.
Static API keys shared between dozens of microservices scale poorly because rotation, ownership, and compromise containment become difficult.
Prefer workload identity or short-lived service credentials where infrastructure supports them:
Service A
|
| workload identity
v
Identity Infrastructure
|
v
short-lived credential
|
v
Service B
Webhook sender.
A full OAuth deployment may be unnecessary. A shared signing secret or asymmetric request signature can often provide a more appropriate verification model than sending a reusable bearer credential.
Failure Scenarios and Security Trade-Offs
Credential systems live on critical request paths, so their failure behavior must be designed before an outage occurs.
Authorization server unavailable.
Existing locally validated tokens may continue working while new token issuance or refresh fails.
Authorization Server
X
unavailable
Existing access token
|
v
API validates locally
|
v
request continues
Expired access token
|
v
refresh needed
|
X
request cannot obtain new access
This isolates existing API traffic from short authorization-server outages when access tokens can be validated locally.
JWT signing-key rotation.
A new key should normally overlap with the previous verification key long enough for already issued tokens to expire.
Time ---->
Key A signs
========
Key A verifies
====================
Key B signs
============
Key B verifies
========================
Removing Key A immediately when Key B begins signing can invalidate active tokens.
API key leaked.
A leaked API key may remain useful until revoked. Individual keys, narrow scopes, quotas, anomaly detection, and rotation reduce the blast radius.
JWT leaked.
A stolen bearer token can generally be used by whoever possesses it until expiration or revocation controls intervene. Short lifetimes limit the exposure window.
Refresh token leaked.
Refresh credentials are often more sensitive because they can be exchanged for new access tokens. They should receive stronger storage protection and lifecycle controls than short-lived access tokens.
Credential store unavailable.
API key validation may fail if each request requires a database lookup. Local or distributed caching can improve availability, but revocation then becomes eventually consistent.
Key revoked centrally
|
v
cached validation remains
|
v
cache TTL expires
|
v
revocation takes full effect
Identity claims change.
If a JWT contains roles or permissions, those claims can remain stale until the token expires. High-risk authorization should therefore consider whether dynamic policy should be checked independently from token claims.
Production Design Example
Consider a logistics platform supporting three types of API clients:
- interactive users using the web application;
- customer backend integrations;
- internal services.
The architecture uses different credential models for each trust relationship:
Identity Platform
/ | \
/ | \
v v v
User OAuth Partner OAuth Workload
| | Identity
v v |
JWT AT JWT AT |
| | |
v v v
Web Client ----> API Gateway <----- Partner Backend
|
v
Shipment Service
/ \
v v
Booking Service Account Service
|
v
Booking Queue
User flow.
An interactive user authenticates through the identity platform and receives a short-lived access token.
User
|
v
Identity Platform
|
| short-lived access token
v
Web Application
|
v
API
The access token carries bounded identity information:
{
"sub": "user-481",
"aud": "logistics-api",
"account_id": "82",
"scope": "shipments:read shipments:create",
"exp": 1787605200
}
The API gateway validates token integrity and trust-domain claims. Shipment Service still performs resource-level authorization.
Partner flow.
A customer backend obtains a short-lived access token using machine credentials:
Customer Backend
|
| client credential
v
Authorization Server
|
| short-lived access token
v
Customer Backend
|
v
Logistics API
The access token might contain only:
shipments:read
shipments:create
tracking:read
It cannot manage users or billing.
Legacy partner flow.
A smaller partner integration may use a scoped API key:
API Key:
partner-key-812
Server-side policy:
account_id=82
scopes=
tracking:read
rate_limit=
100 requests/min
This is acceptable when the additional OAuth lifecycle would not provide enough benefit to justify its complexity.
Internal service flow.
Booking Service should not use a customer API key or user access token as its infrastructure identity.
Booking Service
|
| workload credential
v
Carrier Credential Service
User context may be propagated separately for audit purposes when the workflow originated from a user request.
Failure flow.
If the authorization server becomes temporarily unavailable:
- existing valid JWT access tokens continue to work;
- new user sessions cannot begin;
- expired partner tokens cannot refresh;
- internal workload identities remain independent if they use separate infrastructure.
This limits the blast radius of one identity-system failure.
Monitoring. The platform tracks token-validation failures, token-issuance latency, refresh failures, API-key rejections, expired-token rates, authorization-server availability, and authentication latency.
Scaling. JWT validation happens locally at API instances using cached verification keys. This prevents a central token introspection service from becoming a request-per-second bottleneck.
Deployment. Signing-key changes overlap old and new keys. Permission changes are tested against both user and machine credentials before rollout.
Ready-to-Use Example
A production application should convert credentials into one trusted internal identity model so business authorization does not care whether the caller used OAuth, JWT, or an API key.
from dataclasses import dataclass
from enum import StrEnum
class CredentialType(StrEnum):
ACCESS_TOKEN = "access_token"
API_KEY = "api_key"
WORKLOAD = "workload"
@dataclass(frozen=True)
class Identity:
subject: str
account_id: int | None
scopes: frozenset[str]
credential_type: CredentialType
JWT validation should verify the trust boundary rather than merely decode the payload:
from dataclasses import dataclass
from typing import Any
import jwt
@dataclass(frozen=True)
class JwtConfig:
issuer: str
audience: str
algorithms: tuple[str, ...]
CONFIG = JwtConfig(
issuer="https://identity.example.com",
audience="logistics-api",
algorithms=("RS256",),
)
def validate_access_token(
token: str,
public_key: str,
) -> Identity:
claims: dict[str, Any] = jwt.decode(
token,
public_key,
algorithms=list(CONFIG.algorithms),
issuer=CONFIG.issuer,
audience=CONFIG.audience,
options={
"require": [
"exp",
"iss",
"aud",
"sub",
],
},
)
scopes = frozenset(
claims.get("scope", "").split()
)
account_id = claims.get("account_id")
return Identity(
subject=claims["sub"],
account_id=(
int(account_id)
if account_id is not None
else None
),
scopes=scopes,
credential_type=CredentialType.ACCESS_TOKEN,
)
The allowed algorithm is configured by the server rather than taken from untrusted token input as an authorization decision.
API keys should be stored using a protected representation rather than plaintext whenever the application does not need to recover the original value:
CREATE TABLE api_credentials (
id BIGSERIAL PRIMARY KEY,
key_prefix VARCHAR(16) NOT NULL,
key_hash VARCHAR(128) NOT NULL UNIQUE,
account_id BIGINT NOT NULL,
scopes TEXT[] NOT NULL,
status VARCHAR(16) NOT NULL,
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_used_at TIMESTAMPTZ
);
CREATE INDEX idx_api_credentials_prefix
ON api_credentials (key_prefix);
A prefix allows the service to narrow candidate credentials without storing the complete secret.
Credential lookup should check lifecycle state:
SELECT
id,
account_id,
scopes,
status,
expires_at
FROM api_credentials
WHERE key_prefix = $1
AND status = 'active'
AND (
expires_at IS NULL
OR expires_at > NOW()
);
The application can then compare the presented key against stored candidate hashes using an appropriate constant-time verification mechanism.
Authorization remains identical regardless of credential type:
from fastapi import HTTPException, status
def require_scope(
identity: Identity,
scope: str,
) -> None:
if scope not in identity.scopes:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions",
)
This separation is important:
OAuth / JWT / API Key
|
v
Authentication
|
v
Identity
|
v
Authorization
|
v
Business Logic
Authentication mechanisms can evolve without forcing every application authorization rule to understand credential-specific details.
Security events should identify credential type without exposing the credential:
import json
import logging
from dataclasses import asdict, dataclass
logger = logging.getLogger("security")
@dataclass(frozen=True)
class AuthenticationEvent:
subject: str | None
credential_type: str
result: str
reason: str
trace_id: str
def record_authentication(
event: AuthenticationEvent,
) -> None:
logger.info(
json.dumps({
"event": "authentication_result",
**asdict(event),
})
)
Access tokens, refresh tokens, client secrets, and API keys should never be written into these logs.
Common Mistakes
| Mistake | Production Impact | Better Approach |
|---|---|---|
| Calling JWT an authentication protocol | Token format and authorization architecture become confused. | Treat JWT as a token representation. |
| Assuming OAuth requires JWT | Architecture becomes unnecessarily coupled to one token format. | Choose token representation separately from the OAuth flow. |
| Using OAuth for every simple integration | Protocol complexity increases without meaningful security benefit. | Use scoped API keys where the trust model is simple. |
| Using one API key for every customer | One leak compromises every integration. | Issue independent credentials. |
| Giving API keys unlimited scope | A leaked key provides broad access. | Associate narrow permissions and quotas with each key. |
| Storing API keys in plaintext | Credential-store compromise exposes immediately usable secrets. | Store protected representations when possible. |
| Using API keys in public browser applications | The secret can be extracted by users or attackers. | Use an appropriate user authorization flow. |
| Only decoding JWTs | Untrusted claims may be treated as authenticated identity. | Verify signature and trust-domain claims. |
| Skipping JWT audience validation | Tokens intended for another API may be accepted. | Validate the expected audience. |
| Skipping JWT issuer validation | Tokens from an unintended identity system may be accepted. | Pin trusted issuers. |
| Storing sensitive data in JWT payloads | Bearer-token holders can read data that was assumed private. | Keep claims minimal and non-secret. |
| Using very long-lived JWT access tokens | Stolen tokens remain usable for extended periods. | Use short-lived access tokens. |
| No signing-key rotation plan | Compromise response and routine rotation can cause outages. | Support overlapping verification keys. |
| Putting all authorization decisions in token claims | Permissions remain stale until the token changes. | Keep dynamic resource authorization in the application or policy layer. |
| Logging bearer credentials | Observability infrastructure becomes a credential exposure path. | Log credential metadata, never credential values. |
Production Checklist
- Define the client type: distinguish users, public applications, trusted backends, partners, and internal services.
- Define the trust relationship: determine whether access is direct or delegated.
- Separate token format from authorization protocol: do not treat JWT and OAuth as the same concept.
- Use OAuth for delegated access: avoid sharing user credentials with third parties.
- Use short-lived machine tokens where justified: reduce exposure of long-lived credentials.
- Use API keys only where clients can protect them: avoid embedding permanent secrets in public applications.
- Issue unique API keys: keep credentials isolated by integration.
- Scope API keys: associate only the permissions each integration requires.
- Apply quotas per credential: prevent one integration from exhausting shared capacity.
- Protect stored API keys: avoid plaintext storage when recoverability is unnecessary.
- Support API-key rotation: allow temporary overlap between old and new keys.
- Validate JWT signatures: never trust decoded claims without verification.
- Pin allowed algorithms: configure acceptable algorithms server-side.
- Validate JWT issuer: trust only approved identity systems.
- Validate JWT audience: ensure tokens target the current API.
- Validate expiration: reject expired access tokens.
- Validate not-before when used: reject credentials not yet valid.
- Keep JWT claims minimal: avoid unnecessary sensitive or rapidly changing data.
- Use short access-token lifetimes: reduce the impact of token theft.
- Protect refresh credentials: treat them as high-value long-lived secrets.
- Define revocation behavior: know how quickly compromised credentials can be disabled.
- Plan signing-key rotation: overlap verification keys safely.
- Cache verification keys carefully: avoid a network call to the identity provider for every request.
- Monitor key-refresh failures: detect stale verification material.
- Keep authorization separate: a valid credential does not imply access to every resource.
- Enforce tenant ownership: combine credential scopes with resource-level checks.
- Use workload identity internally: avoid sharing static API keys across large microservice fleets.
- Monitor authentication failures: detect broken clients and credential attacks.
- Monitor token-issuance availability: identity infrastructure is production-critical.
- Never log credentials: redact access tokens, refresh tokens, client secrets, and API keys.
Conclusion
OAuth 2.0, JWT, and API keys solve different problems. OAuth 2.0 defines how clients obtain delegated or machine access to protected resources. JWT provides a signed representation that can carry verifiable claims and enable efficient local validation. API keys provide a simpler direct credential model that remains useful for controlled server-to-server integrations.
The right architecture depends on the client and the trust relationship. OAuth is valuable when delegation, token lifecycle, and scoped authorization matter. JWT is useful when distributed APIs need locally verifiable tokens. API keys remain practical when the integration model is simple and credentials can be protected, scoped, rotated, and revoked safely.
Key Takeaway: Do not choose OAuth, JWT, or API keys based on popularity or implementation convenience. First define who the client is, whether access is delegated, how credentials are stored and rotated, how quickly access must be revoked, and where authorization decisions belong. Then choose the simplest credential and token architecture that meets those production security requirements.
More Articles to Read
- API Security Explained: Threats and Defense Strategies
- Authentication vs Authorization
- Designing Secure API Architectures
- Protecting APIs Against Common Attacks
- Secrets Management in Cloud Applications
- API Security Best Practices for Production Systems
Comments (0)