Security
Part 3 of 7 · OAuth & OIDCJWT vs Opaque Tokens — Validation, JWKS & Revocation
JWTs validate locally via JWKS (iss/aud/exp/kid) and scale reads; opaque tokens make revocation trivial via introspection or a store. Choosing wrong means day-long stolen JWTs or introspection bottlenecks at the edge. Pair short AT TTL with refresh rotation and never skip audience checks.
- 1Gist
- 2Maps
- 3Q&A
- 4Sandbox
Voice readout needs Web Speech Synthesis in this browser.
JWT vs opaque is a revocation vs fan-out trade-off
Prefer
Short JWT + rotation (typical API edge)
Local verify at high QPS. Stolen-AT window equals TTL. Pair with refresh rotation so you can mint the next JWT without a day-long bearer.
- iss / aud / exp / kid every request — no AS hop on the hot path.
- Works offline and multi-region once JWKS is cached.
- Asymmetric (RS256/ES256) so resource servers cannot forge tokens.
- Revoke by TTL, optional jti denylist, or session version.
Alternative
Opaque reference + introspection (or a session store)
Delete the row and the token is dead. You pay an AS/store hop (or a cache that can lie past revoke).
- Best when instant revoke or hiding claims matters.
- Global edge without a replicated store → introspection outage amplifier.
- Cache active=true past revoke and you reintroduced JWT's problem.
- Some gateways exchange opaque for a short JWT at the edge.
Resource server validation
Same pipeline as the flowchart. Fail closed on any miss.
- 1
Shape the bearer
JWT (three base64url segments) vs opaque random. Do not guess alg from the token alone. - 2
JWT: resolve kid via JWKS cache
Unknown kid → refresh JWKS with backoff. Never fetch a JWKS URL from unverified token claims. - 3
Verify sig + iss + aud + exp
Allowlisted alg. Clock skew of a few seconds. aud must be this API. - 4
Opaque: introspect or session store
RFC 7662. Cache positives shorter than remaining TTL; cache negatives even shorter. - 5
Authorize scopes
Signature success is not permission. Check scope / resource indicators.
Overview
Access tokens are either self-contained JWTs (verify locally via JWKS) or opaque references (introspection or a token store). JWTs scale reads and enable offline validation; opaque tokens make revocation trivial and keep claims server-side.
Interviews dig into iss / aud / exp checks, JWKS rotation (kid), and why "JWT + 24h TTL + no denylist" is a revocation footgun. The spine of this page stays JWT versus opaque. Anatomy and algorithm pitfalls are the depth underneath that choice. Pair with refresh rotation and the threat model. ID tokens are always JWTs, but they are for the client — OIDC lesson.
Anatomy
A compact JWS is base64url(header).base64url(payload).base64url(signature).
| Part | Typical fields | Trust |
|---|---|---|
| Header | alg, typ, kid | Untrusted until the signature checks out |
| Payload | iss, aud, exp, and friends | Also untrusted until the signature checks out |
| Signature | Algorithm-specific bytes | Proves integrity with the AS private key or HMAC secret |
Base64url is encoding, not encryption. Anyone holding the token can read the payload. JWE is the encrypted form. Do not log full bearer tokens; the payload may contain PII.
Flow
- 1
JWT string
- nextHeader JSON
- nextPayload JSON
- nextSignature
- 2
Header JSON
- nextSelect key by kid from your JWKS
- 3
Payload JSON
- 4
Signature
- 5
Select key by kid from your JWKS
- nextVerify signature over header.payload
- 6
Verify signature over header.payload
- nextCheck iss aud exp nbf iat
- 7
Check iss aud exp nbf iat
- nextAccept or 401
- 8
Accept or 401
Comparative
| Property | JWT (typical) | Opaque |
|---|---|---|
| Validation | Local crypto + claims | Call AS introspection or DB |
| Revocation | Hard (TTL, denylist, short exp) | Easy (delete row) |
| Size | Larger; leaks claims if not careful | Small reference |
| RS coupling | Needs JWKS cache + rotation | Needs network to AS/store |
| Offline / multi-region | Great | Needs replicated store or latency |
| Debugging | Readable payload (Base64) | Needs AS logs |
What fails if you choose wrong
- Fat JWT with PII in
localStorage→ XSS exfiltrates identity. - JWT TTL = 24h, no denylist → stolen token lives all day.
- Opaque at a global edge without cache → introspection becomes the bottleneck / outage amplifier.
- Skipping
aud→ confused deputy / token reuse across APIs.
JWT validation checklist (resource server)
| Check | Rule |
|---|---|
| Algorithm | Allowlist RS256 or ES256 before crypto. Reject none and anything else. |
| Signature | Verify with your JWKS key for kid. Deny unknown kid. |
iss | Exact match against an issuer allowlist |
aud | Must include this API |
exp | now is before exp, plus a few seconds of skew |
nbf | now is at or after nbf, minus skew |
iat | Reject a far-future iat |
scope / roles | Least privilege for this route |
sub | Present when the route needs a user |
- Fetch signing keys from the JWKS URL; cache; select by
kid. - Verify signature (RS256/ES256 — prefer asymmetric; reject
alg=noneand unexpected algs). - Check
exp/nbf/iatwith small clock skew (about 30–60 seconds; a huge skew widens replay). - Check
issequals the expected issuer. - Check
audmatches this API. - Check
scopeor roles for the route. Bind mTLS /cnfwhen the token is sender-constrained.
Algorithm pitfalls
| Pitfall | Attack | Defense |
|---|---|---|
alg=none | Attacker strips the signature; a loose library accepts it | Allowlist only. Reject none before you look at the signature. |
| Algorithm confusion | Header says HS256; library HMACs with the RSA public key bytes | Separate HMAC and asymmetric verifiers. Never feed a public key into HMAC. |
kid injection | kid points at a file path, a URL, or a poisoned JWKS | Strict kid charset. Fetch only your configured JWKS over pinned HTTPS. |
Trusting header alg | Attacker picks a weak algorithm | The server decides the allowlist. The header only selects among approved algs. |
| Long-lived JWT | Stolen bearer works for hours | Access TTL in minutes, plus refresh rotation |
| Sensitive claims | Logs and XSS expose PII | Minimize claims. JWE only when intermediaries must not read them. |
jti is the JWT id you store on a denylist until exp. Nested JWTs are rarely worth the complexity — keep ID tokens and access tokens as separate layers.
Sequence
- 1
Attacker → Attacker
Step 1 Decode a real token, set alg to none, drop the signature
- 2
Attacker → API
Step 2 Bearer header.payload. with an empty signature
- 3
API → Attacker
Step 3 401 - alg not allowed
- 4
API → API
Failure path - signature check skipped
- 5
API → Attacker
200 - auth bypass
Lesson map
JWT vs Opaque Tokens — Validation, JWKS & Revocation
JWTs validate locally via JWKS (iss/aud/exp/kid) and scale reads; opaque tokens make revocation trivial via introspection or a store. Choosing wrong means day-long stolen JWTs or introspection bottlenecks at the edge. Pair short AT TTL with refresh rotation and never skip audience checks.
Architecture. Attacker Ready. API Ready. Step 1 Decode a real token, set alg to none, drop the signature Ready. Failure path - signature check skipped Ready
Select a node to see why it exists, or an edge to see the protocol, direction, effect, and consequence.
Mermaid export
flowchart TB a["Attacker Ready"] api["API Ready"] step_1["Step 1 Decode a real token, set alg to none, drop the signature Ready"] fail["Failure path - signature check skipped Ready"] a -->|Step 1 Decode a real token, set alg to none, drop the signature| step_1 a -->|Step 2 Bearer header.payload. with an empty signature| api api -->|Step 3 401 - alg not allowed| a api -->|Failure path - signature check skipped| fail api -->|200 - auth bypass| a
JWKS rotation
- The AS publishes multiple keys during rotation; the old
kidremains until all in-flight JWTs expire. - Resource servers must not pin a single certificate; refresh JWKS on unknown
kid(with rate limits / backoff). - Compromise: remove the key immediately; accept outage for tokens still signed with it (or an emergency
jtidenylist).
Revocation strategies for JWTs
- Short access TTL (5–15 min) + refresh rotation.
- Denylist by
jtiuntilexp(Redis) — works, adds state (you are halfway to opaque). - Version / session id claim checked against a session store on each request.
- Sender-constrained tokens (DPoP / mTLS
cnf) reduce the usefulness of stolen bearers.
Introspection (RFC 7662)
Opaque (or JWT) → POST /introspect with the token → JSON with active, scope, sub, and friends. Cache negative/positive carefully; respect exp. Caching active=true past revoke reintroduces JWT's stolen-token window.
Sandbox: JWT verify + JWKS map (Python)
HS256 stand-in so the sandbox stays self-contained. Production is RS256/ES256 plus a real JWKS document. The important checks — alg allowlist, iss, aud, exp — are the same.
Press Run. Snippets must be self-contained — no network, files, or native modules.
Sandbox: opaque introspection cache (TypeScript)
Do not cache inactive forever. Keep positive TTL well under remaining token lifetime.
Press Run. Snippets must be self-contained — no network, files, or native modules.
Pitfalls
Service A mints a JWT with aud=api://orders. An attacker presents it to api://admin. Draw the verification steps. Where does fail-closed happen if you skip aud? Where does it happen if you check aud and iss?
Interview Q&A
JWT vs opaque — when each?
Answer
JWT for high-QPS independent validation and multi-region edges. Opaque when instant revoke or hiding claims matters. Many platforms mint opaque at the AS and exchange for a short JWT at the gateway.
How do you revoke a JWT?
Answer
You mostly do not. Use a short TTL plus refresh rotation. Optional jti denylist until exp, or a session-version claim. Sender-constraint (DPoP/mTLS) makes stolen bearers less useful.
What is JWKS kid for?
Answer
Key selection during rotation so multiple keys can be live. The header kid picks the JWK. Unknown kid → refresh JWKS, then 401 if still unknown.
Why check aud?
Answer
Prevents a token minted for service A from being accepted by service B — the confused deputy. iss alone is not enough.
Introspection caching hazard?
Answer
Caching active=true past the revoke window. Keep TTL much less than remaining token lifetime, purge on revoke events, and cache negatives even shorter.
iss spoofing?
Answer
Only trust keys from your configured issuer JWKS. Do not take the JWKS URL from unchecked token claims (jku / iss) without an allowlist.
Reference token at a CDN edge?
Answer
You need a low-latency introspection store, or a gateway that mints short JWTs (token exchange) so the origin does not INTROSPECT on every byte.
Why prefer RS256 over HS256 for access tokens?
Answer
With HS256 every verifier holds the forging key. With RS256/ES256 resource servers hold only the public JWK. Compromising an API host should not let it mint tokens for its neighbors.
Is a JWT encrypted?
Answer
No. The payload is Base64url JSON. Anyone with the token can read claims. JWS signs. JWE encrypts. Almost no access-token path needs JWE if you keep PII out of the token.
Why allowlist algorithms instead of trusting the header?
Answer
alg=none and algorithm confusion (HMAC using an RSA public key as the secret) exist because libraries honored the attacker's alg. The server allowlists RS256 or ES256, then uses kid only to pick a key of that type.
What is kid injection?
Answer
A kid that names a file, a SQL fragment, or an attacker JWKS URL. Accept keys only from your configured issuer document, and restrict kid to a strict charset.
Go Deeper
- RFC 7519 — JWT
- RFC 7517 — JWK / JWKS
- RFC 7662 — Token Introspection
- RFC 8705 — mTLS / certificate-bound tokens
- Auth0 — JSON Web Tokens
- Okta — Validate access tokens
- oauth.net — JWT access tokens
- RFC 7515 — JWS
- Auth0 — Critical vulnerabilities in JSON Web Token libraries
- Next: Refresh token rotation & reuse detection