Skip to content

fix(oauthserver): consume authorization code under a row lock so it stays single-use#2605

Open
kanywst wants to merge 1 commit into
supabase:masterfrom
kanywst:fix/oauthserver-code-single-use
Open

fix(oauthserver): consume authorization code under a row lock so it stays single-use#2605
kanywst wants to merge 1 commit into
supabase:masterfrom
kanywst:fix/oauthserver-code-single-use

Conversation

@kanywst

@kanywst kanywst commented Jul 1, 2026

Copy link
Copy Markdown

What kind of change does this PR introduce?

Bug fix

summary

The authorization_code grant can return more than one token for a single code when requests race, so the code is not single-use (RFC 6749 §4.1.2). handleAuthorizationCodeGrant looks up the code outside the transaction and without a lock (handlers.go:336), then issues the token and deletes the row inside the transaction (:409, :416), so concurrent requests with the same code all pass the lookup and PKCE check before any of them deletes it. The consent path already locks with FindOAuthServerAuthorizationByIDForUpdate (FOR UPDATE SKIP LOCKED); the token path does not.

Fix: add FindOAuthServerAuthorizationByCodeForUpdate and consume the code under that lock inside the transaction, matching the consent path. The loser sees no row and gets invalid_grant.

Severity is low: PKCE is required here, so a stolen code is not redeemable without the verifier; the effect is the lost single-use guarantee under concurrency.

Test plan: TestAuthorizationCodeSingleUseUnderConcurrency (needs the dev postgres, make docker-test) fires 8 concurrent POST /oauth/token with the same code and asserts one token comes back. Before the fix all 8 succeed (got 8); after, one succeeds and the rest get invalid_grant.

docker compose -f docker-compose-dev.yml up -d postgres && bash hack/migrate.sh postgres
go test ./internal/api/oauthserver/ -run TestOAuthClientHandler/TestAuthorizationCodeSingleUseUnderConcurrency -count=1 -v

…tays single-use

The authorization_code grant looks up the code outside the transaction and
without a lock, then issues the token and deletes the row inside the
transaction, so concurrent requests with the same code each mint a token.
Re-acquire and consume the code under FOR UPDATE SKIP LOCKED before issuing,
matching the consent path.
@kanywst kanywst requested a review from a team as a code owner July 1, 2026 15:49
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