Methods

A code is derived from a shared secret and the current time step, so the same code can reach the user three ways:

Method How the user gets the code Secret lives
app Scans a QR code once, then their device derives every code offline Server and device
email The server derives the code and sends it by email Server only
sms The server derives the code and sends it by SMS Server only

Generation and verification are the same two calls in all three cases. getCode() is what lets the server derive the code it expects, so email and SMS need no second implementation.

app is the strongest of the three, because the code never travels. Offer the others to users who cannot or will not install an authenticator app.

Why delivered codes need a wider window

A delivered code has to survive a mail queue or an SMS carrier, so it cannot expire after one time step of period seconds. Widening verifyCode()'s $window is how its lifetime is extended, and it costs something: every step inside the window is a valid code, and a code stays valid for the whole window rather than being consumed on use.

Three measures pay for that, and none of them is optional.

  • Rotate the secret on success. A delivery user's secret exists only on the server, so replacing it after an accepted code makes that code single-use and invalidates every other code still inside the window.
  • Cap the attempts. A six-digit code is about a million combinations, and a wide window means several are live at once. The attempt limit, not the algorithm, is what makes guessing impractical.
  • Cool down the resend. Without it a caller can spend every attempt, request a fresh code immediately, and repeat at full speed.

The cooldown is per user. It does not slow an attacker spraying codes at many accounts, so rate limit the routes as well.

Security notes

  • Never log a code, a secret or a provisioning URI - the URI carries the secret in its query string.
  • Keep the secret out of the database until an app user has confirmed a code derived from it.
  • Send only to an address or phone number the user has already verified.
  • Store recovery codes hashed, and only hashed.