fix: Consume authorization codes atomically, after fallible loads #77
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
rosa/vernier!77
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/atomic-code-redemption"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fixes #49
Fixes #61
Problem
redeem_authorization_codewas a non-atomic find, an in-memory binding check, and a separate unconditional DELETE whose affected-row count the Postgres adapter ignored. Two concurrent redemptions of the same code could both pass the checks before either DELETE landed, each minting an access token — bypassing ADR-0008's single-use guarantee (#49). The DELETE also preceded the user load, so a transient repository error (or missing user) after the delete permanently burned a valid code with nothing granted (#61).Changes
delete_authorization_codewithconsume_authorization_code, an atomic claim returning whether this caller removed the code; exactly one of any number of concurrent callers observestrue.DELETEand readsrows_affected(); the in-memory mock usesDashMap::remove(..).is_some(), mirroring the row count.redeem_authorization_codereorders so the fallible user load happens before the claim — a failure there leaves the code intact and redeemable — and maps a lost claim toAuthorizationError::InvalidGrant. Binding checks (client, redirect URI, PKCE, expiry, verifier) and their error behavior are unchanged.Tests
crates/infra/tests/authorization_code_repository_conformance.rsruns against both adapters: the first consume claims the code and removes the row, a second consume (and a consume of an unknown code) reports already-consumed.transient_user_lookup_failure_leaves_the_code_redeemable(service test): an injected transientfind_user_by_idfailure errors the redemption without consuming the code, and a retry succeeds.MemoryAppRepogained afail_next_user_lookupknob for this.authorization_code_is_single_usenow asserts the replay is specificallyInvalidGrant.The
cimise task passes.