Two FactorAuth Interview Questions | JiQuest

add

#

Two FactorAuth

System design deep dive · HLD

Design a Two-Factor Authentication (2FA) system: full high-level design.

Requirements, back-of-envelope capacity estimation, a high-level design diagram, a deeper deployment architecture diagram, sequence diagrams for TOTP verification and SMS/push OTP delivery, and an entity-relationship diagram for the data model - with the reasoning an interviewer expects behind every box and arrow.

200MEnrolled users
30sTOTP time-step window
5/minVerify attempts per user cap
Login formenters 6-digit code 2FA verify svcchecks TOTP Rate limiterper user/IP Session grantedshort-lived JWT Secret vaultencrypted TOTP seed only on valid code

1. Clarify requirements before drawing any box

A strong system design answer starts by pinning down scope out loud. For 2FA, that means separating "prove possession of a second factor" from "what happens when the user loses that factor" - recovery is at least as important as verification and is where most real-world incidents happen.

Functional requirements

Enroll a factorProvision a TOTP secret (QR code for an authenticator app) or register a phone number for SMS/push OTP.
Verify a codeAccept a 6-digit TOTP/HOTP code or an SMS/push-delivered OTP and confirm it matches, within a bounded time/counter window.
Deliver OTP out-of-bandSend a one-time code via SMS or a push notification to a registered device on demand.
Recovery via backup codesLet a user who lost their device authenticate with a single-use backup code and re-enroll a new factor.

Non-functional requirements

Secret confidentialityTOTP seeds and backup codes must be unreadable even to someone with raw database access.
Brute-force resistanceA 6-digit code space is only 1,000,000; rate limiting is not optional, it is load-bearing for security.
Availability independent of SMS carriersTOTP verification must keep working even if the SMS gateway is degraded or a carrier is down.
Low verify latencyVerification sits on the login critical path; target <150ms p99 so login doesn't feel slow.
Explicitly out of scope WebAuthn/FIDO2 hardware security keys, biometric factors, and risk-based adaptive authentication (step-up only on anomalous login) are called out as extensions rather than core requirements, so the core design stays focused on TOTP/HOTP, OTP delivery, and backup-code recovery.

2. Back-of-the-envelope capacity estimation

These numbers decide whether the verify path can run entirely off a cache/replica read, how big the SMS delivery bill is, and how much the rate limiter needs to hold in memory per user.

MetricAssumptionResulting estimate
Enrolled users200 million with 2FA enabled~200M rows in otp_secrets
Logins requiring 2FA/day~15% of 500M daily logins~75M verify calls/day ≈ 870/sec average, ~8,000/sec peak (morning login surge)
SMS/push OTP sends~20% of verifies use SMS/push instead of TOTP app~15M SMS/push sends/day, at ~$0.01-0.05/SMS a meaningful direct cost driver
Storage per secret record~300 bytes (encrypted seed, algorithm, counter/step metadata)200M × 300B ≈ 60 GB - comfortably fits a replicated relational cluster
Rate-limit stateSliding window counter per user, ~100 bytes each~200M × 100B ≈ 20 GB hot working set - fits a Redis cluster
Why this matters The SMS cost line is the number most interviewers don't expect a candidate to raise: at 15M sends/day, SMS OTP is a direct operating expense and a third-party-availability dependency, which is exactly why TOTP (free, offline, no carrier dependency) is pushed as the default and SMS/push is offered as a fallback rather than the primary factor.

3. High-level design (HLD)

The HLD names the major components and the one-directional data flow between them, without committing yet to specific infrastructure, regions, or replica counts - that level of detail belongs in the architecture diagram in the next section.

Clientweb / mobile Auth gatewayTLS, session check Enrollment servicewrite path Verification serviceread/verify path KMS (envelope keys) Rate limiter (Redis)sliding window Secret storeencrypted at rest OTP delivery gatewaySMS / push Audit logasync writer
Stateless servicesFast-path infraDurable storageAsync / edge

What each box owns

Enrollment service (write path)

