---
title: "Confirm a second factor before turning it · Agent Wranglers"
url: https://agentwranglers.se/playbook/accounts/second-factors-and-recovery/mfa-enrolment
updated: 2026-10-20
licence: "All rights reserved"
---

# Confirm a second factor before turning it on

By Erik Bergstedt, Agent Wranglers. Updated 2026-10-20.

- Level: Required
- Chapter: [Second factors and recovery](https://agentwranglers.se/playbook/accounts/second-factors-and-recovery)
- Rule: Turn on a new second factor only after the person proves it works once, and let them give it a name.
- How: 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](https://agentwranglers.se/playbook/adversarial-testing/access-and-accounts/two-factor-login) says.
- 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.

Example:

```js
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;
}
```

```python
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
```

**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`.

- Next: [Give recovery codes with the first second factor](https://agentwranglers.se/playbook/accounts/second-factors-and-recovery/recovery-codes)
- [Second factors and recovery](https://agentwranglers.se/playbook/accounts/second-factors-and-recovery)
