This episode manages the 2FA lifecycle: disabling the feature with password and code re-authentication, rotating the secret when a device is suspected of leaking, and removing recovery codes. You will also learn the disabled, pending, and enabled status transitions.

A 2FA feature isn't a static object — it's born, maintained, and sometimes needs to be turned off or replaced. Episode 11 covers this management side: disabling 2FA, rotating the secret, and removing recovery codes. All three are among the most sensitive operations on a user's account.
Why sensitive? Because one attacker scenario is taking over 2FA — turning off the user's own feature and installing it on their own device. That's why every status transition must demand strong proof of identity. You'll build the correct flows and understand the state machine that supports them.
Turning off 2FA is a very dangerous gate: once disabled, the account again depends only on the password. So the server must demand fresh proof of identity — at minimum the password and the current TOTP code:
app.post('/account/disable-2fa', requireAuth, async (req, res) => {
const validPass = await bcrypt.compare(req.body.password, req.user.password_hash);
const secret = decryptSecret(req.user.totp_secret_encrypted);
const validCode = authenticator.check(req.body.token, secret);
if (!validPass || !validCode) {
return res.status(400).json({ error: 'Verifikasi ulang gagal' });
}
await users.update({
where: { id: req.user.id },
data: { totp_secret_encrypted: null, totp_enabled: false }
});
await recoveryCodes.deleteMany({ where: { userId: req.user.id } });
res.json({ ok: true });
});The disable-2fa handler demands two proofs: a correct password and a currently valid TOTP code. Both must pass before the secret is deleted — an attacker with a stolen session alone isn't enough to remove 2FA.
Disabling 2FA also removes the recovery codes in one operation. Leaving old codes behind when the feature is disabled leaves an emergency path with unclear status — delete them all and start clean.
Secret rotation runs when the user suspects their device was lost or stolen, or the 2FA feels off. Rotation replaces the old secret with a new one, forcing Google Authenticator on the old device to stop producing matching codes:
app.post('/account/rotate-2fa', requireAuth, async (req, res) => {
const validPass = await bcrypt.compare(req.body.password, req.user.password_hash);
const validCode = authenticator.check(req.body.token, decryptSecret(req.user.totp_secret_encrypted));
if (!validPass || !validCode) {
return res.status(400).json({ error: 'Verifikasi ulang gagal' });
}
const newSecret = authenticator.generateSecret();
await users.update({
where: { id: req.user.id },
data: { totp_secret_encrypted: encryptSecret(newSecret), totp_enabled: false }
});
await recoveryCodes.deleteMany({ where: { userId: req.user.id } });
res.json({ secret: newSecret });
});Rotation returns to pending status: the new secret is stored encrypted, totp_enabled resets to false, and the user must complete a new enrollment as in episode 6. The old device automatically stops working.
One important decision: rotation must not demand the old TOTP code that a lost device can no longer generate. Require the password (and a recovery code if needed), but don't ask for proof that's technically impossible for the legitimate owner to produce.
The 2FA lifecycle is easiest to understand as a state machine with three statuses:
disabled -> pending -> enabled
^ |
|______ disable ________|Each transition has its own pre-conditions. disabled to pending only needs a session; pending to enabled requires a valid first code; enabled back to disabled requires re-authentication. Rotation is the path from enabled to pending with a new secret.
Every status transition must leave an audit trail: mfa_enabled, mfa_disabled, mfa_rotated, and mfa_recovery_used. Without this trail, the security team can't answer basic questions like who disabled 2FA and when. Episode 20 will cover this observability in full.
When 2FA is disabled or rotated, send a notification through another channel — email or push notification. An account suddenly dropping 2FA without the owner's knowledge is a strong sign the account has been hijacked, and a warning gives the user a chance to react.
Episode 11 completed the 2FA management side: a disable that demands re-authentication, a rotation that replaces the secret and returns the status to pending, and the disabled, pending, and enabled state machine that becomes the shared language of the whole feature.
The key takeaways:
In the next episode, episode 12, we will cover database schema and data management — the users and recovery_codes table structure with indexes and constraints, database migrations, and backup policies that keep 2FA data intact.