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. I open this interview by saying "everything in this design is about not being the next headline breach" because it immediately frames every decision as security-first rather than feature-first. This question forces candidates to reason simultaneously about security primitives, distributed session state, and safe credential storage.
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. All session tokens are cryptographically signed and verifiable server-side.
- 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=3takes approximately 100ms to compute, which fits within our 200ms login budget but leaves almost no headroom for DB latency and network overhead. Tuning parameters that are simultaneously attack-resistant and operationally viable is the core challenge here. I'd draw the 200ms budget breakdown on the whiteboard: 100ms hash + 20ms DB + 30ms network + 50ms headroom. That visual makes the constraint concrete and shows the interviewer you understand operational math, not just security theory.
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.
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
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.
Naive approach: Store the password directly.
Plaintext storage fails immediately. Any database breach exposes every password with zero effort. Because users reuse passwords across services, a single breach cascades into account takeovers at dozens of other services without any additional attack work. I'd pause here in the interview and explicitly say "this is the naive version, and I'm drawing it only to show what breaks" so the interviewer knows you're building toward a better design, not proposing this.
The fix is to hash with Argon2id. Never store the password. Store only the output of a deliberately slow one-way function. Argon2id handles passwords of arbitrary length (unlike bcrypt, which silently truncates input at 72 bytes), making it the correct choice for new systems.
bcrypt truncates input at 72 bytes. A password of 73 bytes produces the exact same hash as the first 72 bytes, silently. Argon2id has no such limit. Use Argon2id for all new systems; upgrade existing bcrypt hashes on next login using the migration path described in the deep dives.
This diagram covers the registration write path. Login and session issuance come next.
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.