System Design: Build an Authentication Service

A full whiteboard walkthrough — requirements, data model, flows, scale and failure — for the most common senior design prompt in this area.

"Design authentication for a platform with ten million users, a web app, a mobile app, and third-party integrations." Here is a structure that covers it in 45 minutes without rambling.

1. Clarify (3 minutes, and do not skip it)

  • Which clients? Web, mobile, server-to-server, third parties — each pushes toward a different scheme.
  • First-party only, or do external developers integrate? The second answer means OAuth.
  • Is the web app same-site with the API?
  • What revocation delay is acceptable? This single number decides most of the architecture.
  • MFA? Enterprise SSO? Compliance regime?
  • Read/write volume, and regions.

2. State the architecture in one sentence

"An OIDC-compliant authorization server issuing 10-minute RS256 access tokens and rotating opaque refresh tokens, with services verifying locally against a cached JWKS and a tokenVersion check for fast revocation."

3. Data model

users              id, email, password_hash, token_version, mfa_enabled
identities         user_id, provider, provider_sub        (social / SSO)
memberships        user_id, tenant_id, role               (multi-tenant)
refresh_tokens     hash, user_id, family_id, used_at, family_expires_at
auth_codes         code_hash, client_id, pkce_challenge, expires_at, used_at
clients            id, secret_hash, redirect_uris[], allowed_scopes[]
signing_keys       kid, public_jwk, private_ref, active_from, retired_at
audit_log          event, user_id, actor, ip, request_id, at

4. Walk the flows

Web login, mobile login (code + PKCE in the system browser), third-party integration (OAuth with consent), machine-to-machine (client credentials or workload identity), refresh with rotation and reuse detection, and logout at three levels: this session, this device, everywhere.

5. Scale

Stateless verification keeps the hot path free of network calls. JWKS cached for ten minutes; tokenVersion cached five seconds; refresh tokens in Postgres because writes are rare. Token lifetimes jittered so a deploy does not create a synchronised refresh wave.

6. Failure

Auth server down: existing sessions work from cached keys, new logins fail — and you must say that failing open is never acceptable. Redis down: 503, not 401, so you do not log out the entire user base at once. Key compromise: rotate, which invalidates everything immediately.

7. Volunteer what you would not build

"I would use an existing OIDC provider unless there is a specific reason not to." Knowing when not to build an auth server is itself a senior signal.

Example

Example · bash
# The architecture, on one whiteboard

  Web (abc.com)   Mobile      Partner app     Internal services
       │             │             │                  │
       │ code+PKCE   │ code+PKCE   │ code+PKCE        │ client credentials
       ▼             ▼             ▼                  ▼
  ┌──────────────────────────────────────────────────────────┐
  │  Authorization Server (OIDC)                             │
  │  /authorize /token /introspect /revoke /jwks /userinfo   │
  │  RS256 · access 10m · refresh 30d rotating               │
  └──────────────────────────────────────────────────────────┘
       │ access token (JWT)                    ▲
       ▼                                       │ JWKS, cached 10 min
  ┌──────────────┐   internal token   ┌────────────────────┐
  │ API Gateway  │──────────────────▶ │ Services           │
  │ verify+route │                    │ verify + ownership │
  └──────────────┘                    └────────────────────┘
                                              │
                                       Postgres (RLS by tenant)

# Numbers to say out loud:
#   access token 10 min   → worst-case revocation delay
#   tokenVersion cache 5s → revocation delay if we pay one cached lookup
#   refresh 30 days       → absolute session cap, rotation on every use
#   JWKS cache 10 min     → survives an IdP outage for existing tokens

When to use it

  • A candidate opens by asking for the acceptable revocation delay and uses the answer to justify every later decision.
  • An interviewer probes the multi-region case and the candidate explains why stateless verification avoids a cross-region session lookup.
  • A design round ends strongly because the candidate volunteered that they would buy rather than build unless a specific requirement forced it.

More examples

The endpoints, and what each one must enforce

Listing endpoints is easy and scores nothing. The annotations are what demonstrate you have thought about each one under attack.

Example · javascript
// Sketch the surface, then annotate. The annotations are the interview.

POST   /oauth/authorize     // + PKCE challenge, exact redirect_uri match,
                            //   state echoed back, consent for third parties
