System Design Masterclass · Module 22 / Week 4

Authentication & Authorization

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.

  1. §01 takeaway · 10 min
  2. §02 revocation sim, in full · 20 min · MANDATORY
  3. §03 flow selection, in full · 15 min · MANDATORY
  4. §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 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.

STEP 03

OAuth2/OIDC: which flow, and the device problem

MANDATORY
STEP 04

Authorization: models and placement

MANDATORY
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.