Token Design: JWT vs Opaque Tokens
Introduction
Token format is a foundational decision for API security and scalability. JSON Web Tokens (JWTs) provide self-contained claims, while opaque tokens force introspection against an authorization server. Both approaches can be correct depending on latency, revocation, and trust boundaries.
Decision Drivers
Selecting a token design should be based on operational constraints rather than preference.
- Latency budget: JWTs allow offline validation, opaque tokens require network calls.
- Revocation strategy: Opaque tokens can be revoked centrally, JWTs need short TTLs or revocation lists.
- Trust boundary: JWTs leak claims to every consumer, opaque tokens keep claims hidden.
- Key management: JWTs require key rotation and JWKS distribution.
JWT Characteristics
JWTs are signed and optionally encrypted. They are ideal when resource servers need claims without calling the issuer.
- Best for high-throughput APIs and low latency.
- Require strict issuer, audience, and signature validation.
- Must enforce short expirations and rotation for sensitive scopes.
Opaque Token Characteristics
Opaque tokens are random identifiers that are meaningless outside the issuer.
- Best for centralized policy enforcement and immediate revocation.
- Introspection response can include dynamic context (risk score, device trust).
- Requires reliable network access to the authorization server.
Hybrid Models
Many systems combine both strategies.
- Use JWTs internally between trusted services.
- Use opaque tokens for external clients and high-risk scopes.
- Wrap opaque tokens into session-bound caches to reduce latency.
Python Example: Local JWT Validation vs Introspection
The following Python example shows how an API can validate JWTs locally or fall back to token introspection for opaque tokens.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import time
import jwt
import requests
JWKS_ISSUER = "https://login.example.com"
CLIENT_ID = "payments-api"
INTROSPECTION_URL = "https://login.example.com/oauth2/introspect"
def validate_jwt(token: str, public_key: str) -> dict:
payload = jwt.decode(
token,
public_key,
algorithms=["RS256"],
audience=CLIENT_ID,
issuer=JWKS_ISSUER,
options={"require": ["exp", "iat", "iss", "aud"]},
)
return payload
def introspect_token(token: str, client_id: str, client_secret: str) -> dict:
response = requests.post(
INTROSPECTION_URL,
data={"token": token},
auth=(client_id, client_secret),
timeout=2,
)
response.raise_for_status()
data = response.json()
if not data.get("active"):
raise ValueError("Token inactive")
return data
Operational Considerations
- Use short TTLs for JWTs to limit blast radius.
- Cache JWKS keys and handle key rotation gracefully.
- Instrument introspection latency and error rates.
- Avoid putting sensitive PII inside JWT claims unless encrypted.
Conclusion
JWTs and opaque tokens are not competing primitives; they are complementary tools. Choose the format based on revocation requirements, latency budgets, and whether every resource server should see the user claims.