2FA that can't lock me out
I wrote the TOTP code from the RFCs and built the state machine around one failure: me, locked out of my own admin panel.
There is exactly one account on this site's admin console: mine. That makes the threat model for its two-factor auth lopsided. The odds that an attacker brute-forces the password past the rate limiter are low. The odds that I fumble an enrollment step on some future phone are, based on my history with authenticator apps, excellent. So I built the 2FA around the second risk.
The code itself is dependency-free. TOTP is RFC 6238, which is a thin layer over RFC 4226: HMAC a counter derived from the clock, truncate, take six digits. It is a small amount of hashing glue, and before trusting my implementation I ran it against the test vectors published in RFC 4226 itself. My opinion on the tradeoff: for an algorithm this small and this well specified, a third-party auth package is more attack surface than safety. The RFC comes with test vectors. A package comes with a maintainer.
The lockout-safety is in the state machine, not the crypto. Enrolling stages a secret as pending. A pending secret cannot be enabled. The only transition to active runs through a live six-digit code, which proves the authenticator app actually holds the secret it claims to. The classic self-inflicted lockout, enable 2FA and then discover the QR scan silently failed, is not a bug I fixed. It is a sequence the state machine cannot represent. There is no code path from pending to enforced that skips the proof.
Enable and disable are also deliberately not the same lever as reset. Disable keeps the secret, so toggling 2FA back on later takes one click and no re-enrollment. Reset wipes the secret for a new phone. They look redundant on the settings page and they are not: one answers "pause it," the other answers "start over," and conflating them is how systems end up wiping secrets people wanted kept.
Then there is the valve. Setting DISABLE_2FA=1 in the host environment forces 2FA off. That sounds like a backdoor until you look at the trust ladder: changing host environment variables requires the hosting account itself, which is a strictly stronger credential than the TOTP code it bypasses. That is the rule for escape hatches in general. A recovery path is safe exactly when opening it requires more privilege than it grants. Under that rule, the valve does not weaken the system; it defines the system's floor as "whoever owns the hosting account," which was already true.
Around all of this sit the boring perimeter numbers: login attempts are rate-limited per IP, and a session cookie lasts one day before the password is required again. This week's audit pass tightened the rest of the admin surface to match, one shared guard for every admin API route instead of per-route improvisation.
One honest caveat lives in the docs next to the feature. The 2FA state sits in a JSON file store on the host, and it has survived every restart so far, but whether the host preserves untracked files across a full rebuild is unverified. If it ever does not, the failure is a wiped secret: 2FA silently off, one re-enrollment, no lockout, which is the correct direction for that unknown to fail in. The documented plan, if it bites, is moving the state to a path the host guarantees or into the database. I would rather ship with a known unknown written down than with a guarantee I have not tested.
Most 2FA designs defend against an attacker who has your password. Mine also defends against me, at 2am, holding a new phone and old confidence. Between those two adversaries, only one has ever actually shown up.