Stateless Validation
Stateless Authentication: How Validation Works
This is the core benefit of JWTs — zero external calls to validate a request:
sequenceDiagram
participant C as Client
participant GW as API Gateway
participant Auth as Auth Server
participant DB as User DB
Note over C,Auth: Login (one-time)
C->>Auth: POST /login {email, password}
Auth->>DB: Verify credentials
DB-->>Auth: User found
Auth-->>C: {access_token: "eyJ...", refresh_token: "abc..."}
Note over C,GW: Every subsequent API call — no auth server involved
C->>GW: GET /api/orders
Authorization: Bearer eyJ...
Note over GW: Validate JWT locally:
1. Decode header → get kid + alg
2. Fetch public key (cached from JWKS)
3. Verify signature
4. Check exp > now
5. Check aud matches this service
6. Check iss is trusted auth server
7. Extract sub → user_id
GW-->>C: 200 OK {orders: [...]}
Note over GW: No call to Auth server
No call to DB
No call to Redis
Validation is pure computationimport time
class JWTValidator:
"""Stateless JWT validation — no external calls needed."""
def __init__(self, jwks_client, expected_issuer: str,
expected_audience: str):
self.jwks = jwks_client # caches public keys from JWKS endpoint
self.issuer = expected_issuer
self.audience = expected_audience
def validate(self, token: str) -> dict:
"""Validate JWT and return claims. Raises on any failure."""
# 1. Decode header WITHOUT verifying (to get kid and alg)
unverified_header = jwt.get_unverified_header(token)
kid = unverified_header.get("kid")
alg = unverified_header.get("alg")
# 2. CRITICAL: reject 'none' algorithm
if alg is None or alg.lower() == "none":
raise SecurityError("Algorithm 'none' is not allowed")
# 3. CRITICAL: only allow expected algorithms
if alg not in ["RS256", "ES256"]:
raise SecurityError(f"Algorithm '{alg}' is not allowed")
# 4. Get the signing key from JWKS
public_key = self.jwks.get_key(kid)
if not public_key:
raise SecurityError(f"Unknown key ID: {kid}")
# 5. Verify signature, expiry, issuer, audience
try:
claims = jwt.decode(
token,
public_key,
algorithms=["RS256", "ES256"], # allowlist, not from header
audience=self.audience,
issuer=self.issuer,
options={
"require": ["exp", "iat", "sub", "iss", "aud"],
},
)
except jwt.ExpiredSignatureError:
raise AuthenticationError("Token has expired")
except jwt.InvalidAudienceError:
raise AuthenticationError("Token audience mismatch")
except jwt.InvalidIssuerError:
raise AuthenticationError("Token issuer not trusted")
except jwt.InvalidSignatureError:
raise SecurityError("Token signature is invalid")
return claimsSession Store vs JWT: Performance at Scale
Shared session store (Redis):
100K requests/second × 1 Redis lookup each = 100K Redis ops/s
Latency: +0.5–2ms per request (network round-trip to Redis)
Failure mode: Redis down → all auth fails
JWT (stateless):
100K requests/second × 0 external calls = 0 Redis ops/s
Latency: +50µs per request (local cryptographic verification)
Failure mode: nothing to fail — pure computation
Performance gain: ~20–40× less latency for auth per request
Operational gain: no session store to scale, replicate, or monitorTest Your Understanding
The pitch is ‘zero external calls to validate a JWT.’ But validation needs the public key from the JWKS endpoint — isn’t that a network call on every request?
No — the JWKS is fetched once and cached. Public keys change only on rotation (rare), so verifiers pull the key set on startup (or the first time they see a new kid) and cache it in memory. Steady-state validation is signature check + claim checks — pure CPU, ~50µs, no network. A request triggers a fetch only the first time an unknown kid appears, after which it’s cached again.
Contrast: session stores and token introspection hit the network on every request — the exact bottleneck JWTs remove.
An admin revokes a user’s ‘admin’ role in the database. For the next few minutes the user keeps hitting admin-only endpoints successfully. Why — and is it a bug?
The JWT is a snapshot. The role was baked into the token when it was issued; the gateway validates that token locally and never consults the database, so it can’t see the change until a new token is issued (on the next refresh). That’s the core trade-off of stateless auth: per-request freshness traded for zero-lookup speed.
Mitigations: short access-token TTL so stale claims self-correct quickly; for security-critical demotions, revoke the refresh token (blocks renewal) and/or add the token’s JTI to a gateway blocklist for instant effect.