Second factors and recovery Required
Confirm a second factor before turning it on
Turn on a new second factor only after the person proves it works once, and let them give it a name.
How to do it
For an authenticator app, show the QR code and the secret as text, then ask for a current code. Accept one 30-second step either side. Store the last accepted time step and refuse any code from that step or an earlier one, as NIST SP 800-63B-4 section 3.1.4.2 requires. The same check applies at every sign-in. For a passkey or security key, the successful registration is the proof. Keep the factor pending until then, and drop pending ones after 15 minutes. Ask for a name, such as "Work phone", and show it with the date added and last used. Rate limit code entry, as offer two-factor login says.
Example
import { verify } from 'otplib';
const DRIFT_SECONDS = 30;
export async function confirmTotp(db, userId, code) {
const pending = await db.totp.findPending(userId);
if (!pending) return false;
const result = await verify({ secret: pending.secret, token: code, epochTolerance: DRIFT_SECONDS, afterTimeStep: pending.lastTimeStep ?? undefined });
if (!result.valid) return false;
await db.totp.activate(pending.id, { lastTimeStep: result.timeStep });
return true;
}
import hmac
import time
import pyotp
STEP_SECONDS = 30
DRIFT_STEPS = 1
def matching_step(secret: str, code: str, last_step: int | None) -> int | None:
totp = pyotp.TOTP(secret)
current = int(time.time()) // STEP_SECONDS
for step in range(current - DRIFT_STEPS, current + DRIFT_STEPS + 1):
if (last_step is None or step > last_step) and hmac.compare_digest(totp.at(step * STEP_SECONDS), code):
return step
return None
def confirm_totp(db, user_id: str, code: str) -> bool:
pending = db.totp.find_pending(user_id)
step = matching_step(pending.secret, code, pending.last_step) if pending else None
if step is None:
return False
db.totp.activate(pending.id, last_step=step)
return True
How to check
Start authenticator setup and submit a wrong code. Two-factor login stays off. Submit the current code, and it turns on under the given name. At sign-in, the same code a second time is refused.
For agents: if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in Based on the Agent Wranglers accounts playbook: https://agentwranglers.se/playbook/accounts/second-factors-and-recovery/mfa-enrolment.