Auth System
Design a secure login and session management system for a web application, covering credential storage, session tokens, multi-factor authentication, OAuth flows, and password reset at millions of users.
What is a user authentication system?
Authentication confirms that a user is who they claim to be. The system verifies credentials, issues a session token, enforces expiry, and provides account recovery.
The engineering challenge is correctness: one misconfigured hash function, one missing rate limiter, or one improperly validated token can expose millions of accounts. The design should therefore make security properties, distributed session state, and credential storage explicit before optimizing for convenience.
TL;DR
Store users and password hashes in a transactional database. Hash new passwords with a tuned, memory-hard password-hashing function such as Argon2id; never store plaintext or reversible credentials. Rate-limit login and reset attempts at the edge and in a shared counter store.
Use short-lived access tokens for ordinary request validation and rotate opaque refresh tokens server-side when the application needs low-latency validation plus revocation. Store only hashes of refresh and password-reset tokens. Make reset links one-time and time-limited, keep forgot-password responses neutral, and publish email delivery asynchronously.
Scope and assumptions
The following are illustrative interview assumptions and security targets; hash cost and rate limits must be benchmarked and tuned for the deployment:
- Ten million registered users, one million daily active users, and a peak login rate of approximately 700 requests per second.
- Email/password registration, login, session expiry, logout, account recovery, and the security controls around those flows are in scope.
- The final path uses a short-lived signed access JWT plus a rotated opaque refresh token. Opaque Redis sessions remain a valid alternative when immediate per-request revocation is more important than avoiding a lookup.
- A 200ms p99 login target and an illustrative Argon2id configuration are used for capacity discussion, not as universal safe parameters. Measure memory, time, concurrency, and failure behavior on the actual authentication fleet.
- Authorization/RBAC, organization SSO, social login, WebAuthn/passkeys, and MFA are named extension points rather than fully designed paths in this article.
Functional Requirements
Core Requirements
- Users can register with email and password.
- Users can log in and receive a session token.
- Authenticated sessions expire after a configurable period.
- Users can reset forgotten passwords securely.
Below the Line (out of scope)
- Fine-grained authorization / RBAC (separate from authentication).
- Federated SSO across organizations (SAML).
The hardest part in scope: Credential storage. A bcrypt misconfiguration, a skipped timing-safe comparison, or a missing per-user salt turns a routine database breach into immediate mass credential exposure, with downstream account takeovers at every other service where users reuse the same password.
Social login via OAuth/OIDC is out of scope because it replaces the credential check entirely. To add it: implement Authorization Code flow with PKCE (Proof Key for Code Exchange) to exchange an authorization code for an access token at the provider, then store a provider_id and provider_user_id alongside the user record. Never store the provider access token in the database.
Federated SSO (SAML) is out of scope because it introduces an identity provider protocol that sits above the auth system. To add it: implement a SAML Service Provider that validates signed assertions from the IdP and maps them to local user records.
Non-Functional Requirements
Core Requirements
- Latency: Login completes in under 200ms p99.
- Scale: 10M registered users, 1M DAU. Login rate peaks at approximately 700 requests per second.
- Availability: 99.99% uptime. Authentication is the gateway to every other feature; a login outage halts all downstream services for all users.
- Security: Passwords are never stored in plaintext. Access tokens are cryptographically signed and verifiable server-side; opaque refresh and reset tokens are random values validated through hashed server-side state.
- Brute force protection: Login and password reset endpoints enforce rate limiting. Accounts lock after 5 consecutive failed login attempts within 15 minutes.
Below the Line
- Social login via OAuth/OIDC providers (Google, GitHub)
- Passkeys and WebAuthn credential management
- TOTP or SMS multi-factor authentication
- Session concurrency limits (maximum N active sessions per user)
The hardest engineering problem in scope: Getting password hashing right under real operational constraints. Argon2id at
memory=64MB, time=3is an illustrative configuration that may take approximately 100ms on one class of server, leaving limited headroom inside a 200ms login target. Tune parameters against the real CPU, memory, concurrency, database, and network budget; a sample breakdown is 100ms hash + 20ms DB + 30ms network + 50ms headroom.
Social login is below the line because it replaces credential checking rather than extending it. To add it: implement OAuth 2.0 Authorization Code + PKCE and map provider identities to local user records via a SocialIdentity join table.
Passkeys and WebAuthn are below the line because they require a separate registration ceremony and a different assertion verification path. To add them: store a credential_id and public_key per authenticator in a WebAuthnCredential table and verify the signed client data assertion on each login.
TOTP MFA is below the line but only just. To add it: store an encrypted TOTP secret per user in MFACredential, issue a short-lived challenge token after password verification, require a valid 6-digit code before issuing a full session token, and add a POST /auth/mfa/verify endpoint.
Session concurrency limits are below the line because they require active session enumeration per user. To add them: maintain a SET sessions_by_user:{user_id} in Redis with session IDs as members, and reject new logins when the cardinality exceeds the configured maximum.
30-second answer / outline
- Normalize and validate the email, rate-limit the request, and look up the user through an indexed, transactional store.
- Verify the submitted password with the stored Argon2id/bcrypt parameters using the library's verification routine; never compare raw passwords or fast hashes.
- Issue a short-lived access token and a random refresh token whose hash and token family are stored server-side. Rotate the refresh token on use and revoke the family on replay.
- Validate ordinary requests locally from the access-token signature, while keeping refresh, logout, reset, and rate-limit state in shared storage.
- For recovery, return a neutral response, store only a hash of a time-limited one-time token, and update the password plus token state atomically.
5-minute explanation
Separate the credential lifecycle from request authorization. Registration writes a normalized user record with a password hash. Login is the expensive path because it intentionally spends CPU and memory on password verification; rate limits must run before that work. Successful login produces a short-lived access credential and a refresh credential with a narrower revocation surface.
The request path verifies the access token locally when possible, so business services do not need a database lookup for every call. Refresh and logout still need shared state because rotation and revocation are stateful. A design that instead chooses opaque Redis sessions is simpler to revoke immediately but adds a lookup to every authenticated request.
Recovery is a separate threat model. The forgot-password response is deliberately indistinguishable for known and unknown addresses. Email delivery is asynchronous. The reset token is random, only its hash is stored, and a conditional update consumes it once while replacing the password and invalidating any appropriate sessions according to policy.
The final layer is operations and abuse resistance: rate-limit by IP and account, add progressive friction without making shared mobile IPs unusable, monitor failures and refresh replays, rotate signing keys, and test database/Redis/email failure modes. The deep dives justify these choices and their limits.
45-minute interview approach
This is a time-boxed plan for answering the design question, not a claim that the article should be read in 45 minutes.
- 0β5 minutes β Clarify the contract: Confirm browser/mobile clients, token format, session lifetime, immediate revocation needs, email verification, MFA/SSO scope, and recovery behavior.
- 5β10 minutes β Establish scale and threat model: Use the illustrative users, login rate, latency, availability, and brute-force assumptions. Identify credential stuffing, token theft, enumeration, replay, and email compromise.
- 10β15 minutes β Define APIs and entities: Walk through register, login, refresh, logout,
/auth/me, forgot-password, reset-password, users, sessions/tokens, and reset records. - 15β22 minutes β Draw registration/login: Show rate limiting, user lookup, password verification/hash, transactional writes, access-token issuance, refresh-token hash storage, and failure responses.
- 22β29 minutes β Deep dive on sessions: Compare opaque Redis sessions with access JWT plus refresh rotation. Explain revocation, TTLs, key choice, replay detection, and signing-key rotation.
- 29β35 minutes β Draw recovery: Show neutral enumeration-safe responses, email queue, token hashing, expiry, one-time consumption, password replacement, and session invalidation policy.
- 35β41 minutes β Security, reliability, and operations: Cover Argon2id tuning, CAPTCHA/progressive delays, Redis/DB outage behavior, audit logs, monitoring, backups, and incident response.
- 41β45 minutes β Trade-offs and close: Compare MFA/passkeys/SSO extension points, recap the security invariants, state what is out of scope, and invite follow-up questions.
Core Entities
- User: The registered account. Carries
email,hashed_password(the Argon2id or bcrypt output string, which embeds the salt automatically),created_at, andis_verified. - Session: An active authenticated session. Carries
session_id(a cryptographically random opaque value),user_id,expires_at, and arevokedboolean for explicit invalidation. - PasswordResetToken: A one-time recovery credential. Carries
token_hash(the database never stores the raw token, onlysha256(token)),user_id,expires_at, and ausedflag. - MFACredential (when MFA is in scope): A registered second factor. Carries
user_id,type(TOTP or SMS), andsecret(encrypted at rest with application-level encryption, not just database-level).
Full column types, indexes, and foreign key constraints are deferred to a data model deep dive. The four entities above are sufficient to drive every endpoint and system walkthrough in this article.
API Design
FR 1 - Register a new account:
POST /auth/register
Body: { email, password }
Response: 201 Created Β· { user_id }
Return only user_id on registration. Do not issue a session token until the email is verified; an unverified account should not access protected resources.
FR 2 - Log in and receive tokens:
POST /auth/login
Body: { email, password }
Response: 200 OK Β· { access_token, refresh_token, expires_at }
The access_token is a short-lived signed JWT (15 minutes). The refresh_token is an opaque random value stored server-side. The full token lifecycle tradeoff is covered in the deep dives.
FR 2 (with MFA) - Verify a TOTP code:
POST /auth/mfa/verify
Body: { challenge_token, totp_code }
Response: 200 OK Β· { access_token, refresh_token, expires_at }
challenge_token is a short-lived intermediate token (5 minutes) issued after a correct password but before MFA passes. This prevents a full session from being issued until both factors succeed.
FR 3 - Validate a session:
GET /auth/me
Headers: Authorization: Bearer <access_token>
Response: 200 OK Β· { user_id, email }
GET /auth/me serves as both a profile endpoint and a session health check. Clients call it to confirm a stored token is still valid before making other authenticated requests.
FR 3 - Log out:
POST /auth/logout
Headers: Authorization: Bearer <access_token>
Body: { refresh_token }
Response: 204 No Content
Use POST for logout because it is side-effecting (it invalidates server-side state). Include the refresh token in the body so it is revoked immediately, not just left to expire on its own.
FR 4 - Initiate password reset:
POST /auth/forgot-password
Body: { email }
Response: 202 Accepted Β· { message: "If that address is registered, a reset link was sent." }
Always return 202 with an identical message regardless of whether the email exists. Any response that diverges based on email presence leaks account enumeration information.
FR 4 - Complete password reset:
POST /auth/reset-password
Body: { token, new_password }
Response: 200 OK
token is the raw value from the email link. The server computes sha256(token) before any database lookup. The raw token never persists in the database.
High-Level Design
Critical flows
Trace four flows in order: registration and password storage; login and token issuance; protected-request validation and logout; and password recovery. Rate limiting and auditability cross all four. The token choice determines which parts need shared state on the request path.
1. User registration
Solving: Store a new account so the user can authenticate in the future.
Components:
- Client: Web or mobile app sending
POST /auth/register. - Auth Service: Validates email format, checks for duplicate accounts, hashes the password, and writes to the database.
- Database: Stores user records with a UNIQUE constraint on
email.
Request walkthrough:
- Client sends
POST /auth/registerwith email and password. - Auth Service validates the email format; reject 400 for malformed addresses.
- Auth Service queries the DB for an existing user with that email; return 409 if found.
- Auth Service hashes the password and writes the new user row.
- Auth Service returns 201 with
user_id.
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with SDEpedia Premium.