SYSTEM_DESIGN
System Design: OAuth 2.0 Authorization Server
Design a production OAuth 2.0 authorization server supporting Authorization Code Flow with PKCE, Client Credentials, and refresh token rotation. Covers token issuance, revocation, introspection, and security hardening against common attacks including CSRF and token theft.
Requirements
Functional Requirements:
- Implement OAuth 2.0 flows: Authorization Code + PKCE, Client Credentials, and Refresh Token
- Issue opaque access tokens and signed JWT tokens depending on client configuration
- Support OpenID Connect (OIDC): issue ID tokens, expose userinfo endpoint, publish JWKS endpoint
- Enforce token lifetimes: access tokens 15 minutes, refresh tokens 30 days with rotation
- Support token revocation: clients and users can invalidate tokens immediately
- Manage client registrations: confidential and public clients with scope and redirect URI whitelisting
Non-Functional Requirements:
- Token issuance latency under 100ms at the 99th percentile
- Token introspection latency under 10ms (in the critical path of every API request)
- Support 50,000 token issuance requests per second at peak
- 99.999% availability for token introspection endpoint
- All token secrets stored with AES-256 encryption at rest; TLS 1.3 required for all endpoints
Scale Estimation
50,000 token issuances/second * 3,600 seconds = 180 million tokens/hour. Active refresh tokens (30-day TTL) = 50,000 * 86,400 * 30 = 130 billion records — too large for Redis alone; use PostgreSQL for durable storage with Redis as a hot cache. Token introspection: if every API call introspects its token, and the platform serves 500,000 API requests/second, the introspection service must handle 500,000 RPS — served from Redis with sub-millisecond lookup.*
High-Level Architecture
The authorization server has four main endpoints: /authorize (user-facing consent flow), /token (token issuance for all grant types), /introspect (token validation for resource servers), and /revoke (token invalidation). These run as stateless application servers behind a load balancer. All persistent state lives in PostgreSQL (durable) with Redis as the caching and session layer.
The Authorization Code flow: (1) Client redirects user to /authorize with response_type=code, code_challenge (PKCE), state, and scope. (2) Auth server authenticates the user (delegates to identity provider), displays consent screen. (3) Auth server generates a one-time authorization code (32 random bytes, SHA-256 hashed before storage), stores it in Redis with 60-second TTL, and redirects to the client's redirect_uri. (4) Client POSTs the code + code_verifier to /token. (5) Auth server verifies PKCE (SHA-256(code_verifier) == stored code_challenge), issues access token + refresh token.
JWT access tokens are signed with RS256 (RSA 2048-bit private key). The private key is stored in AWS KMS or HashiCorp Vault; signing operations use the KMS API (no private key material in application memory). The public key is published at /.well-known/jwks.json for resource servers to cache and verify tokens locally without introspecting the auth server — reducing introspection load by 95% for stateless JWT verification.
Core Components
Token Issuance Service
Access tokens are JWTs containing: sub (user_id), aud (resource server), scope, exp (15-minute TTL), iat, jti (unique token ID for revocation). The JWT is signed with RS256 using a key fetched from KMS; signing adds ~5ms to issuance latency. Refresh tokens are 32-byte cryptographically random values (CSPRNG), stored as SHA-256(token) in PostgreSQL (never the raw token) to prevent database compromise exposing active tokens. Refresh token rotation: each use of a refresh token issues a new refresh token and immediately invalidates the old one; detection of a previously used (reused) refresh token triggers revocation of the entire token family (all refresh tokens for that user/client pair).
Token Introspection & Caching
For JWT access tokens, resource servers cache the JWKS public keys and verify tokens locally (no network call, <1ms). For opaque tokens or revocation checking, the introspection endpoint looks up the jti claim in Redis: key revoked:{jti} exists → token revoked. Redis lookup: 0.5ms. For refresh tokens, the introspection endpoint queries PostgreSQL by token hash. A read replica handles introspection queries without blocking the primary for writes.
PKCE & CSRF Protection
PKCE (RFC 7636) is mandatory for all public clients (SPAs, mobile apps) and recommended for confidential clients. The code_challenge (SHA-256 of code_verifier) is stored with the authorization code. On token exchange, the server verifies SHA-256(submitted code_verifier) == stored code_challenge, ensuring the token exchange request originates from the same client that initiated the authorization flow. State parameter validation (opaque random value stored in client session, verified on redirect) prevents CSRF attacks on the authorization endpoint.
Database Design
PostgreSQL schema: clients (client_id, client_secret_hash, redirect_uris TEXT[], allowed_scopes TEXT[], token_endpoint_auth_method, require_pkce, created_at), authorization_codes (code_hash VARCHAR(64), client_id, user_id, scope, code_challenge, expires_at, used_at), refresh_tokens (token_hash VARCHAR(64), client_id, user_id, scope, family_id UUID, issued_at, expires_at, revoked_at), token_revocations (jti VARCHAR, revoked_at, reason). Redis stores: authorization codes (short TTL, 60s), revocation list (jti → timestamp, TTL = token expiry), and rate limiting counters.
API Design
GET /authorize — Initiates Authorization Code flow; validates client_id, redirect_uri, scope, state, and PKCE parameters.
POST /token — Issues tokens for Authorization Code, Client Credentials, and Refresh Token grant types.
POST /introspect — RFC 7662 token introspection; returns active/inactive status and token claims for resource servers.
POST /revoke — RFC 7009 token revocation; accepts access or refresh tokens and immediately invalidates them.
Scaling & Bottlenecks
JWT signing via KMS adds 5ms per token issuance due to network round-trip to the KMS service. A local key cache (HSM or in-process key rotation) reduces this to <0.1ms by keeping the current signing key in memory, refreshed every rotation period (90 days). Key rotation uses a two-key overlap window: the old key signs until expiry + clock_skew, while the new key signs new tokens immediately; resource servers cache both JWKS entries during the transition.
Refresh token storage at 130 billion rows is challenging for PostgreSQL. Partitioning by expires_at (monthly partitions) enables fast DROP of expired partition rather than slow DELETE. An archive job moves expired tokens to S3 for audit retention before dropping the partition. Active-token Redis cache (recently used refresh token hashes, 10-million-entry LRU) serves 95% of refresh token validations without hitting PostgreSQL.
Key Trade-offs
- JWT vs. opaque tokens: JWTs enable stateless token verification at resource servers (no introspection call) but cannot be truly revoked before expiry without a distributed revocation list; opaque tokens require introspection on every request but can be revoked instantly.
- Short vs. long access token TTL: 15-minute TTLs limit the window for stolen token abuse but require more frequent refresh operations; 1-hour TTLs reduce refresh load but extend the exploitation window for stolen tokens.
- RS256 vs. HS256 signing: RS256 (asymmetric) allows any resource server to verify tokens using the public JWKS without sharing a secret; HS256 (symmetric) is faster but requires sharing the signing secret with every resource server.
- Centralized vs. federated authorization: A single auth server simplifies client management and policy enforcement; federated (per-service) auth servers provide isolation but complicate cross-service token acceptance and key management.
GO DEEPER
Master this topic in our 12-week cohort
Our Advanced System Design cohort covers this and 11 other deep-dive topics with live sessions, assignments, and expert feedback.