System Design Problem

Design an Authentication and Authorization System (OAuth 2.0/SSO)

Commonly Asked By:OktaMicrosoftGoogleAuth0

Interview Setup

Interview Prompt

Design an OAuth 2.0 / SSO authentication system for 1B users that issues tokens, supports single sign-on across apps, and validates 500K tokens per second at the API gateway.

Clarifying Questions (ask before designing)

QuestionWhy it matters
Public clients (mobile/SPA) or confidential server apps only?
  • PKCE mandatory for public clients
  • client_secret unsafe in mobile binaries.
Stateless JWT validation at gateway or server-side session lookup?
  • JWT = 500K validations/sec without auth service
  • sessions = easier revocation.
Token revocation required before expiry?
  • Short access TTL (15 min) + refresh rotation limits blast radius
  • Redis blocklist for emergency revoke.
SSO across multiple third-party apps (OIDC) or internal only?OIDC adds consent screen, redirect URI validation, and per-client scopes.

Scope

In scope

  • OAuth 2.0 flows (authorization code, PKCE)
  • JWT lifecycle
  • Refresh token rotation
  • RBAC vs ABAC
  • Session management
  • Token revocation

Out of scope (state explicitly)

  • Full HR employee directory / SCIM provisioning product
  • Hardware security module manufacturing
  • Building a social network on top

Functional Requirements

Start by asking your interviewer whether you are building an identity provider or integrating one. Confirm OAuth 2.0 flows, enterprise SAML SSO, and whether MFA is in scope for this round.

  • User registration/login: Email+password, social login (Google, GitHub, Apple)
  • OAuth 2.0 provider: Issue access/refresh tokens; support authorization code, PKCE, client credentials flows
  • Single Sign-On (SSO): Login once, access multiple applications (SAML 2.0 and OIDC)
  • Multi-Factor Authentication (MFA): TOTP, SMS OTP, WebAuthn/passkeys
  • Role-Based Access Control (RBAC): Users have roles; roles have permissions
  • API key management: Issue, rotate, revoke API keys for machine-to-machine auth
  • Session management: Active session list, revoke sessions, device tracking
  • Password policies: Minimum strength, breach detection, forced rotation

Non-Functional Requirements

Your interviewer will care most about token validation latency and five-nines availability. Auth down means the entire platform is down, so call out stateless JWT validation under 5 ms early.

  • Low Latency: Token validation < 5 ms (stateless JWT); login < 500 ms
  • High Availability: 99.999%: auth down = entire platform down
  • Security: Bcrypt/Argon2 password hashing; token encryption; rate limiting on login
  • Scale: 1B+ users, 500K+ auth requests/sec
  • Compliance: SOC 2, GDPR (right to delete), PCI DSS for payment-adjacent

Capacity Estimations

Run this math before you size session storage. Auth requests per second and token validation ratio tell you Redis session load; user count drives credential store sharding.

MetricCalculationValue
Total usersGiven1B
Login requests / secDerived from daily volume ÷ 86400 (+ peak factor)50K
Token validation / secDerived from daily volume ÷ 86400 (+ peak factor)500K
Token refresh / secDerived from daily volume ÷ 86400 (+ peak factor)10K
Active sessionsGiven500M

Architecture Diagram

In the room: authorization code with PKCE for SPAs rather than implicit flow, explaining how refresh-token rotation detects compromise via reuse.

Walk your interviewer through the federation hub first. Auth is a federation hub: customer apps use OIDC/OAuth 2.0 with PKCE; enterprise customers connect via SAML 2.0. The identity provider issues short-lived access tokens and refresh tokens, stores sessions in Redis, and propagates logout across relying parties via back-channel or front-channel flows; I draw B2C OIDC and enterprise SAML as separate entry lanes. For broader caching and session storage patterns, see Caching Patterns and Invalidation, and for fraud mitigation at login, refer to Fraud Detection System.

