Skip to content

Make authorization code and password reset token consumption atomic#78

Merged
eswan18 merged 1 commit into
mainfrom
fix/atomic-token-consumption
Apr 17, 2026
Merged

Make authorization code and password reset token consumption atomic#78
eswan18 merged 1 commit into
mainfrom
fix/atomic-token-consumption

Conversation

@eswan18
Copy link
Copy Markdown
Owner

@eswan18 eswan18 commented Apr 17, 2026

Summary

Two endpoints had a TOCTOU where the "already used?" check and the mark-used write were separate statements. Concurrent requests with the same code/token could both pass the check and both proceed — minting two token sets from one auth code, or resetting a password twice from one reset token.

Fixes:

  • ConsumeAuthorizationCode and MarkPasswordResetTokenUsed now include AND consumed_at IS NULL / AND used_at IS NULL in their WHERE clauses and return the row count (:execrows).
  • Handlers check rowsAffected == 1 before issuing tokens / changing the password. 0 rows ⇒ concurrent request won ⇒ refuse.
  • Reorder HandleResetPasswordPost: mark the token used before updating the password. Previously the order was reversed with an explicit "log but don't fail" on the mark-used write, meaning a transient DB error after a successful password update would leave the token valid for replay.

Auth code fix matches RFC 6749 §10.5 (codes MUST be usable at most once).

Test plan

  • go test ./... passes
  • New TestAuthorizationCodeCannotBeReused — exchange code twice, second attempt returns 400 invalid_grant
  • New TestPasswordResetTokenCannotBeReused — reset twice with same token, second attempt returns 400; verifies DB password hash is still the first one (second attempt didn't overwrite)
  • After deploy, manually verify login / password reset still work end-to-end against staging

🤖 Generated with Claude Code

Both endpoints had a TOCTOU where the token/code validity check and the
"mark-used" write were separate statements. Two concurrent requests with
the same code could both pass the !consumed check and both succeed —
minting two token sets from one auth code, or resetting a password twice
from one reset token.

ConsumeAuthorizationCode and MarkPasswordResetTokenUsed now include the
"not already used" predicate in their WHERE clause and return the row
count. Handlers check rowsAffected == 1 before proceeding; 0 rows means
a concurrent request won, and we refuse to issue tokens / change the
password.

Also reorder HandleResetPasswordPost: mark the token used BEFORE updating
the password. Previously the order was reversed with an explicit "log but
don't fail" on the mark-used write — meaning a transient DB error after
a successful password update would leave the token valid for replay.
Failing the reset when the user has to request a new token is strictly
safer.

Auth code replay fix matches RFC 6749 §10.5, which requires that an
authorization code be usable at most once.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@eswan18 eswan18 merged commit ef96841 into main Apr 17, 2026
1 check passed
@eswan18 eswan18 deleted the fix/atomic-token-consumption branch April 17, 2026 21:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant