Time-based one-time password (TOTP)
The 6-digit code an authenticator app regenerates every 30 seconds, computed from a secret the app and the server both hold.
See it
What it is
TOTP (RFC 6238) is the standard behind every authenticator app. At enrollment the server generates a random shared secret and hands it over in a QR code, which is just an otpauth:// URI carrying that secret plus your app name. From then on both sides hash the secret together with the current 30-second time step and truncate the result to 6 digits. Nothing is transmitted, which is why the code works on a plane and why there is no message for anyone to intercept.
It is the cheapest second factor that is actually worth having: no SMS bill, no vendor, and it works with Google Authenticator, 1Password, Bitwarden, Authy, and every other app that reads the same URI. Reach for it when you want MFA without the cost of SMS or the browser and device requirements of WebAuthn.
Gotchas worth knowing before you ship. Phone clocks drift, so accept the previous and next time step (roughly a 90-second window) and no more. Codes are replayable inside their own window, so store the last accepted time step per user and refuse to accept it twice. The shared secret is password-equivalent: encrypt it at rest and never log it. And TOTP is phishable, because a fake login page can relay the code within 30 seconds, which is exactly the gap passkeys close.
Ask AI for it
Implement RFC 6238 TOTP enrollment and verification using a maintained library (otplib for Node, pyotp for Python) instead of hand-rolling HMAC. Enrollment endpoint: generate a 20-byte base32 secret, build the otpauth://totp/{issuer}:{account}?secret=...&issuer=... URI, render it as a QR code plus copyable text, and keep the secret in a pending state until the user confirms with one valid code. Store secrets encrypted with an app-level key, never plaintext. Verification: SHA-1, 6 digits, 30-second period, a window of plus or minus one step, and persist the last accepted time step per user so the same code cannot be replayed. Rate limit verification to 5 attempts per 15 minutes per account and return a generic error on failure.