Files
skills/docs/security-stack/security/otp/src/totp/verify-many.md
2026-05-29 08:20:56 +01:00

2.2 KiB

totp/verify-many

Purpose

Backup-aware TOTP verify. Used during rotation windows where an old and a new TOTP secret coexist — the user's authenticator may still be holding the old secret while the server has moved to the new one. Returns the matching secret index and signed step delta so the caller can mark the rotation complete after the user has successfully authenticated against the new secret.

Features

  • Multi-secret loop with constant-time evaluation across the entire list.
  • Same ±window semantics as verify.
  • Returns { valid, delta, secretIndex } so the caller can record which secret was used.
  • Window cap and Zod-boundary semantics shared with verify.

Usage: Basic Example

import { verifyMany } from "@suaveplan/otp";

const r = verifyMany(code, [oldSecret, newSecret]);
if (r.valid) markUsed(r.secretIndex);

Usage: Advanced Example

import { verifyMany } from "@suaveplan/otp";

// During rotation grace period — accept either secret but log when the old
// one is used so the rollout dashboard tracks lag.
const r = verifyMany(code, [oldSecret, newSecret], {
  window: 1,
  clock: () => Date.now(),
});
if (r.valid && r.secretIndex === 0) metric.inc("totp.rotation.legacy");

API Reference

  • verifyMany(code, secrets, opts?): TotpVerifyManyResult — { valid: true; delta; secretIndex } on match, all-null triple on miss.

Implementation Notes

Both loops (over secrets and over windowed steps) run to completion even after the first match so the wall-time of verifyMany is independent of which secret / which step matched. The match flag uses matchedDelta === null to record the first hit without affecting subsequent iterations. This is the same constant-time pattern as verify, extended to cover the secret dimension as well.

When the secret rotation completes (every authenticated user has re-enrolled against the new secret), the caller drops the old secret from the list and verifyMany collapses back to single-secret behaviour. There is no separate "single-secret" code path inside verifyMany — a single-element list runs the same way as a multi-element one. This keeps the constant-time guarantees uniform across rotation states.