You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Authorization-code replay race (TOCTOU) in the authorization_code grant at POST /oauth/token.
handleAuthorizationCodeGrant reads and validates the code (expiry / client / redirect_uri / PKCE) with no row lock, and only opens a transaction later to issue tokens and Destroy the code. Two concurrent requests with the same code both pass validation before either commits, violating RFC 6749 §4.1.2.
To Reproduce
Mint one approved authorization code (public client + PKCE code_verifier).
Fire N concurrent POST /oauth/token exchanges of that same code behind a start barrier.
Observe ≥2 responses carry access_token.
Expected behavior
Exactly one exchange succeeds (200 + access_token); the rest return 400 invalid_grant. The code is single-use.
Describe the bug
Authorization-code replay race (TOCTOU) in the
authorization_codegrant atPOST /oauth/token.handleAuthorizationCodeGrantreads and validates the code (expiry / client / redirect_uri / PKCE) with no row lock, and only opens a transaction later to issue tokens andDestroythe code. Two concurrent requests with the same code both pass validation before either commits, violating RFC 6749 §4.1.2.To Reproduce
code_verifier).POST /oauth/tokenexchanges of that same code behind a start barrier.access_token.Expected behavior
Exactly one exchange succeeds (
200+access_token); the rest return400 invalid_grant. The code is single-use.Fix
#2612
FindOAuthServerAuthorizationByIDForUpdate(FOR UPDATE SKIP LOCKED, from fix(oauth-server): serialize concurrent authorize/consent with row-level lock #2512). Concurrent redemptions serialize — the loser skips the locked row and getsinvalid_grant*apierrors.OAuthErrorout of the transaction soinvalid_grantreaches the client instead of a 500.handlers_replay_test.go: N concurrent redemptions assert single-use (success == 1). Passes with the fix; without the lock, all N succeed.