Disabling 2FA
Why disabling 2FA needs the second factor
If 2FA could be disabled with just a password, an attacker who obtains the password (phishing, breach, shoulder surfing) could disable 2FA and take over the account. The password alone is not enough — that is the entire point of 2FA.
To disable 2FA, the user must prove they have the second factor: either a valid TOTP code or a recovery code. This ensures the person disabling 2FA actually controls the authenticator, not just the password.
The disable endpoint
route.post("/me/2fa/disable", {
resolve: async (c) => {
const user = authenticate(c.request);
if (user instanceof Response) return user;
const body = await c.request.json();
const { code, recoveryCode } = body;
// Get the TOTP secret
const row = db
.prepare("SELECT secret, enabled FROM totp_secrets WHERE user_id = ?")
.get(user.id) as { secret: string; enabled: number } | undefined;
if (!row || row.enabled !== 1) {
return Response.json({ error: "2FA is not enabled" }, { status: 400 });
}
// Verify the second factor
let verified = false;
if (recoveryCode) {
verified = await verifyRecoveryCode(user.id, recoveryCode);
} else if (code) {
verified = verifyTOTPCode(row.secret, code);
}
if (!verified) {
return Response.json(
{ error: "Invalid code. Provide a valid TOTP code or recovery code to disable 2FA." },
{ status: 401 },
);
}
// Disable 2FA
db.prepare("DELETE FROM totp_secrets WHERE user_id = ?").run(user.id);
db.prepare("DELETE FROM recovery_codes WHERE user_id = ?").run(user.id);
return Response.json({ message: "2FA has been disabled." });
},
}); The endpoint requires either a TOTP code or a recovery code. On success, it deletes both the TOTP secret and all recovery codes. The user’s login flow reverts to password-only.
Why we delete rather than set enabled = 0
Setting enabled = 0 would leave the secret in the database. If the user re-enables 2FA later, we would reuse the old secret. This has two problems:
- The old QR code (which the user might have shared, screenshot, or not properly destroyed) would work again.
- If the reason for disabling was that the secret was compromised, re-enabling would reuse the compromised secret.
Deleting the secret and recovery codes forces a fresh setup if the user re-enables 2FA.
Exercises
Exercise 1: With 2FA enabled, try disabling it without any code. It should fail.
Exercise 2: Disable 2FA with a valid TOTP code. Verify the login flow is now single-step (no requiresTwoFactor).
Exercise 3: Re-enable 2FA. The setup endpoint should work (no existing secret). You should get a new QR code with a new secret.
Why must disabling 2FA require a TOTP or recovery code, not just the password?