Sessions vs JWTs as a placement-of-state decision, the revocation window you buy with statelessness, OAuth2/OIDC without the alphabet soup, and authorization models that survive 200 services and five device platforms.
~3h study~1.5h exercise1 interactive simFast track ~75 min
Fast track — JWTs in prod already?
Compresses to ~75 min. Token anatomy collapses; the revocation-window sim, the OAuth flow selection, and authorization placement stay mandatory.
§01 takeaway · 10 min
§02 revocation sim, in full · 20 min · MANDATORY
§03 flow selection, in full · 15 min · MANDATORY
§04 authz placement, in full · 20 min · MANDATORY
STEP 01
Sessions vs JWTs: where does the state live?
SKIM
Fast-track takeawayServer sessions: opaque ID, state in Redis — instant revocation, tiny cookie, but every request pays a lookup (M02: +0.3–1 ms × every hop that needs identity) and the store is shared serving-path infra. JWTs: signed self-contained claims — any service validates locally with the public key (JWKS), zero lookups, perfect for 200-service fan-out… and irrevocable until expiry (§02, the whole trade). Anatomy that matters: alg (RS256/ES256 — asymmetric, per M20; reject none and don't accept HS256 where RS256 is expected — the classic confusion attack), exp/iat/nbf validated with clock skew tolerance, aud/iss actually checked (a token for service A replayed at service B must fail), and claims kept small — the token rides every request (M06: header bloat is latency). Standard architecture: short-lived JWT access token + long-lived refresh token, next section.
STEP 02
The revocation window
MANDATORY
Fast trackThe slider below is the module's central trade. A stateless token is a bearer instrument: whoever holds it, is you, until it expires.
Access-token lifetime trade-off
Worst-case revocation lag
15 min
Banned account / stolen token / password change stays live this long — on every service, with zero further checks.
Auth-server refresh load
—
Refresh requests/sec at 5M concurrent viewers — each refresh is also your re-check point for bans and entitlement changes.
The standard machine: access token (5–15 min, stateless, validated locally everywhere) + refresh token (days–months, stateful, validated only at the auth server). Revocation lives at the refresh point: ban the user → their next refresh fails → worst case = access TTL. You've concentrated statefulness at one low-frequency endpoint instead of every request.
Refresh-token hygiene: rotation on every use (old one invalidated; reuse of a rotated token = theft signal → kill the whole session family), bound to device where possible, stored server-side per session — which also gives you the "sign out all devices" and concurrent-session views an OTT product needs anyway.
When 15 minutes is too long (compromised-account playback abuse, instant compliance takedowns): a small denylist of revoked jti/subjects checked at the gateway only — one Redis lookup at one layer buys near-instant revocation while services stay stateless. It's a cache-invalidation problem (M01) and should be sized honestly: entries live only as long as the max access TTL.
Key rotation: JWKS with overlapping keys, kid in every token header, consumers caching JWKS with a refresh-on-unknown-kid path (M20's key-id rule, applied). Rotating without overlap logs everyone out; not rotating is a standing risk.
The JWKS endpoint: where "validate locally" actually comes from
"Any service validates locally" (§01) hides one network dependency: the public keys arrive from the issuer's JWKS endpoint — by convention /.well-known/jwks.json, advertised as jwks_uri in the OIDC discovery document (/.well-known/openid-configuration), which is how Spring's issuer-uri autoconfiguration finds it. The document is a keys[] array of public keys — kty, use, alg, and the kid that token headers reference; during rotation it simply contains both generations, which is the overlap mechanism from the bullet above.
Consumer discipline — this is an M01 problem: fetch at startup, cache in-memory with a TTL of hours, refresh on unknown kid — never per request. The two failure modes are both cache sins: per-request fetching turns the endpoint into a serving-path dependency at full traffic; and an unthrottled refresh-on-unknown-kid path is a stampede amplifier (M01 §03) — one bad token with a garbage kid, replayed by a botnet, becomes your services DDoSing your own auth server. Rate-limit the refresh path and negative-cache unknown kids for a minute.
Publisher discipline — order is everything: publish the new key in JWKS first, wait out consumers' cache TTL, only then start signing with it, and keep the old key published until the last token signed with it has expired (max access TTL). Sign-before-publish is the rotation outage: every service rejects the new tokens as unverifiable until caches refresh — a platform-wide 401 storm you scheduled yourself.
Availability blast radius: steady-state, a down JWKS endpoint costs nothing (keys are cached); but it blocks new consumers starting up and any rotation in progress — precisely the two things happening during your incident recovery or a kickoff scale-out (M25's pre-scale adds pods that all need the keys on boot). So: serve JWKS as the static, CDN-cacheable, unauthenticated document it is (M10 — it's public keys; Cache-Control is your friend), monitor it like serving-path infra, and alert on kid-miss rates (M24) — a climbing miss rate is either a rotation mid-flight or an attack probing your validation path.
STEP 03
OAuth2/OIDC: which flow, and the device problem
MANDATORY
The one distinction that untangles the soup: OAuth2 is delegated authorization ("this app may act on the user's behalf"); OIDC adds authentication on top (the ID token: "who the user is"). "Login with X" is OIDC; "app may read your calendar" is OAuth2.
Flow selection is mostly settled: anything with a user and a browser/webview → Authorization Code + PKCE (implicit is dead; PKCE closes the code-interception hole and is now recommended for all clients, confidential included). Service-to-service, no user → client credentials (though inside your own estate, mTLS workload identity from M21 often makes it redundant). Password grant: dead — never build new on it.
The OTT-specific flow — Device Authorization Grant: your smart-TV problem. TV shows a short code + QR; user approves on their phone (full browser, password manager, biometrics); TV polls the token endpoint. Design notes: polling interval respect (slow_down), code TTL ~10 min, and the phone leg is where all your real authN strength (MFA, risk checks) lives — the TV never sees credentials. This flow is why your TV login UX and your security posture aren't in tension.
Token exchange at the edge: external tokens (from the IdP) exchanged at the gateway for internal tokens with your claim schema — keeps IdP migrations from touching 200 services and lets internal claims (plan tier, region, device class) be yours to design.
STEP 04
Authorization: models and placement
MANDATORY
RBAC (roles → permissions) covers your admin/CMS/back-office cleanly. ABAC (policy over attributes: user plan, device class, content rating, region, time) is what entitlement actually is — "premium plan, in SA, on a DRM-L1 device, within the rental window" is an attribute policy, not a role. ReBAC (relationship graphs, Zanzibar-style) earns its complexity for sharing/ownership graphs (profiles, watch parties) — don't adopt it for tier checks.
Placement (the M09 rule, completed): gateway does authentication + coarse gates (valid token? not revoked? route requires auth?); domain services own their authorization decisions — entitlement rules live in the entitlement service. Central policy engines (OPA-style) are fine as libraries/sidecars evaluating locally; a central authorization service on the request path is a latency + availability tax on every call (M02, M13) — cache decisions with explicit TTLs and accept the staleness window you chose (it's the same trade as §02, one level down).
Claims vs lookups: putting plan-tier in the JWT makes it checkable everywhere for free — and stale until token refresh (an upgrade mid-session doesn't take effect!). Rule: claims for slow-changing, non-critical attributes; live lookups (or M01-cached ones with event invalidation) for anything users expect to change instantly. Your SMIL_ERROR_LICENSE_NOT_GRANTED class of incidents is exactly this staleness surface — design it, don't inherit it.
Deny by default, log denials richly (M21's observability point), and test authorization as code — policy changes get PRs, reviews, and regression tests like any other logic, because they are.
Staff expectationBeing able to draw the two-identity request end-to-end: workload identity (mTLS, M21) authenticating the calling service, user identity (JWT) authenticating the human, and at each hop which one authorizes what. Most authz bugs in microservice estates are confusions between the two — a service authorizing an action because the *caller service* is trusted, on a *user claim* nobody re-verified. Naming the two channels explicitly in design docs kills the bug class.
STEP 05
Exercise
MANDATORY
Fast trackStep 1 (~30 min): building the refresh machine with rotation + reuse detection is the pattern you'll reuse forever.
1
Build the token machine. Spring Authorization Server (or hand-rolled): ES256 access tokens (10 min) + rotating refresh tokens in Postgres. Implement reuse detection (rotated token presented again → revoke session family, alert). Prove the revocation window empirically: ban a user, time until their access dies with and without a gateway denylist.
2
Device flow. Implement the Device Authorization Grant end-to-end with a fake TV (CLI that displays the code and polls). Handle slow_down, expiry, and denial. Note how little the "TV" ever learns.
3
Attack your own validation. Against your resource server: alg=none token, HS256-signed-with-the-public-key token, expired token with skewed clock, valid token with wrong aud. All four must fail; if any passes, you've found the config every scanner looks for.
4
Paper. For five real claims in your platform's tokens (plan tier, device class, region, profile id, entitlement snapshot?): classify each claim-vs-lookup per §04's rule, with the staleness each choice imposes and one incident class it creates or prevents.
Self-check
Why does the access+refresh split concentrate revocation, and what's the worst case?
Access tokens are validated statelessly everywhere (no lookup), so they can't be recalled; refresh happens at one stateful endpoint where bans/changes are checked. Worst case = one full access TTL: a stolen access token or banned account lives ≤ TTL minutes. Shrink further only by re-adding state at the gateway (denylist) — a scoped, TTL-bounded exception, not a return to sessions.
User upgrades to premium mid-session; playback still says basic. Explain via §04 and fix twice.
Plan tier rides as a JWT claim, snapshot at issuance — correct until refresh, stale after the upgrade. Fixes: (a) force a token refresh on plan-change events (push/notify the client to re-auth silently); (b) make tier a lookup at the entitlement check, cached with event-driven invalidation (M01) so the upgrade event purges it. Claims for cheap ubiquity, lookups for instant truth — an upgrade the user just paid for demands instant truth.
Why is the device flow's security *better* than typing a password on the TV, not just more convenient?
The TV — the least trustworthy, least updatable, keylogger-friendly-remote device class — never receives credentials at all; authN happens on the phone with its full stack (password manager, biometrics, MFA, risk engine). The TV ends up holding only scoped tokens, revocable per §02. Convenience and security point the same way, which is rare enough to appreciate.
You rotate the signing key and within a minute every service starts returning 401s. Reconstruct the mistake and the correct sequence.
Signed before publishing (or before caches expired): tokens now carry a kid consumers' cached JWKS doesn't contain; even refresh-on-unknown-kid can't save you if the endpoint doesn't yet list the key — every validation fails until it does. Correct order: publish new key → wait ≥ consumer cache TTL → sign with new key → keep old key published ≥ max access TTL → remove. Symmetric corollary: never delete the old key early, or every not-yet-expired token dies mid-flight. Rotation is a two-sided overlap window, and both edges are yours to schedule.
A reviewer proposes a central authorization service every request calls. Argue the alternative.
On-path central authz adds a network hop (M02 latency at fan-out multiplied), a shared availability dependency (M13: its brownout is everyone's brownout), and a scaling hotspot. Alternative: policy distributed to the edge of each service — engine as library/sidecar, policies pulled/pushed as versioned artifacts, decisions cached with explicit TTL. Central *policy management*, local *policy evaluation*: the control/data-plane split (M09's mesh logic) applied to authorization.