ASVS 5.0 V6.5.1 requires that lookup secrets (recovery codes), out of band authentication codes and time based one time passwords can each be used successfully only once. A second attempt with the same value must fail. It is a Level 2 requirement.
Why it matters
A one time code is only one time if the server remembers it was used. A TOTP is valid for a short window, so an attacker who sees it over a shoulder, on a phishing page or in a proxy can replay it within that window unless you reject reuse. Recovery codes that never get crossed off turn into permanent passwords.
How to implement it
Record consumption atomically. The check and the mark must be one operation, or two parallel requests will both succeed.
-- recovery codes: consume in a single statement
UPDATE recovery_codes
SET used_at = now()
WHERE user_id = $1 AND code_hash = $2 AND used_at IS NULL
RETURNING id;
-- zero rows returned means the code was invalid or already used
For TOTP, store the last accepted time step per user and reject anything at or before it.
def verify_totp(user, code):
step = totp.verify(user.seed, code, window=1) # returns matched time step or None
if step is None or step <= user.last_totp_step:
return False
updated = db.execute(
"UPDATE users SET last_totp_step=%s WHERE id=%s AND last_totp_step < %s",
(step, user.id, step))
return updated == 1
Out of band codes should be deleted or flagged on first successful use.
How to test it
- Log in with a valid TOTP, log out, and log in again with the same code inside its window. The second must fail.
- Use a recovery code twice. The second must fail.
- Fire two simultaneous requests with the same code and confirm only one succeeds.
Common mistakes
- Checking "not used" and then marking used in two steps.
- Tracking replay in per process memory, which breaks with several servers.
- Accepting the previous time step again after a success.
Quick check
Why do parallel requests matter?
If validation and marking are separate steps, two requests can both pass validation before either marks the code used.