Generates a random 160-bit TOTP seed, wraps it via envelope encryption (a data key from the KMS encrypts the seed, the KMS key itself never leaves the KMS), renders the provisioning QR code (otpauth:// URI), and issues 10 single-use backup codes whose salted hashes - never the plaintext - are stored. It never talks to the OTP delivery gateway; SMS enrollment is a separate, lighter-weight phone-number-verification flow.

Verification service (read/verify path)

Decrypts the stored seed via the KMS, computes the expected TOTP for the current time step (and the adjacent ±1 step to tolerate clock drift), and compares it to the submitted code using a constant-time comparison to avoid timing side-channels. Every attempt - success or failure - first passes through the rate limiter, and a failure increments a per-user failure counter used for both rate limiting and account-lock alerting.

KMS and secret store

The secret store never holds a plaintext TOTP seed; it holds ciphertext plus a reference to the KMS data key used to wrap it. This means a database dump alone is useless to an attacker without also compromising the KMS - the two systems are operated with separate access controls specifically so one breach doesn't imply the other.

Rate limiter, OTP delivery gateway, and audit log

The rate limiter enforces both a per-user cap (e.g. 5 verify attempts/minute) and a per-IP cap (to blunt distributed credential-stuffing), backed by Redis for O(1) sliding-window checks. The OTP delivery gateway abstracts away the specific SMS/push provider behind a common interface so a provider outage can fail over to a secondary. The audit log records every enrollment, verify attempt, and backup-code use asynchronously, so a slow audit write can never add latency to the login path.

4. Detailed architecture diagram

The architecture diagram takes every HLD box and answers "how is this actually deployed?" - replica counts, sharding, regions, and the specific technology choice, which is what an interviewer is checking for once they've accepted the high-level shape.

Edge layer WAF / bot filter API gateway + L7 LB Rate limiterper-user / per-IP CAPTCHA on repeated failure Region: us-east-1 Enrollment svc ×4 pods Verification svc ×12 pods Regional KMS (envelope keys) Redis cluster (rate limits) Region: eu-west-1 (active-active) Enrollment svc ×3 pods Verification svc ×8 pods Regional KMS + Redisno cross-regionsecret replication Secret store Shard 0-3 Shard 4-7 each shard: 1 primary + 2 encrypted replicas OTP delivery pipeline Delivery queue Provider router primary SMS provider, failover to secondary Audit & anomaly store Append-only audit log feeds fraud/anomaly detection
DecisionChoiceReasoning
Secret replication scopeKMS keys and Redis rate-limit state stay regional, not cross-region replicatedMinimizes the blast radius of a single region's key material being compromised; a user's secret is decrypted only where it was enrolled or via a controlled re-key migration.
Secret store sharding keyHash of user_idEven distribution; the verification service always has user_id at verify time, so no secondary lookup is needed to find the shard.
OTP delivery provider strategyProvider router with automatic failover, not a single hard-coded SMS vendorA single carrier/provider outage must not lock every SMS-based user out; the router shifts traffic to a secondary provider transparently.
Audit isolationSeparate append-only store, written asynchronouslyA backlog in fraud/anomaly processing must never add latency to the synchronous verify path that gates login.

5. Sequence diagrams for the two critical flows

A sequence diagram is where an interviewer checks whether you actually understand call order, what is synchronous versus fire-and-forget, and exactly where rate limiting sits relative to the actual verification logic.

5.1 TOTP verification during login

Client Verify service Rate limiter KMS Secret store 1. POST /verify {code=482913} 2. check & increment attempt count 3. under limit (2/5 this minute) 4. fetch encrypted seed + decrypt 5. read ciphertext, unwrap key, decrypt 6. compute TOTP for T-1, T, T+1; constant-time compare 7. write success audit event (async) 8. 200 OK, session token issued

Step 3's rate-limit check happens before any decryption or comparison work runs, so a client that has already exhausted its attempt budget is rejected cheaply without ever touching the KMS - protecting both the user's account and the KMS's request quota from a brute-force loop. Step 6's ±1 time-step tolerance (90 seconds total) absorbs realistic clock drift on the user's phone without meaningfully widening the guessable window.

5.2 SMS/push OTP delivery with rate limiting

Client Verify service Rate limiter Provider router SMS carrier 1. POST /otp/send 2. check send cooldown (1 per 30s per user) 3. allowed 4. generate 6-digit OTP, store hash + TTL=5min 5. send(otp, phone) 6. primary provider timeout 7. retry via secondary provider, delivered 8. 202 Accepted, "code sent"

Only the OTP's salted hash is stored (step 4), the same principle as a password, so a database read can never directly reveal a still-valid code. Step 6-7's provider failover is why the OTP is generated and persisted before any provider is contacted - if the send itself fails and needs to be retried through a different provider, the code stays valid and does not need to be regenerated, which would otherwise invalidate a code the user might already be looking at from a slow first delivery.

6. Entity-relationship (ER) diagram and schema

The data model has to answer three questions: how is a TOTP seed protected at rest, how is a brute-force attempt tracked without becoming the write bottleneck, and how does backup-code consumption stay atomic so a code can never be used twice.

users PK id BIGINT email VARCHAR phone_e164 VARCHAR 2fa_enabled BOOLEAN created_at TIMESTAMP otp_secrets PK id BIGINT FK user_id BIGINT seed_cipher BYTEA kms_key_ref VARCHAR algorithm ENUM backup_hashes VARCHAR[] created_at TIMESTAMP status ENUM auth_attempts PK id BIGINT FK user_id BIGINT method,result ENUM attempted_at TIMESTAMP 11 1N one user has one active otp_secrets record; one user has many auth_attempts

Key modeling decisions

seed_cipher is envelope-encrypted, never plaintextkms_key_ref points at the KMS data key that wraps the seed; a raw DB dump alone cannot yield a usable TOTP secret.
backup_hashes stores salted hashes, one per codeEach of the 10 backup codes is hashed independently so consuming one can be an atomic "mark used" update without touching the others.
auth_attempts is append-only and time-partitionedHigh write volume (every login attempt) rolls off old partitions without locking recent inserts, mirroring a click-log or event-log table.
NoSQL alternativeA key-value store keyed on user_id works for otp_secrets since access is always by user_id, but auth_attempts benefits from a wide-column store (Cassandra) for its time-series write pattern.
Storage choiceUse whenWatch out for
Relational (Postgres), sharded by user_idYou want row-level encryption at rest plus transactional "verify and mark backup code used" atomicity.Cross-shard analytics (e.g. global fraud queries) need a separate read-optimized replica or warehouse.
Key-value / wide-column (DynamoDB/Cassandra)Access pattern is purely "get secret by user_id" and "append attempt event" at very high scale.Atomic backup-code consumption needs conditional writes; ad-hoc joins for support tooling are harder.

7. Deep dives interviewers actually probe

TOTP vs HOTP - why time-based, and how is clock drift handled?

TOTP (RFC 6238) derives the code from the shared seed and the current 30-second time step; HOTP (RFC 4226) derives it from the seed and a monotonically increasing counter. TOTP is preferred because it self-resyncs every 30 seconds without any server-side counter state to track per device, while HOTP's counter can desync if a code is generated but never submitted (common on hardware tokens with a physical button). The verify service checks the current step and one step on either side (±30s) to tolerate realistic clock drift on the user's device without materially widening the brute-force window.

// TOTP core (RFC 6238): HMAC-SHA1 over the time counter, truncated to 6 digits
long timeCounter = Instant.now().getEpochSecond() / 30;
byte[] hmac = HmacSHA1(seed, timeCounter);
int code = truncate(hmac) % 1_000_000;

Why is rate limiting "load-bearing" for a 6-digit code?

A 6-digit code has only 1,000,000 possible values, and any 30-second window has a valid code. Without rate limiting, an attacker could plausibly brute-force it in well under a minute against an unthrottled endpoint. A cap like 5 attempts/minute per user (plus a separate per-IP cap to blunt distributed attempts) reduces the practical success probability to effectively zero within the code's validity window, which is why the rate limiter sits in front of the cryptographic comparison, not behind it.

Backup-code recovery - how do you prevent reuse and abuse?

Each of the 10 issued backup codes is single-use: consuming one is a conditional UPDATE that flips its stored hash's status from unused to used only if it was still unused, making concurrent double-submission safe. After a backup code is used, the user is prompted to re-enroll a fresh TOTP factor and the remaining backup codes are optionally invalidated if the login pattern looks anomalous (new device, new geography), treating "used a backup code" itself as a moderate-risk signal worth extra scrutiny.

What happens when the SMS provider is down or a number is ported (SIM-swap risk)?

The provider router fails over to a secondary SMS/push vendor automatically (sequence 5.2), bounding provider-outage impact. SIM-swap is a distinct threat: because SMS OTP is inherently phone-number-bound, high-risk actions (changing the registered phone number itself, or disabling 2FA) require re-verifying via the existing factor first and impose a cooldown period before the new number becomes usable for recovery, specifically to blunt "port the number, then immediately use it to take over the account" attacks.

How do you protect the TOTP seed if the database is ever dumped?

Envelope encryption: each seed is encrypted with a per-record or per-tenant data key, and that data key is itself encrypted by a master key that lives only in the KMS and never touches application memory in plaintext form for longer than a single decrypt operation. A database dump yields only ciphertext plus opaque key references - useless without also compromising the KMS, which is operated under separate credentials and audit logging specifically so the two breaches are not correlated.

What is the single biggest bottleneck as this scales 10x?

Not TOTP verification - it is cheap, stateless HMAC computation once the seed is decrypted. The real bottleneck becomes KMS decrypt call volume, since most KMS providers charge and rate-limit per API call. That is solved by caching a short-lived (single-request-scoped, in-memory only, never persisted) decrypted seed within the verify request's execution context, and by batching KMS calls where the provider supports it, rather than increasing KMS request quota indefinitely.

8. Summary: what a strong answer covers

Clarified scope before designingJustified every number with a calculationRate limiting placed before crypto comparison Envelope-encrypted secrets via KMSProvider failover for OTP deliveryAtomic, single-use backup codes
Interview tip When asked to design a 2FA system, the strongest signal is treating recovery (backup codes, SIM-swap protection, re-enrollment) as a first-class part of the design rather than an afterthought - most real 2FA incidents are recovery-flow abuse, not TOTP algorithm weaknesses.
No comments
Leave a Comment