Overview
Two-factor authentication (2FA) requires two different kinds of proof before granting access, usually something you know (a password) plus something you have (a phone app, hardware key) or something you are (a fingerprint). Even if a password is phished or leaked, an attacker still needs the second factor.
The most common second factor is a time-based one-time password (TOTP) from an authenticator app. The server and the app share a secret; both compute a 6-digit code from the secret and the current 30-second time window, so the server can verify the code without any network call. SMS codes are weaker (SIM swapping), while passkeys and hardware security keys (WebAuthn/FIDO2) are phishing-resistant.
The vault needs a combination (something you know) and a physical key (something you have). Learning the combination alone does not open it.
When to use it
- Protecting user accounts, especially admin and financial accounts.
- Meeting compliance requirements (PCI DSS, SOC 2).
- Step-up authentication for sensitive actions (changing email, large transfers).
Where it shows up in interviews
Recognize it when: prevent account takeover.
- Design an authentication service
- Design a banking login flow
Recognize it when: extra check for risky actions.
- Design a payment approval flow
- Design admin console access
Where it is used in real software
Implement TOTP (RFC 6238), compatible with most sites that show a QR code during setup.
Requires 2FA for contributors and supports TOTP, security keys, and passkeys.
Apple, Google, and Microsoft sync WebAuthn credentials across devices as phishing-resistant replacements for passwords plus codes.
Key terms
- Factor
- Knowledge, possession, or inherence (biometric).
- TOTP
- One-time code from a shared secret and the current time window.
- Shared secret
- Random key given to the authenticator app via QR code at enrollment.
- WebAuthn / FIDO2
- Public-key login bound to the website's origin, resistant to phishing.
- Recovery codes
- One-time backup codes for when the device is lost.
How it works, step by step
- 1Enrollment
Server generates a secret, shows it as a QR code (otpauth URI), and the user scans it.
- 2Confirm enrollment
User enters a code from the app to prove the secret was stored correctly; server saves the secret encrypted.
- 3Login step 1
User submits password; server verifies it but does not issue a full session yet.
- 4Login step 2
User enters the current 6-digit code; server computes codes for the current window (and one adjacent) and compares.
- 5Issue the session
On success, create the session; rate limit attempts and offer recovery codes.
STEP 1The user enters email and password. The server verifies the password hash and creates a pending login.
Second-factor options
Security vs convenience
| Factor | Phishing resistant? | Main risk | Notes |
|---|---|---|---|
| SMS code | No | SIM swap, interception | Better than nothing; avoid for high-value accounts |
| Email code | No | Email account compromise | Common for low-risk apps |
| TOTP app | No (codes can be phished in real time) | Phishing proxies | Works offline; widely supported |
| Push approval | Partly | MFA fatigue (spam approvals) | Add number matching |
| Security key / passkey | Yes | Losing the device | Bound to the site's origin |
NOWFactor: SMS code | Phishing resistant?: No | Main risk: SIM swap, interception | Notes: Better than nothing; avoid for high-value accounts
Offer TOTP widely and encourage passkeys or security keys for the strongest protection.
Implementation
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto"; // RFC 6238 TOTP (SHA-1, 30 s, 6 digits) with a raw byte secretfunction totp(secret: Buffer, timeMs = Date.now(), stepSec = 30, digits = 6) { const counter = Buffer.alloc(8); counter.writeBigUInt64BE(BigInt(Math.floor(timeMs / 1000 / stepSec))); const hmac = createHmac("sha1", secret).update(counter).digest(); const offset = hmac[hmac.length - 1] & 0x0f; const code = (hmac.readUInt32BE(offset) & 0x7fffffff) % 10 ** digits; return code.toString().padStart(digits, "0");} function verifyTotp(secret: Buffer, submitted: string, now = Date.now()) { // accept previous, current, and next window for small clock drift return [-1, 0, 1].some((w) => { const expected = Buffer.from(totp(secret, now + w * 30_000)); const actual = Buffer.from(submitted.padStart(6, "0").slice(0, 6)); return timingSafeEqual(expected, actual); });} const secret = randomBytes(20); // store encrypted; encode as base32 in the otpauth:// QR codeverifyTotp(secret, totp(secret)); // trueComplexity and performance
Current window plus drift tolerance.
Rate limiting is essential.
Trade-offs
Each extra step reduces takeover risk but adds friction; use remembered devices and risk-based prompts to balance.
Account recovery flows are often the weakest link; recovery codes and verified support processes must be as strong as the 2FA itself.
Variants and related techniques
Counter-based one-time passwords instead of time windows.
Only prompt for a second factor when risk signals (new device, location) are high.
Common mistakes
- No rate limiting on code entry.
Fix: Limit attempts per account and lock or slow down after failures.
- Storing TOTP secrets in plain text.
Fix: Encrypt secrets at rest with a key management service.
- Accepting a code twice.
Fix: Record the last used time step per user to prevent replay.
Interview questions
How does TOTP work without the server contacting the phone?
At enrollment the server and app share a secret. Both compute an HMAC of the secret and the current 30-second time counter, truncate it to 6 digits, and compare. Because both know the secret and the time, no network communication is needed.
Why are passkeys more secure than SMS or TOTP codes?
Passkeys use public-key cryptography bound to the site's origin, so a phishing site cannot obtain a credential that works on the real site, and there is no shared secret or code to intercept or relay.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Implement TOTP enrollment and verification | Medium | HMAC, time windows, replay protection. |
| Design account recovery for 2FA users | Hard | Balancing security and support. |