Every method so far has been a single proof of identity. Multi-factor auth stops betting everything on one — it combines proofs of different kinds, so cracking one isn't enough. Today's star is TOTP, the 6-digit authenticator-app code. You'll implement it from scratch in ~20 lines and prove your implementation correct against the official RFC test vector — the same math Google Authenticator runs.
🎯 Mission: a mental model of modern auth + the ability to wire it into a real appStrong auth combines factors of different types, not more of the same type. The three types: something you know (password), something you have (phone/authenticator/passkey), something you are (biometric). Two passwords aren't MFA; a password + a TOTP code are. TOTP works because both sides derive the same short-lived code from a shared secret + the current time — no code is ever transmitted to be intercepted.
"Multi-factor" specifically means proofs from different categories. Stacking two of the same kind barely helps — an attacker who can phish one password can phish two. The leverage comes from forcing an attacker to compromise two unrelated things at once.
Password, PIN, security question. Weakest — phishable, guessable, reused. (Lesson 4.)
Your phone (TOTP/push), a security key, a passkey. The device is the proof. (This lesson + Lesson 9.)
Fingerprint, face. Usually unlocks a "have" factor locally (e.g. Touch ID releasing a passkey) rather than traveling over the wire.
TOTP (Time-based One-Time Password, RFC 6238) is beautifully simple. At enrollment, server and app agree on one shared secret (the QR code you scan). After that, nothing is ever exchanged — both sides independently compute the same code from secret + current time. Three steps:
floor(unixSeconds / 30) — the current 30-second window, so the code changes every 30s.HMAC-SHA1(secret, counter) — a keyed hash binding the secret to this time window.mod 10⁶ → the 6-digit code you see.otpauth:// URIotpauth://totp/MyApp:you?secret=BASE32&issuer=MyApp. The app stores the secret; that's the whole handshake.
Phones drift. Check the current step and the adjacent ones (valid_window=1) so a slightly-off clock still works.
A code is valid for ~30s — within that window, record an accepted code so it can't be replayed twice.
Encrypt secrets at rest; issue one-time backup codes at enrollment for a lost device.
A zero-dependency lab implements RFC 6238 in ~20 lines and checks it against the spec's official test vector — then shows enrollment, login, clock-skew tolerance, and expiry.
# in your terminal (no npm install needed):
cd ~/projects/learn/public/courses/auth/practice/totp-lab
node demo.js
PROOF — official RFC 6238 test vector (seed "12345…", T=59, 8 digits): expected: 94287082 ours: 94287082 ✅ MATCH — our implementation is correct login: app shows 274917 → server verify {"ok":true,"drift":0} clock skew: code from 30s ago → {"ok":true,"drift":-1} (accepted) expiry: code from 5 min ago → {"ok":false}Matching
94287082 means your 20 lines are byte-for-byte the same algorithm as every authenticator app on earth — you could scan the lab's otpauth:// URI into Google Authenticator and the codes would line up. That's the payoff: TOTP isn't a black box anymore. Open demo.js; the whole algorithm is the totp() function.TOTP is a second factor (something you have), layered on a first (password or — increasingly — a passkey). It massively raises the bar: a leaked password alone no longer logs anyone in. But be honest about its ceiling:
You don't need the second factor on every click. Step-up auth asks for it only for sensitive actions — changing a password, a large transfer, viewing secrets — while normal browsing rides the existing session. The pattern: record when and how strongly the user last authenticated, and re-challenge if a high-risk action needs a fresher/stronger proof.
function requireRecentMfa(maxAgeMs) { // Express middleware return (req, res, next) => { const { mfaAt } = req.session; if (!mfaAt || Date.now() - mfaAt > maxAgeMs) return res.redirect('/verify-mfa?next=' + encodeURIComponent(req.originalUrl)); next(); }; } app.post('/account/delete', requireRecentMfa(5 * 60_000), handler); // re-prove within 5 min
Q1. A site requires your password and a security-question answer. Is that MFA? Why or why not?
No — both are "something you know," so it's two factors of the same type. An attacker who phishes or researches one can usually get the other, and neither requires possession of a device. Real MFA combines different categories (know + have, e.g. password + TOTP). (§01.)
Q2. TOTP never sends the code over the network during login setup — so how does the server know what code to expect?
Because both sides share the secret (exchanged once, at enrollment, via the QR/otpauth:// URI) and both compute the code independently from secret + floor(time/30) via HMAC. The server just recomputes its own code and compares. Nothing but the final 6 digits is ever typed, and even those are ephemeral. (§02.)
Q3. Your boss says "we added TOTP, so we're safe from phishing now." Correct them.
TOTP defeats offline attacks (a leaked password alone is now useless) but not a real-time phishing proxy: a fake site can collect the password and the live TOTP code and replay both within 30 seconds. The phishing-proof factor is the origin-bound passkey (Lesson 9), because its signature can't be relayed to another origin. TOTP raises the bar; passkeys close the door. (§05.)
You can now layer factors and gate sensitive actions. Two practical gaps remain before you can ship a complete app. Lesson 11 covers the account lifecycle — the flows every real app needs and every tutorial skips: email verification, password reset, and account recovery, each riddled with subtle footguns (and each reusing the single-use-token machinery you already know). Then Lesson 12 is the capstone: a decision framework plus a complete, running Express auth app that combines everything.
💬 Ask me anything — e.g. "wire TOTP enrollment with a real QR code in Express (otplib)", "design the backup-codes flow", or "when should we skip passwords entirely and go passkey-only?" · Back to Lesson 9 · Lesson 8 · Resources.