Files
verdande/internal/store/migrations/0029_totp_single_use.sql
T
kristianwindandClaude Opus 5 77994eff09 feat(security): a TOTP code can be used once
RFC 6238 says the verifier must reject a code it has already accepted: a code
stays valid for its whole thirty-second window, so one seen in flight — a
phishing page, a proxy, over the shoulder — could be replayed until it expired.

Tasks now record the counter of the last accepted step (totp_last_step), and a
login accepts a code only when its step is strictly greater. The guard is in the
UPDATE, not a read-then-write, so two logins racing with the same stolen code
cannot both find the old value and both proceed — exactly one moves the row.

auth.TOTPStep is VerifyTOTP that also reports which step matched, walking the
same one-step window; the code entered a little early or late spends the step it
was issued at, not the step it was checked at, so a late-but-valid code is still
single-use. Verified end to end: the same code presented to two logins in one
window is accepted once and refused the second time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 23:34:34 +02:00

12 lines
715 B
SQL

-- A TOTP code is meant to be usable once. RFC 6238 says so in as many words: the
-- verifier must reject a code that has already been accepted, or a code observed in
-- flight — over the shoulder, off a phishing page, out of a proxy — can be replayed
-- for the rest of its thirty-second window.
--
-- totp_last_step records the counter of the most recently accepted code (Unix time
-- divided by the period). A login accepts a code only when its step is strictly
-- greater, so the same code cannot be spent twice and time cannot run backwards.
-- Null means no code has been accepted yet, which is every account until its owner
-- next signs in with one.
ALTER TABLE users ADD COLUMN totp_last_step INTEGER;