Loading...

Component Deep Dives

SAML 2.0 vs OIDC: When to Use Each

Next we walk through each component on the diagram. Evaluating SAML vs OIDC is the primary architectural fork when designing modern single sign-on systems.

OIDC is the modern default for SaaS and mobile; SAML persists for enterprise IdP integrations; state which you would pick for this use case.

OIDC (OpenID Connect) (Recommended) - Modern default:
  - JSON/JWT tokens, REST-friendly, mobile-native
  - Built on OAuth 2.0 authorization code + PKCE
  - Use for: SaaS apps, mobile, SPAs, microservices
  - Flow: /authorize → code → /token → id_token + access_token

SAML 2.0 - Enterprise legacy:
  - XML assertions, browser POST binding, heavier
  - Use for: enterprise SSO (Okta/ADFS → legacy apps)
  - Flow: SP-initiated → IdP login → SAML assertion POST to /acs

Hybrid architecture:
  - OIDC for customer-facing apps and API access
  - SAML bridge for enterprise customers requiring federation
  - Same user store; map SAML NameID → internal user_id
  - Staff probe: "How do you handle SAML clock skew?" → NotBefore/NotOnOrAfter
    with 5-min tolerance; NTP sync on IdP and SP

JWKS Rotation and Key Compromise

JWKS rotation and key compromise response is a staff-level probe: explain the kid header, overlap window, and token_version bump on incident.

Signing keys (RS256):
  - Publish public keys at /.well-known/jwks.json with kid header
  - Rotate monthly: add new key, sign new tokens with new kid
  - Old keys remain in JWKS for verification-only (30-day overlap)

