I was going through the auth code in Local Medical Diary looking for something unrelated when I noticed the login handler didn't reference two_factor_enabled anywhere. That felt wrong, because the product very much has two-factor auth — there's a setup flow with a QR code, backup codes you can download, and a toggle in Settings that says "Enabled" once you've gone through it.
So I went looking for where that flag actually gets checked during login. It doesn't. Login verifies email and password with bcrypt and mints a full session JWT the moment that check passes. That's it. No other file in the codebase called the TOTP verification function or read the two-factor secret outside of the settings page where you turn it on. There was no test for the login handler either, so nothing had ever caught this.
In practice: an account could show "2FA enabled" in Settings, with real backup codes generated and everything, and still be fully accessible to anyone who had the password. The second factor existed as a feature. It just wasn't wired into the one place that mattered.
The fix I went with is a pending-token pattern. When a 2FA-enabled account logs in successfully with a password, the backend now withholds the real session and returns a short-lived pending token instead. That token is explicitly marked as pending, and the auth middleware rejects it on every protected route — so even if someone intercepted it, it's useless against anything except the one new endpoint built to consume it: /api/auth/login/verify-2fa. Submit a valid TOTP code or backup code there, and that's when the real session gets issued.
On the frontend, the login form needed a second step to actually use this — enter your code, then get logged in — otherwise anyone with 2FA turned on would just be stuck with a pending token and no way to finish signing in. That part's shipped. There's a related gap I flagged but didn't fix in the same pass: the WhatsApp magic-link login path mints a full session the same old way, with no 2FA check either. That's next.
The lesson I keep coming back to: a feature that's fully built in the UI can still be completely absent in the one code path that decides whether it matters. If you want to know whether an auth check actually runs, don't look at the settings page — read the function that issues the session, and ask what it doesn't look at.