Two-Factor Authentication System: Low-Level Design

Two-factor authentication (2FA) requires users to present two forms of evidence: something they know (password) and something they have (phone, hardware key). Even if a password is stolen, an attacker without the second factor cannot log in. Designing a 2FA system requires understanding TOTP (Time-based One-Time Passwords), SMS OTPs, push notifications, and the UX trade-offs of each approach.

TOTP: Time-Based One-Time Passwords

TOTP (RFC 6238) generates a 6-digit code that changes every 30 seconds, computed from a shared secret and the current time. Algorithm: TOTP(secret, time) = HOTP(secret, floor(unix_timestamp / 30)). HOTP = HMAC-SHA1(secret, counter), truncated to 6 digits. The server and the authenticator app (Google Authenticator, Authy) independently compute TOTP with the same secret and timestamp — if they match, authentication succeeds. Setup: generate a random 20-byte secret, encode as base32, encode in a QR code: otpauth://totp/{issuer}:{account}?secret={base32secret}&issuer={issuer}. The user scans the QR code with their authenticator app. The server stores the secret (encrypted at rest). Validation: the server accepts TOTP codes for the current 30-second window plus one window on each side (to handle clock skew between server and device). Replay prevention: track the last accepted counter value per user — reject codes that have already been used within the valid window. Secrets must be encrypted at rest and isolated — a database breach should not expose all TOTP secrets.

SMS OTP

SMS OTP: the server generates a random 6-digit code, stores it server-side with a TTL, and sends it to the user’s registered phone number via an SMS provider (Twilio, AWS SNS). On submission, the server looks up the stored code and compares. SMS OTP is less secure than TOTP: SIM swapping (an attacker convinces the carrier to transfer your phone number to their SIM), SS7 network attacks (intercept SMS in transit), and phishing attacks (fake login page also requests the OTP). NIST deprecated SMS OTP as a primary 2FA method in 2016 due to these vulnerabilities, though it remains widely used for its accessibility. SMS OTP storage: store as a hashed value (SHA-256 of the code). Do not store the plaintext code — a database breach shouldn’t expose active OTPs. On validation: hash the submitted code and compare against the stored hash. Rate limiting: limit to 5 OTP attempts per 15 minutes per user. Lock the account if the limit is exceeded and require re-sending a new code. Throttle OTP sends: allow at most 1 OTP request per 60 seconds (prevent SMS flooding).

Push Notifications and Number Matching

Push-based 2FA (Duo Security, Microsoft Authenticator): instead of the user typing a code, the server sends a push notification to the user’s registered device asking “Did you just try to log in?” The user approves or denies. Number matching enhancement: display the same 2-digit number on both the login screen and the push notification — the user must select the matching number on their phone. This prevents push bombing (attacker repeatedly sends push notifications hoping the user approves one by mistake) because the user must actively read and match the number. Implementation: the push notification service (Firebase Cloud Messaging, APNs) delivers the notification to the mobile app. The app shows the approval screen with the number. On approval/denial, the mobile app calls the authentication service API. The authentication service updates the pending 2FA session state. The browser polling the authentication service receives the update and proceeds or rejects. State is stored server-side in Redis with a 5-minute TTL keyed by session_token: {session_token: {user_id, number, status, expires_at}}.

Hardware Security Keys (WebAuthn / FIDO2)

Hardware security keys (YubiKey, Google Titan) are the strongest 2FA form: phishing-resistant because the key cryptographically binds authentication to the origin domain. WebAuthn (Web Authentication API): the browser mediates communication between the website and the security key. Registration: the server generates a challenge; the browser calls navigator.credentials.create() with the challenge and server origin. The key generates a public/private key pair, signs the challenge with the private key (which never leaves the key), and returns the public key and attestation. The server stores the public key for this credential. Authentication: the server generates a new challenge; the browser calls navigator.credentials.get(); the key signs the challenge with the private key; the server verifies the signature using the stored public key. Phishing prevention: the key includes the origin (website domain) in the signed data. A phishing site at fake-bank.com cannot use credentials registered for real-bank.com — the signatures would be for different origins. WebAuthn also works as a passwordless authentication mechanism (passkeys) — the key can be a phone using biometric authentication.

Recovery Codes and Account Recovery

Users lose phones and hardware keys. Recovery codes are pre-generated one-time codes that allow account access when 2FA is unavailable. Generation: generate 8-10 random 16-character codes (alphanumeric, hyphen-separated: a3b4-c5d6-e7f8). Store SHA-256 hashes in the database — not plaintext. Show to the user once at 2FA setup with instructions to print or store securely. Each code is single-use: on redemption, mark as used. If all codes are exhausted, provide an identity verification process (support ticket with government ID verification) to reset 2FA. Recovery code display UX: show all codes on one page with a “Download” button — never show them in subsequent sessions. Warn: “These codes cannot be shown again.” Backup 2FA methods: allow users to register multiple 2FA methods (TOTP + hardware key). If the primary method is unavailable, fall through to the backup. Enforce that at least one method is always active — prevent users from accidentally removing all 2FA methods and locking themselves out.

Scroll to Top