Gateway caches JWKS for 1 hour. On unknown kid:
  1. Force refresh JWKS (don't wait for TTL)
  2. Retry validation once
  3. If still unknown → 401 (possible key compromise or misconfig)

Key compromise incident:
  1. Revoke compromised kid immediately from JWKS
  2. Bump token_version for all users (invalidates all access tokens)
  3. Force re-login; rotate refresh tokens
  4. Audit log: which tokens signed with compromised kid

OAuth 2.0 Authorization Code Flow with PKCE

PKCE is mandatory for public clients: explain why code_verifier prevents authorization code interception before walking through the six-step flow.

1. Client generates code_verifier + code_challenge = SHA256(code_verifier)
2. Client redirects to auth server: GET /authorize?response_type=code&code_challenge=...
3. User authenticates (login + MFA if enabled)
4. Auth server redirects back with authorization code
5. Client exchanges code for tokens at POST /token
6. Auth server validates code + verifier, returns tokens

Why PKCE? Prevents authorization code interception attack.
Without PKCE: intercepted code can be exchanged for tokens.
With PKCE: code exchange requires code_verifier that only original client has.

JWT Token Structure and Validation

Access token (JWT):
  Header: { "alg": "RS256", "kid": "key-2026-03" }
  Payload: {
    "sub": "user-uuid", "iss": "https://auth.example.com",
    "aud": "api.example.com", "exp": 1710403200,
    "scope": "read write", "roles": ["admin"],
    "tenant_id": "tenant-uuid"
  }

Validation at API Gateway (stateless, < 1ms):
  1. Decode JWT -> get kid, look up public key from JWKS
  2. Verify signature, check exp, iss, aud
  3. Extract roles/permissions

Token revocation:
  a. Short expiry (15 min) + refresh tokens
  b. Token blacklist in Redis
  c. Version counter in JWT + DB

RBAC: Role-Based Access Control

Hierarchy: User -> has -> Roles -> have -> Permissions

Example:
  User "Alice" -> roles: ["editor", "viewer"]
  Role "editor" -> permissions: ["article:create", "article:edit"]
  Role "viewer" -> permissions: ["article:read"]

Permission format: resource:action
  Storage: user_roles + role_permissions in PostgreSQL
  Cache: in JWT + Redis

API Design

Authentication and Token Lifecycle APIs

REST endpoints for user registration, multi-factor login, token refresh rotation, and active session management.

HTTP
POST /api/v1/auth/register
{ "email": "alice@example.com", "password": "SecureP@ss123", "name": "Alice" }
-> 201 { "user_id": "...", "email_verification_sent": true }

POST /api/v1/auth/login
{ "email": "alice@example.com", "password": "SecureP@ss123" }
-> 200 { "access_token": "eyJ...", "refresh_token": "tGz...", "expires_in": 900 }
 OR 200 { "mfa_required": true, "mfa_token": "...", "mfa_methods": ["totp","sms"] }

POST /api/v1/auth/mfa/verify
{ "mfa_token": "...", "code": "123456", "method": "totp" }
-> 200 { "access_token": "eyJ...", "refresh_token": "tGz..." }

POST /api/v1/auth/token/refresh
{ "refresh_token": "tGz..." }
-> 200 { "access_token": "eyJ...(new)", "refresh_token": "tGz...(rotated)" }

POST /api/v1/auth/logout
-> 200 { "logged_out": true }

GET /api/v1/auth/sessions
-> { "sessions": [{ "device": "iPhone", "ip": "...", "current": true }] }

--- Common Errors ---
401 Unauthorized: invalid or expired token, wrong audience
403 Forbidden: insufficient scope or suspended account
429 Too Many Requests: login brute-force (over 5 attempts in 15 min)
503 Service Unavailable: auth DB failover in progress (retry with backoff)

Common Error Responses

400 Bad Request: invalid input, missing required fields, or malformed JSON payload
401 Unauthorized: missing or invalid authentication token or API key
403 Forbidden: authenticated caller lacks required permissions for this resource
404 Not Found: requested resource ID does not exist
409 Conflict: duplicate write or version conflict, retry with a unique idempotency key
422 Unprocessable Entity: syntactically valid request failed semantic business validation
429 Too Many Requests: rate limit quota exceeded, client should honor Retry-After header
500 Internal Error: unexpected server failure, retry safely with an idempotency key
503 Service Unavailable: downstream dependency is unavailable or overloaded, retry with exponential backoff
422 Unprocessable Entity: password does not meet security requirements or MFA verification is required
423 Locked: account temporarily locked after repeated failed login attempts

Data Model

PostgreSQL

SQL
CREATE TABLE users (
    user_id UUID PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255), name VARCHAR(100),
    mfa_enabled BOOLEAN DEFAULT FALSE, mfa_secret VARCHAR(64),
    token_version INT DEFAULT 0,
    status ENUM('active','suspended','deleted'), created_at TIMESTAMPTZ
);