POST   /oauth/token         // grant: authorization_code | refresh_token |
                            //        client_credentials | token-exchange
                            //   code single-use; replay revokes issued tokens
POST   /oauth/introspect    // for opaque tokens; client-authenticated
POST   /oauth/revoke        // revoke a refresh family
GET    /.well-known/jwks.json          // two keys live during rotation
GET    /.well-known/openid-configuration
GET    /userinfo            // requires the access token, not the ID token

POST   /auth/register       // throttled; generic errors; breach check
POST   /auth/login          // throttled per IP AND per account; backoff
POST   /auth/mfa/verify     // partial session → full; rotate the session id
POST   /auth/logout         // this session
POST   /auth/logout-all     // every family for the user + tokenVersion++
POST   /auth/forgot         // identical response whether or not the email exists
POST   /auth/reset          // single-use token; revokes ALL sessions on success

GET    /auth/sessions       // "active devices" — needs the per-user index
DELETE /auth/sessions/:id   // revoke one device

// Cross-cutting, mentioned once rather than repeated per endpoint:
//   every mutating endpoint  → rate limited, audit logged, request-id traced
//   every token issued       → jti, iss, aud, exp, jittered lifetime
//   every error              → uniform shape, no existence disclosure

The revocation conversation, in full

Presenting options with numbers and then committing to one is the shape of a senior answer. Listing options without choosing reads as indecision.

Example · bash
INTERVIEWER: "A user is compromised. How fast can you lock them out?"

CANDIDATE:  "Depends what we are willing to pay per request. Four options:"

  1. Do nothing special — wait for expiry
     delay: up to 10 minutes        cost: zero
     when : most products, honestly

  2. tokenVersion on the user, checked against a 5-second cache
     delay: ~5 seconds              cost: one cached lookup per request
     when : the usual sweet spot

  3. jti denylist in Redis, TTL = remaining token life
     delay: immediate               cost: a Redis GET per request
     when : per-token precision needed; list is self-pruning

  4. Opaque tokens + introspection
     delay: immediate               cost: a network hop (cacheable ~5s)
     when : regulated environments that mandate it

"I'd default to 2. It gets us within five seconds for one lookup we can cache
 aggressively, and it is one integer on the user row."

INTERVIEWER: "And the UI says 'sign out everywhere' — is that honest?"

CANDIDATE:  "With option 1, no — access tokens still work for up to ten minutes,
 so either the copy should say 'within a few minutes' or we implement option 2.
 I'd rather change the architecture than the copy, but it should be a decision
 someone made, not an accident."

# Naming the dishonest-UI problem unprompted is a strong signal: it shows you
# think about the promise the product makes, not only the mechanism.

Questions the interviewer has queued up

The last answer is worth rehearsing. Enthusiasm for building an auth server from scratch is usually read as inexperience with operating one.

Example · bash
Q: "Ten million users. Where is the bottleneck?"
A: Not verification — RS256 is ~50µs and needs no network. It is the refresh
   endpoint (a database write per rotation) and login (Argon2 is deliberately
   ~250ms of CPU). Scale login horizontally and watch refresh write volume.
   Jitter token lifetimes so a deploy does not synchronise the whole fleet.

Q: "Multi-region?"
A: Stateless verification is what makes it work — no cross-region session
   lookup. Refresh tokens need a write, so either a regional primary with
   sticky routing, or accept cross-region latency on refresh only. Signing
   keys are replicated read-only; only one region mints.

Q: "A signing key leaks."
A: Rotate. Publish the new key, switch signing, remove the old from JWKS —
   which invalidates every outstanding token immediately. Disruptive and
   correct. This is why two keys must always be publishable.

Q: "How do you test this?"
A: Unit tests for verification with wrong aud, wrong iss, expired, alg=none.
   Integration tests for the full flows. And five assertions in CI: another
   user's record returns 404, a revoked credential returns 401, a foreign
   origin gets no CORS headers, a tampered token returns 401, and 25 wrong
   passwords produce a 429.

Q: "What would you NOT build?"
A: The authorization server itself, unless there is a reason. Auth0, Okta,
   Keycloak or Ory are all correct answers, and the flows and failure modes
   we just discussed are identical either way. I'd build the parts that are
   specific to us — tenancy, roles, ownership — and buy the protocol.

Discussion

  • Be the first to comment on this lesson.