Token Design: JWT vs Opaque Tokens

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.

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.

Contents