CREATE TABLE refresh_tokens (
    token_id UUID PRIMARY KEY, user_id UUID NOT NULL,
    token_hash VARCHAR(64) NOT NULL,
    expires_at TIMESTAMPTZ NOT NULL, revoked BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE user_roles ( user_id UUID, role_name VARCHAR(50), PRIMARY KEY (user_id, role_name) );
CREATE TABLE role_permissions ( role_name VARCHAR(50), permission VARCHAR(100), PRIMARY KEY (role_name, permission) );

Redis

session:{session_id}         -> Hash { user_id, device, ip, last_active }
revoked_token:{jti}          -> "", TTL = token remaining lifetime
permissions:{user_id}        -> SET of permission strings, TTL 300
login_attempts:{email}       -> INT (max 5 per 15 min), TTL 900

Event Bus Design (Kafka)

Topic: auth-events
  Partitions: 64
  Partition key: user_id
  Retention: 90 days (audit + threat detection)

Producer: Auth Service on login, logout, token refresh, MFA, password reset
  Event: { event_id, user_id, event_type, ip, geo, device_fingerprint, success, timestamp }

Consumer groups:
  1. audit-log: ClickHouse login history, geo analysis, anomaly detection
  2. threat-detection: credential stuffing, impossible travel, brute force
  3. revocation-broadcast: token revoke events to all regional Redis clusters

Sync path: JWT validation at API Gateway < 1ms (stateless JWKS cache)
Async path: audit and threat detection never block login response
DLQ: auth-events-dlq

Fault Tolerance

ConcernSolution
Auth service down
  • JWTs still validated at gateway (stateless)
  • only login/refresh affected
Redis downToken validation falls back to JWT-only (no revocation check)
Brute force
  • Rate limit: 5 login attempts per 15 min per email
  • CAPTCHA after 3 failures
Credential stuffing
  • Check passwords against HaveIBeenPwned API
  • flag compromised accounts
Token theft
  • Short access token TTL (15 min)
  • refresh token rotation
  • device binding
Key compromise
  • Key rotation: new signing key monthly
  • old keys valid for verification only

Additional Considerations

Interview Walkthrough

  • 25-minute cut

    Skip arch50/arch75 depth unless staff.

    • Authorization code + PKCE for public clients (8 min)
    • JWT validated at gateway (500K/sec) (9 min)
    • Login 50K/sec hits auth service (8 min)
  • Start with OAuth 2.0 authorization code flow with PKCE for SPAs instead of implicit flow, and never store tokens in localStorage without rotation.
  • Walk through SSO: central IdP issues JWT access tokens (15 min) + opaque refresh tokens (7 days) with rotation on every refresh.
  • Explain how refresh token rotation detects reuse, where an invalidated refresh token used again triggers revocation of all sessions as a compromise signal.
  • Cover RBAC with permissions cached in Redis (5-min TTL) and invalidated on role change via pub/sub.
  • Mention credential stuffing defenses: rate limits, CAPTCHA after 3 failures, breached-password checks, step-up auth for sensitive actions.
  • Discuss token revocation via Redis blocklist keyed by JWT jti with TTL matching remaining token lifetime.
  • Common pitfall: long-lived JWTs without refresh rotation, because a stolen access token grants hours of unauthorized access with no revocation path.

Engineering Trade-offs

Password Storage: Why Argon2id

Auth systems balance token convenience against revocation, weighing short-lived JWTs with refresh rotation against opaque server-side sessions.

bcrypt: good, but vulnerable to GPU attacks (fixed memory usage).
scrypt: better (memory-hard), but complex to tune.
Argon2id: winner of Password Hashing Competition.
  Memory-hard + CPU-hard. Configurable: time, memory, parallelism.
  Recommended: Argon2id with 64MB memory, 3 iterations, 4 threads.
  Each hash takes ~200ms and 64MB RAM -> GPU attacks infeasible.

Refresh Token Rotation

On every token refresh:
  1. Validate current refresh_token
  2. Issue new access_token + NEW refresh_token
  3. Invalidate old refresh_token

If stolen old refresh_token is used:
  It's already invalidated -> request fails
  Detection: invalidated token reuse -> compromise detected -> revoke ALL tokens

Credential Stuffing Defense

Defense layers:
  1. Rate limiting: max 5 failed attempts per email per 15 min
  2. IP rate limiting: max 50 attempts per IP per hour
  3. CAPTCHA after 3 failed attempts
  4. Device fingerprinting: new device + correct password -> email verification
  5. Breached password check against HaveIBeenPwned API
  6. Anomaly detection: new country, new device, unusual hour

Account Takeover (ATO) Detection

Signals of compromise:
  - Password changed + email changed within 5 minutes
  - Login from new country + immediate sensitive action
  - Session from unrecognized device + bulk data export

Action:
  1. Send notification to all active sessions
  2. Require re-authentication for sensitive actions
  3. If password AND email changed: lock account + send recovery to ORIGINAL email

Step-up authentication:
  Sensitive actions require re-entering password even if session is valid.

💬Review

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...