fix(oidc): make authorization code redemption atomic (BUNYIP-199) #238

Merged
David merged 2 commits from fix/bunyip-199-atomic-code-redemption into main 2026-06-26 11:38:39 +02:00
Member

Authorization code redemption loaded the row, verified it, and marked it consumed in three separate round-trips with no locking, so two concurrent /token requests bearing the same code could both pass the consumed_at check and both mint tokens (double-spend / code-replay race).

Wrap load+consume in a SERIALIZABLE transaction with SELECT ... FOR UPDATE, exactly as rotate_refresh_token does. The row-level lock plus SERIALIZABLE isolation serialise concurrent redemptions: the loser blocks on the lock, then sees consumed_at set and loses the race. The final UPDATE is additionally guarded on consumed_at IS NULL with a zero-rows-affected check as defence-in-depth. On the lost race the token family minted from the same op_session is revoked (revoke_reason = 'reuse_detected'), mirroring refresh-token reuse detection, before returning invalid_grant.

#BUNYIP-199

Authorization code redemption loaded the row, verified it, and marked it consumed in three separate round-trips with no locking, so two concurrent /token requests bearing the same code could both pass the consumed_at check and both mint tokens (double-spend / code-replay race). Wrap load+consume in a SERIALIZABLE transaction with SELECT ... FOR UPDATE, exactly as rotate_refresh_token does. The row-level lock plus SERIALIZABLE isolation serialise concurrent redemptions: the loser blocks on the lock, then sees consumed_at set and loses the race. The final UPDATE is additionally guarded on consumed_at IS NULL with a zero-rows-affected check as defence-in-depth. On the lost race the token family minted from the same op_session is revoked (revoke_reason = 'reuse_detected'), mirroring refresh-token reuse detection, before returning invalid_grant. #BUNYIP-199
fix(oidc): make authorization code redemption atomic (BUNYIP-199)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 1m8s
Check / fmt + clippy + build + tests (pull_request) Successful in 32m31s
e4a9e3ec76
Authorization code redemption loaded the row, verified it, and marked it consumed in three separate round-trips with no locking, so two concurrent /token requests bearing the same code could both pass the consumed_at check and both mint tokens (double-spend / code-replay race).

Wrap load+consume in a SERIALIZABLE transaction with SELECT ... FOR UPDATE, exactly as rotate_refresh_token does. The row-level lock plus SERIALIZABLE isolation serialise concurrent redemptions: the loser blocks on the lock, then sees consumed_at set and loses the race. The final UPDATE is additionally guarded on consumed_at IS NULL with a zero-rows-affected check as defence-in-depth. On the lost race the token family minted from the same op_session is revoked (revoke_reason = 'reuse_detected'), mirroring refresh-token reuse detection, before returning invalid_grant.

#BUNYIP-199
Merge branch 'main' into fix/bunyip-199-atomic-code-redemption
Some checks failed
E2E / Playwright against deployment (pull_request) Failing after 57s
Check / fmt + clippy + build + tests (pull_request) Successful in 10m45s
Create release / Create release from merged PR (pull_request) Has been skipped
bb01200d4c
David merged commit 451856868a into main 2026-06-26 11:38:39 +02:00
David deleted branch fix/bunyip-199-atomic-code-redemption 2026-06-26 11:38:40 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!238
No description provided.