threshold1

TOTP

Time-based one-time codes from an authenticator app — Google Authenticator, Authy, 1Password, anything that reads a standard otpauth:// URI. This is a step-up factor, not a way to sign up or log in from zero.

Setup — requires an existing session

TOTP attaches to a user who is already authenticated. There's no registerWithTotp() or loginWithTotp()enrollTotp() and verifyTotp() both require a session, same as calling any other method after login()/register() succeeds. This is deliberate, not a limitation to work around: threshold1's identity model requires an email, and TOTP has no identity of its own to sign up with — it only makes sense as something you add to an account that already exists.

await auth.login("user@example.com"); // establish a session first

const { otpauthUrl, qrCodeDataUrl, secret } = await auth.enrollTotp();
// Show qrCodeDataUrl for scanning, or secret for manual entry.

const { enrolled } = await auth.verifyTotp("123456");
// The first successful call after enrollTotp() completes enrollment.
// Every call after that is an ordinary step-up check.

Calling enrollTotp() again always replaces any existing secret immediately, even a previously-verified one — only call it when the user genuinely intends to (re)set up TOTP, not as a "get the current secret" accessor.

Real configuration values

  • Period: 30 seconds per code, the TOTP standard.
  • Tolerance: ±1 step (previous, current, and next 30-second window — 90 seconds of effective drift tolerance either side of "now"). This isn't itself configurable.
  • No rate limit on verification attempts. This is a factual statement about current behavior, not a gap: protection here comes from the 6-digit code space combined with the replay guard below, not from a request-count lockout the way OTP and backup codes have.
  • Secret encrypted at rest (AES-256, server-side key) — never stored or returned in plaintext after the initial enrollTotp() response.

Replay guard

Each code is usable exactly once, even within its ~90-second valid window — a code that verified successfully can't be replayed a second time before it naturally expires. This is enforced by tracking the specific time-step each code corresponds to and atomically consuming it on first use, so a captured or logged code is only ever good for one login, not for the whole window it happened to fall in.

Failure codes

verifyTotp() returns invalid_code for a wrong or already-used code. See the Error Reference for details.

What's next

  • Backup Codes — the recovery path for when a user's authenticator app is unavailable.