Design an authentication and SSO service
The interviewer asks
“Design the central authentication and single sign-on service for a company with several products (web, Android, iOS) and 100 million users. Users log in once and stay logged in across products; enterprise customers want to sign in with their own identity provider; and 300 internal services need to verify who is calling them.”
What a strong answer looks like
A strong answer builds an OpenID Connect provider (or wraps Keycloak/Auth0/Cognito) and is precise about the protocol: Authorization Code + PKCE for web and mobile, Client Credentials (or workload identity, mTLS) for service-to-service, federation with SAML/OIDC for enterprise SSO. It issues short-lived signed access tokens (JWT, 5-15 min) that APIs verify locally against a cached JWKS, plus long-lived opaque refresh tokens stored server-side with rotation and reuse detection, which gives revocation. SSO across products comes from a central session cookie on the auth domain. The deep dives: token design and revocation (stateless JWT vs session lookups), refresh token rotation, key rotation, login security (rate limits, OTP abuse, credential stuffing, MFA), and availability, since auth down means everything down. India-specific: phone + OTP is the main login, so OTP cost and SMS pumping fraud matter.
Concepts to mention
1 Requirements
Functional
- Sign up and log in with phone + OTP, email + password, and Google/Apple social login
- Single sign-on across all company web and mobile products
- Enterprise SSO: customers connect their Okta/Azure AD via SAML 2.0 or OIDC; optional SCIM user provisioning
- Multi-factor authentication (TOTP, passkeys/WebAuthn) and step-up auth for risky actions
- Issue access tokens with scopes and claims; refresh tokens for long sessions
- Logout everywhere, revoke sessions per device, admin force-logout for a compromised account
- Service-to-service authentication for internal APIs
- Login history and security alerts (new device, new location)
Non-functional
- Availability 99.99% for token verification and refresh; login 99.95%
- Token verification in APIs adds under 1 ms (local signature check, no network call)
- Login p95 under 500 ms excluding OTP delivery; OTP delivered within 10 s for 95% of users
- Revocation effective within 5-15 minutes for normal logout, within seconds for security incidents
- Scale: 100M users, ~5M logins/day, ~20k token refreshes/s at peak
- Passwords hashed with a slow memory-hard function; no secrets or tokens in logs
Out of scope (say it out loud)
- Fine-grained authorization policies inside each product (only coarse scopes and roles here)
- KYC and identity verification
- Customer-facing account management UI design
2 Estimates
3 API
| Endpoint | What it does |
|---|---|
GET/oauth2/authorize?response_type=code&client_id=&redirect_uri=&scope=openid%20profile&state=&code_challenge=&code_challenge_method=S256 | Starts the Authorization Code + PKCE flow; shows login if there is no SSO session cookie, else redirects back with a code. |
POST/oauth2/token | Exchange code + code_verifier for tokens; refresh_token grant with rotation; client_credentials for services. |
GET/.well-known/openid-configuration and /oauth2/jwks | Discovery document and public signing keys; cached by every service. |
POST/v1/otp/send and /v1/otp/verify | Phone login: send OTP (rate limited per phone, IP, device) and verify (max 5 attempts). |
POST/oauth2/revoke and /v1/sessions/{id}:revoke | Logout one device or all devices; revokes refresh tokens and publishes a revocation event. |
POST/saml/acs/{connection_id} | SAML assertion consumer for enterprise IdPs; validates signature, audience, time window. |
GET/oauth2/userinfo | OIDC userinfo for clients that need profile claims. |
4 Data model
usersPostgres (sharded by user_id, or Aurora/Spanner-class)user_id (PK)phone_e164 (unique)email (unique)password_hash (argon2id)mfa_methodsstatuscreated_atUnique indexes on phone and email; lookups by identifier on login. Data stays in Indian regions where required.
identitiesPostgresuser_idprovider (google|apple|saml:conn_id)subjectlinked_at; unique(provider, subject)Links external identities to one user; account linking rules prevent takeover via unverified emails.
sessions / refresh_tokensPostgres + Redis cachesession_iduser_iddevice_infotoken_family_idcurrent_token_hashparent_hashexpires_atrevoked_atlast_used_ipStore only a SHA-256 hash of the refresh token. Family id enables reuse detection: an old token used again revokes the whole family.
signing_keysKMS/HSM + Postgres metadatakidalg (ES256)public_keystatus (next|active|retiring)created_atPrivate keys never leave KMS/HSM or are held encrypted; JWKS publishes active + next + retiring keys.
otp_challengesRediskey otp:{phone}value {code_hashattemptsexpires_at}TTL 5 minHash the code; cap verify attempts; separate counters for send rate per phone, IP and device.
sso_connectionsPostgrestenant_idprotocol (saml|oidc)idp_metadatacertsemail_domainsenforce_ssoscim_token_hashEmail-domain routing: user types name@bigcorp.com and is sent to BigCorp's IdP.
revocation listRedis + pub/sub to servicesrevoked jti or user_id → revoked_after timestampTTL = max access token lifetimeOnly for urgent revocations; small because entries expire with the access token TTL.
5 Architecture
Hover a box to highlight its connections.
6 Request flows
Mobile login with phone OTP and PKCE
- 1The app generates a random code_verifier and its SHA-256 code_challenge, then opens /oauth2/authorize in a system browser tab (not an embedded WebView).
- 2The user enters a phone number. The auth service checks send limits (e.g. 3 per phone per 10 min, 20 per IP per hour, device fingerprint) and sends a 6-digit OTP via the SMS provider, with WhatsApp as a fallback.
- 3The OTP hash is stored in Redis with a 5-minute TTL and an attempts counter. The user enters the code; after 5 wrong attempts the challenge is burned.
- 4On success, the service creates an SSO session (HttpOnly, Secure, SameSite cookie on auth.example.com) and redirects to the app's redirect URI with a one-time authorization code bound to the code_challenge.
- 5The app POSTs the code and code_verifier to /oauth2/token. The server checks SHA-256(verifier) = challenge, so an intercepted code is useless.
- 6It returns an access token (ES256 JWT, 15 min, claims: sub, aud, scope, exp, jti, sid) and an opaque refresh token (stored hashed, with a token family id). A login event goes to Kafka for risk checks and the 'new device' alert.
API call and token refresh
- 1The app calls GET /orders with Authorization: Bearer <JWT>. The orders service verifies the signature with the cached JWKS key matching kid, then checks iss, aud, exp and scope. No network call.
- 2After 15 minutes the token expires; the app calls /oauth2/token with grant_type=refresh_token.
- 3The auth service hashes the presented token, finds it is the current token of family F, issues a new refresh token, marks the old one as used, and returns a new access token.
- 4If an already-used refresh token is presented again (stolen and replayed), the service revokes the entire family F and forces re-login on that device.
Enterprise SSO via SAML
- 1A user types priya@bigcorp.com; the email domain maps to BigCorp's SAML connection with enforce_sso = true.
- 2The federation service redirects to BigCorp's IdP with a signed AuthnRequest; the user logs in with corporate MFA.
- 3The IdP posts a signed assertion to /saml/acs; the service validates signature, audience, recipient, NotOnOrAfter and the InResponseTo id (replay protection).
- 4The user is mapped via identities(provider=saml:bigcorp, subject) and a normal session and tokens are issued; SCIM deprovisioning later revokes all their sessions.
7 Deep dives
Access tokens: JWT vs opaque, and how to revoke
A JWT access token is signed by the auth service; any API can verify it locally with the public key from the JWKS endpoint, so 2M verifications per second cost the auth service nothing. The weakness is revocation: a JWT is valid until exp, even after logout. An opaque token (random string) requires a lookup (introspection) on every call, which is easy to revoke but puts the auth store on every request path. The standard answer is a hybrid. Access tokens are short-lived JWTs (5-15 minutes) and contain only stable claims (sub, aud, scope, tenant, sid), not data that changes often. Refresh tokens are opaque, long-lived (30-90 days with sliding expiry), stored hashed in the database, and checked on every refresh, so logout revokes the refresh token and the access token dies within 15 minutes. For urgent cases (account takeover, employee offboarding) publish a small revocation entry (sid or user_id revoked_after T) via Redis pub/sub to services, which check it in memory; entries expire after 15 minutes because the tokens would have expired anyway. Always validate alg against an allow-list (never accept 'none' or switch RS256 to HS256), and check iss and aud so a token minted for one product cannot be replayed against another.
Refresh token rotation and reuse detection
Mobile apps keep users logged in for months, so the refresh token is the crown jewel. Store it in the Android Keystore / iOS Keychain, never in plain shared preferences; for browsers, keep tokens out of JavaScript where possible (a backend-for-frontend holds them and the browser gets an HttpOnly session cookie). On the server, rotate on every use: each refresh returns a new refresh token and marks the old one used, all linked by a token family id. If a used token comes back, either an attacker or the real client is replaying a copy, so the server revokes the whole family and the user logs in again. Handle the honest race: an app with two parallel requests may refresh twice within a second, or a network failure may drop the response. Allow a short grace window (e.g. 10-30 s) where the previous token returns the same new token, so legitimate retries do not log users out. Store only SHA-256 hashes of tokens so a database leak does not give out sessions, and record device, IP and last-used time so users can see and revoke sessions.
Signing keys and rotation
Tokens are signed with an asymmetric key (ES256 is smaller and faster than RS256; EdDSA is also used). The private key lives in KMS/HSM or is encrypted at rest, and only the auth service can use it. Each key has a kid that is placed in the JWT header. Rotation without downtime works in three phases: publish the new public key in JWKS as 'next' (services refresh JWKS every few minutes and cache it), wait longer than the JWKS cache time, then start signing with the new key, and keep the old public key in JWKS until all tokens it signed have expired (15 minutes plus margin). Services should refetch JWKS when they see an unknown kid (with a rate limit to stop attackers forcing refetches). If a key is compromised, remove it from JWKS immediately and accept that every session using it must refresh. Calling KMS for every signature adds latency and cost at 20k tokens/s, so a common pattern is a short-lived signing key generated in memory and certified by the KMS root, or KMS with local caching of data keys.
Login abuse: OTP pumping and credential stuffing
In India most consumer logins use phone + OTP, so the OTP endpoint is both the front door and a money leak. Attackers trigger millions of OTP sends to premium-rate or random numbers (SMS pumping), costing lakhs per day, or try to brute-force 6-digit codes. Defences: rate limits per phone, per IP, per /24 subnet, per device id and globally per country prefix; CAPTCHA or Play Integrity / App Attest checks before sending; exponential cool-down for resends; at most 5 verify attempts per challenge; and alerts when send volume deviates from normal by hour. Codes are random 6 digits, stored hashed, valid 5 minutes, single use. For passwords, credential stuffing (lists of leaked passwords) is the main threat: hash with Argon2id or bcrypt, check new passwords against breached-password lists, rate-limit per account and per IP with progressive delays, use risk scoring (new device, impossible travel) to require MFA, and return the same error message for 'unknown user' and 'wrong password'. Passkeys (WebAuthn) remove phishing risk entirely and are worth offering.
8 Trade-offs
Build your own OIDC provider vs Keycloak / Auth0 / Cognito
Security protocols are easy to get subtly wrong (redirect URI matching, PKCE, SAML signature wrapping attacks). Managed providers or Keycloak give certified flows quickly. Building your own makes sense at very large scale, for full control of user experience (OTP flows, regional languages), cost, and data localisation. Many companies run Keycloak or a managed provider for enterprise SSO and their own service for high-volume consumer OTP login.
Short vs long access token lifetime
Short (5 min) gives faster revocation but more refresh traffic and more sensitivity to auth outages. Long (1 hour) cuts load but leaves stolen tokens valid longer. 10-15 minutes plus a fast revocation channel for emergencies is a common balance.
Central session cookie SSO vs token sharing
SSO across products works by every product redirecting to the same auth domain, which already has a session cookie, so the user sees no login form. Sharing tokens between apps directly or using third-party cookies breaks with browser privacy changes. On mobile, a shared system browser session or an account manager achieves the same.
9 Bottlenecks
Refresh storm after an outage
If the auth service is down for 20 minutes, almost every access token expires, and on recovery 20M clients refresh at once. Clients should use jittered retries; the service should prioritise refreshes over new logins, and access tokens can carry a slightly randomised TTL so expiries spread out.
Session store hot path
At 20k refreshes/s, every refresh writes the session row. Shard sessions by user_id, keep writes minimal (one row update), and cache session lookups in Redis with write-through.
10 Failure handling
Auth service down
Existing access tokens keep working because verification is local, so most traffic is unaffected for up to the token lifetime. New logins and refreshes fail; apps should keep using cached tokens and retry with backoff. Run the auth service active-active across availability zones and regions, with the users database replicated, because auth is the most critical dependency.
SMS provider outage
OTP delivery fails. Use two or three SMS providers with health-based routing, a WhatsApp or voice-call fallback, and longer-lived sessions so fewer users need to log in during the outage.
Signing key leaked
Remove the key from JWKS, sign with a new key, and force all sessions to refresh. Because refresh tokens are opaque and server-side, users stay logged in after one refresh; only tokens signed with the leaked key stop working.
11 What impresses
- States clearly that JWTs cannot be revoked before exp and designs the hybrid with numbers
- Explains refresh token reuse detection and the grace window for honest races
- Walks through zero-downtime key rotation with JWKS caching
- Brings up OTP SMS pumping cost and concrete limits per phone, IP and device
- Notes that auth outage affects everything and designs for local verification and multi-region
- Mentions PKCE, system browser instead of WebView, and alg/aud validation pitfalls
12 Mistakes
- Putting long-lived JWTs (days) on clients with no revocation plan
- Every API calling the auth service to validate every token
- Using the implicit flow or storing tokens in localStorage without considering XSS
- Not checking aud and iss, or accepting the alg from the token header
- Storing refresh tokens or OTPs in plain text
- No rate limits on OTP send and verify
13 Follow-ups
Lessons this design uses
Practice questions
- How do you authenticate a WebSocket connection, and what happens when the user's token expires or is revoked while the connection is open for hours?
- You are building login for a new web and mobile app. Should you use server-side sessions or JWTs? Walk me through the trade-offs.
- A user reports their phone was stolen. Your system uses JWT access tokens valid for 24 hours. How do you make sure the thief loses access immediately, and how would you redesign token handling?
- Your user table stores passwords as SHA-256 hashes. A security reviewer says this is not good enough. Why, and what should you do?
- Your mobile app lets users 'Sign in with Google' and also calls your own APIs. Explain the OAuth flow you would use and why PKCE matters.
- Design a central authorisation service that all product teams call to answer 'can user U do action A on resource R?'. It needs p99 under 10 ms and must handle 1 million checks per second.
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.