domain: authorization code redemption is non-atomic, concurrent redeems bypass single-use #49
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
rosa/vernier#49
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Found during a code review of the
domaincrate.Location:
crates/domain/src/services.rs:824(redeem_authorization_code)Severity: Security — ADR-0008 single-use bypass
Problem
redeem_authorization_codeperforms a non-atomic find then in-memoryredeemthen delete, with no row lock, anddelete_authorization_codeignoresrows_affected. Two concurrent redemptions of the same code can both pass verification before either DELETE lands, and each mints an access token.Failure scenario
An attacker who intercepts a code (or a client that double-submits) fires two
POST /tokenrequests concurrently. Both passfind_authorization_code_by_idandrecord.redeem, then both DELETE (idempotent), and two bearer tokens are issued from one single-use code.Suggested fix
Make consumption atomic: a conditional DELETE ... RETURNING (or UPDATE ... WHERE not-yet-redeemed) that the first redemption wins, and treat a zero-row result as invalid_grant.
Verified during triage by code inspection: redemption is a load, an in-memory check, and a separate unconditional DELETE whose affected-row count is ignored — nothing serializes two concurrent redemptions of the same code. This brief also covers issue #61: the DELETE precedes the user load, so a transient failure after the consume burns the code with nothing granted. Both defects live in the same function and the same fix must resolve both orderings, so they share this brief.
Agent Brief
Category: bug
Summary: Make authorization-code consumption atomic (single-use under concurrency, per ADR-0008) and consume the code only when a grant is actually issued (covers issues #49 and #61)
Current behavior:
redeem_authorization_codeloads theAuthorizationCodeRecord, runs the binding checks (AuthorizationCodeRecord::redeem— client, redirect URI, PKCE, expiry, verifier), deletes the code through the repository, then loads the user and returns the grant. Two defects:invalid_grantand must restart the whole authorization flow.Desired behavior:
Consuming a code is a single atomic claim that exactly one caller can win; every concurrent or subsequent attempt fails with
AuthorizationError::InvalidGrant. A code is consumed only once nothing fallible stands between the claim and returning the grant: a failure loading the user (or any other transient error before the claim) leaves the code intact and redeemable. The binding checks and their error behavior are unchanged.Key interfaces:
DELETE … RETURNING-style operation, with the zero-row case mapped toInvalidGrantin the service.redeem_authorization_codereorders so fallible loads (the user) happen before the claim.Acceptance criteria:
InvalidGrant(the claim reports zero rows; no second token is minted).InvalidGrant.cimise task passes.Out of scope: