JWT Internals#
Problem statement (interviewer prompt)
A microservice fleet must authenticate millions of requests/sec without hitting a session store. Design a token format that the issuer can sign, every service can verify locally, and the client can carry across requests. What are the failure modes?
A JWT is three base64url segments separated by dots: header.payload.signature. Each service holds the issuer's public key and verifies the signature locally; no database lookup per request.
flowchart LR
C([Client]) --> Auth[Auth service<br/>issues JWT]
Auth -->|signed JWT| C
C --> Svc1[Service A]
C --> Svc2[Service B]
Svc1 -. verify with issuer's public key .-> JWKS[(JWKS endpoint)]
Svc2 -. verify .-> JWKS
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class C client;
class Auth,Svc1,Svc2 service;
class JWKS datastore;
Sounds wonderful; comes with pitfalls. Most JWT bugs are about how revocation, expiry, and signing-key rotation work.
JWT (RFC 7519) is the most common stateless authentication token. It's simple by design and routinely misused; understanding the moving parts is what separates safe deployments from disasters.
Anatomy#
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMyJ9
.
eyJzdWIiOiJ1c2VyXzQyIiwidGVuYW50IjoiYWNtZSIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxNzAwMDAzNjAwfQ
.
<signature>
Three base64url-encoded JSON segments:
| Segment | Example |
|---|---|
| Header | {"alg":"RS256","typ":"JWT","kid":"123"} |
| Payload | {"sub":"user_42","tenant":"acme","iat":1700000000,"exp":1700003600} |
| Signature | RSA-SHA256(secret_or_private_key, header + "." + payload) |
Signing algorithms#
alg |
Type | Use |
|---|---|---|
| HS256 | Symmetric (HMAC) | Single trusted party; secret shared between issuer and verifier |
| RS256 | RSA signature | Federation; verifiers hold public key only |
| ES256 | ECDSA signature | Same as RS256 but smaller keys |
| EdDSA | Ed25519 signature | Modern; recommended for new systems |
none |
UNSIGNED | NEVER accept; classic attack |
JWKS - rotating keys#
sequenceDiagram
participant C as Client
participant Auth as Auth service
participant Svc as API service
participant JWKS as /.well-known/jwks.json
Auth->>JWKS: publish new public key (kid=v2)
C->>Auth: login -> JWT signed with v2
C->>Svc: request + JWT(kid=v2)
Svc->>JWKS: GET keys (cached)
Svc->>Svc: verify with kid=v2
Note over Auth: rotate, old kid=v1 still valid for old tokens
Best practice: issuer exposes a JWKS endpoint with current and prior public keys. Verifiers fetch and cache them, indexed by kid. Rotate keys regularly; never blast all old tokens by removing keys before they expire.
Common attacks#
1. alg: none#
Old JWT libraries accepted alg: none (no signature). Attacker forges any payload. Fix: enforce alg allowlist server-side.
2. Algorithm confusion#
If your server holds an RS256 public key and the library accepts both RS256 and HS256 with the same secret, an attacker can sign HS256 using the public key as the HMAC secret. Fix: bind alg per key.
3. kid injection#
kid field used to look up a key in the filesystem or DB without validation: attacker uses ../../etc/passwd or a SQL-injected kid. Fix: validate kid against an allowlist.
4. Stolen token replay#
JWTs are bearer tokens. If stolen (XSS, log leak, network sniffing), the attacker uses them until expiry. Fixes:
- Short expiry (5-15 min).
- Refresh tokens stored differently and rotatable.
- Bind to a fingerprint (TLS exporter, DPoP).
Revocation - the hard part#
JWTs are stateless; there's no per-token "logout" without state. Options:
| Strategy | Cost |
|---|---|
| Wait for expiry | Simplest; needs short exp |
| Blocklist in Redis | Stateful again; verify per request |
| Token versioning | Embed user's "token generation"; bump on logout |
| Refresh-token rotation | Short JWT + long refresh; revoke refresh server-side |
Most production systems use refresh-token rotation with short access JWTs.
Refresh-token flow#
sequenceDiagram
participant C as Client
participant A as Auth
participant R as Refresh store
C->>A: login
A->>C: access JWT (15m) + refresh token (7d)
Note over C: store refresh in httpOnly cookie
C->>A: refresh (when access about to expire)
A->>R: validate, rotate, revoke old
A->>C: new access + new refresh
Refresh tokens are opaque random strings stored server-side; they support revocation directly.
Standard claims (RFC 7519)#
| Claim | Meaning |
|---|---|
iss |
Issuer |
sub |
Subject (user id) |
aud |
Audience (intended recipient) |
exp |
Expiration time (unix seconds) |
nbf |
Not-before time |
iat |
Issued-at |
jti |
Unique token id |
Always verify exp, iss, aud, and clock skew (leeway: 30s).
Token size#
A JWT is large compared to opaque tokens:
- Typical: 700 bytes - 2 KB.
- Every request carries it - bandwidth tax on chatty APIs.
- Cookies have a 4 KB cookie + 8 KB header limit; chunky JWTs break things.
When NOT to use JWT#
- Highly sensitive sessions where instant revocation matters.
- Endpoints behind a single trust boundary (just use sessions).
- High-frequency RPC where overhead matters.
Where JWTs are used#
flowchart TB
JWT((JWT))
CRY[Crypto primitives<br/>signing math]
SEC[Security fundamentals<br/>AuthN/AuthZ context]
OAUTH[OAuth / SSO<br/>issuance layer]
GW[API Gateway<br/>typical validator]
CRY --> JWT
SEC --> JWT
OAUTH --> JWT
JWT --> GW
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class JWT service;
class CRY,SEC,OAUTH,GW datastore;
Glossary & fundamentals#
| Tag | Concept | What it is | Page |
|---|---|---|---|
HLD |
Cryptography primitives | the signing layer | cryptography-primitives |
HLD |
Security fundamentals | parent topic | security-fundamentals |
HLD |
OAuth SSO | issues JWTs | oauth-sso |
HLD |
API gateway | typical JWT validator | api-gateway |
Quick reference#
Structure#
base64url(header).base64url(payload).base64url(signature)
Algorithms#
alg |
Notes |
|---|---|
| HS256 | Single trusted party (shared secret) |
| RS256 | Federation; verifiers hold public key |
| ES256 | RS256 alternative, smaller keys |
| EdDSA | Modern; recommended |
none |
NEVER accept |
Standard claims#
iss, sub, aud, exp, nbf, iat, jti.
JWKS#
/.well-known/jwks.jsonpublishes current + prior keys- Verifier caches, indexes by
kid - Rotate regularly; keep old keys until tokens expire
Attacks#
alg: none- enforce allowlist- Algorithm confusion - bind alg per key
kidinjection - validate against allowlist- Token replay - short exp, refresh rotation, fingerprint binding
Revocation strategies#
| Strategy | Cost |
|---|---|
| Wait for exp | Simplest |
| Blocklist in Redis | Stateful again |
| Token version per user | Bump on logout |
| Refresh rotation | Common production pattern |
Refresh flow#
- Access JWT: 5-15 min
- Refresh token: opaque, 7-30 day, server-side, rotatable
- httpOnly secure cookie
Verify checklist#
algin allowlist- Signature
expwith clock skew leewayiss,audnbfif present
Size#
700 bytes - 2 KB typical; tax on every request.
Refs#
- RFC 7519
- RFC 8725 (JWT BCP)
- OWASP JWT Cheat Sheet
- Auth0 JWT handbook
FAQ#
What is a JWT?#
A JSON Web Token is three base64url segments separated by dots: header, payload, and signature. The issuer signs it so any service can verify without a database lookup.
JWT vs session cookie, when use which?#
Use JWT for stateless microservice auth where every service must verify cheaply. Use sessions when you need instant revocation and tighter control over each login.
How do you revoke a JWT?#
Keep tokens short-lived and pair them with a refresh token. For instant revocation, check a denylist or a versioned subject claim on sensitive operations.
What is JWKS?#
A JWKS endpoint publishes the issuer's signing public keys. Services cache the keys, verify the kid header on each token, and rotate without downtime.
Are JWT payloads encrypted?#
Signed JWTs are not encrypted, only tamper-evident, so the payload is readable. Use JWE if you actually need to keep claims secret from the holder.
Related Topics#
- Cryptography Primitives: the signing math behind JWT
- OAuth SSO: the issuance layer for JWTs in federated systems
- API Gateway: the typical JWT validation point
Further reading#
- RFC - RFC 7519 - JSON Web Token
- Doc - OWASP JWT Cheat Sheet
- Blog - Auth0 - JWT handbook
- Doc - RFC 8725 - JWT Best Current Practices