Skip to content

fix: enable coalesceRefreshTokenExchange to stop invalid_grant storms - #136

Closed
mattzcarey wants to merge 1 commit into
mainfrom
fix/token-refresh-invalid-grant
Closed

fix: enable coalesceRefreshTokenExchange to stop invalid_grant storms#136
mattzcarey wants to merge 1 commit into
mainfrom
fix/token-refresh-invalid-grant

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Problem

We see a steady stream of:

Token refresh failed: 400 {"error":"invalid_grant","error_description":"The provided authorization grant ... refresh token is invalid, expired, revoked ..."}

Our tokenExchangeCallback redeems a single-use, rotating upstream Cloudflare refresh token, so the downstream refresh_token grant is non-idempotent. Two conditions trigger the errors:

  1. Shared token / concurrent refresh — a refresh token used by two sessions (e.g. two chat windows) gets refreshed simultaneously. Both decrypt the same upstream credentials and both call the upstream token endpoint, racing to redeem the same single-use upstream token. One wins; the rest get invalid_grant.
  2. Lost response / retry — a client retries with its previous refresh token; the callback re-runs against the already-rotated upstream token, redeeming it again and cascading further rotations.

Fix

Enable the new opt-in coalesceRefreshTokenExchange option in @cloudflare/workers-oauth-provider:

  • Single-flight — concurrent refresh_token requests for the same token within an isolate are coalesced; the callback (and upstream redemption) runs once and all callers share the response.
  • Idempotent replay — a refresh presented with the grant's previous token is treated as a retry of the rotation that already happened: the callback is skipped and a fresh access token is minted from current props without re-hitting upstream.

Provider dependency

Pinned to the pkg.pr.new preview build of cloudflare/workers-oauth-provider#216 (the PR that adds this option) for a production soak:

"@cloudflare/workers-oauth-provider": "https://pkg.pr.new/cloudflare/workers-oauth-provider/@cloudflare/workers-oauth-provider@216"

This will be switched back to a normal semver range once #216 ships.

Rollback

Revert this PR (or just the dependency pin) to return to ^0.6.0 with default behaviour. The option is opt-in and changes nothing else.

Checks

  • npm run typecheck
  • npm test ✅ 183/183
  • npm run format:check

Our tokenExchangeCallback redeems a single-use, rotating upstream Cloudflare
refresh token, so the refresh_token grant is non-idempotent. When a refresh
token is shared across sessions (e.g. two chats) or a client retries a lost
response, concurrent/duplicate refreshes race to redeem the same upstream
token — one wins, the rest get 'Token refresh failed: 400 invalid_grant', and
retries with the previous token re-run the callback and cascade more rotations.

Enable the new opt-in coalesceRefreshTokenExchange option in workers-oauth-
provider, which single-flights concurrent same-token refreshes within an
isolate and replays previous-token retries without re-hitting upstream.

Pinned to the pkg.pr.new preview build of cloudflare/workers-oauth-provider#216
for a production soak. Revert this commit (or the dependency pin) to roll back.

Made-with: pi
@mattzcarey mattzcarey closed this Jun 2, 2026
danielbodnar pushed a commit to danielbodnar/mcp that referenced this pull request Jun 10, 2026
* feat: add OAuth protected resource metadata (RFC 9728)

Add resourceMetadata config to OAuthProvider for the
/.well-known/oauth-protected-resource endpoint. Bump
workers-oauth-provider to PR cloudflare#136 build which implements
the endpoint.

* chore: upgrade workers-oauth-provider to PR 143 preview

Includes both RFC 9728 changes:
- /.well-known/oauth-protected-resource endpoint (cloudflare#136)
- resource_metadata in WWW-Authenticate headers (cloudflare#143)

* chore: bump workers-oauth-provider to v0.2.4 (released)

Replace pkg.pr.new preview build with the released v0.2.4 which
includes RFC 9728 support (cloudflare#136, cloudflare#143), RFC 8252 loopback port
flexibility for native apps (cloudflare#145), and other fixes.
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