feat: revoke existing grants on re-authorization by default#144
Merged
mattzcarey merged 9 commits intocloudflare:mainfrom Feb 27, 2026
Merged
Conversation
🦋 Changeset detectedLatest commit: 19bbf24 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
deathbyknowledge
approved these changes
Feb 26, 2026
Contributor
Author
|
gonna release this as a minor but the last one was massive so waiting a couple of days to test this better. |
When re-authorizing, clients may still hold old tokens pointing to old grants with outdated props. This can cause infinite re-auth loops when the app throws invalid_grant to force re-authentication. The new revokeExistingGrants option revokes all existing grants for the same user+client combination before creating the new grant, ensuring clients must use the new tokens with updated props. Fixes cloudflare#34
completeAuthorization() now revokes all existing grants for the same user+client combination after storing the new grant. This fixes infinite re-auth loops (issue cloudflare#34) where stale props in old grants cause clients to endlessly re-authorize. Changes from the original PR: - Default flipped: revokeExistingGrants defaults to true (opt-out via false) - Revocation happens AFTER new grant is stored (no data-loss window) - Pagination support for users with >1000 grants - Changeset bumped to minor (behavior change) - Added 5 reproduction tests covering the bug and the fix
Squashed from PR cloudflare#157 by @rc4. When an authorization code is reused, revoke all tokens issued from the first exchange as a precaution against replay attacks. Revocation is best-effort to ensure the invalid_grant response always returns. Includes test coverage.
db1dbaa to
dd0ec4b
Compare
…mpatibility The helper functions accepted OAuthProvider (defaulting to OAuthProvider<Env>) but tests create OAuthProvider<TestEnv>, causing type errors in CI.
Merged
mattzcarey
pushed a commit
that referenced
this pull request
Mar 4, 2026
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @cloudflare/workers-oauth-provider@0.3.0 ### Minor Changes - [#158](#158) [`b26f7ff`](b26f7ff) Thanks [@mattzcarey](https://github.com/mattzcarey)! - Add `clientIdMetadataDocumentEnabled` option to make CIMD (Client ID Metadata Document) support explicitly opt-in. Previously, CIMD auto-enabled when the `global_fetch_strictly_public` compatibility flag was present, which could cause crashes for servers where URL-shaped client_ids hit bot-protected endpoints. When not enabled (the default), URL-formatted client_ids now fall through to standard KV lookup instead of throwing. - [#144](#144) [`49a1d24`](49a1d24) Thanks [@mattzcarey](https://github.com/mattzcarey)! - Add `revokeExistingGrants` option to `completeAuthorization()` that revokes existing grants for the same user+client after creating a new one. Defaults to `true`, fixing infinite re-auth loops when props change between authorizations (issue #34). Set to `false` to allow multiple concurrent grants per user+client. Revoke tokens and grant when an authorization code is reused, per RFC 6749 §10.5. This prevents authorization code replay attacks by invalidating all tokens issued from the first exchange. **Breaking behavior change:** Previously, re-authorizing the same user+client created an additional grant, leaving old tokens valid. Now, old grants are revoked by default. If your application relies on multiple concurrent grants per user+client, set `revokeExistingGrants: false` to preserve the old behavior. ### Patch Changes - [#164](#164) [`4b640a3`](4b640a3) Thanks [@pnguyen-atlassian](https://github.com/pnguyen-atlassian)! - Include `client_secret_expires_at` and `client_secret_issued_at` in dynamic client registration responses when a `client_secret` is issued, per RFC 7591 §3.2.1. - [#165](#165) [`9cce070`](9cce070) Thanks [@mattzcarey](https://github.com/mattzcarey)! - Use `Promise.allSettled` instead of `Promise.all` for best-effort grant revocation in `completeAuthorization()`, ensuring all grants are attempted even if one fails. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two related grant-security improvements, shipped as one body of work:
Revoke existing grants on re-authorization — When a user goes through the OAuth flow again for the same client, all previous grants (and their tokens) for that user+client are now revoked after the new grant is stored. This fixes infinite re-auth loops caused by stale props in old grants (issue No provision to restart the authorization flow if the oauth server to the worker oauth client does not provide refresh tokens #34).
Revoke tokens on authorization code reuse (RFC 6749 §10.5) — When an authorization code that has already been exchanged is presented again, the provider now revokes the grant and all tokens from the original exchange. This prevents authorization code replay attacks. (Originally PR Revoke tokens on authorization code reuse #157 by @rc4, squashed into this PR.)
Breaking behavior change
completeAuthorization()now defaultsrevokeExistingGrantstotrue. Previously, re-authorizing the same user+client created an additional grant, leaving old tokens valid. Now, old grants are revoked by default. This is an edge case but it could be hit by some MCP clients.If your application relies on multiple concurrent grants per user+client, set
revokeExistingGrants: falseto preserve the old behavior:MCP servers should not need to preserve this so setting the default to true here but allowing opt out for developers who need it.
Changes
src/oauth-provider.tsrevokeExistingGrants?: booleanoption toCompleteAuthorizationOptions(defaults totrue)userId + clientId(with cursor-based pagination for >1000 grants)Promise.allauthCodeIdis absent on a grant, callsrevokeGrant()(best-effort) and returnsinvalid_grant__tests__/reauth-grant-revocation.test.ts(new, 5 tests)revokeExistingGrants: false(the bug)tokenExchangeCallbackthrowinginvalid_grant__tests__/oauth-provider.test.ts(1 new test)invalid_grant, all tokens and the grant are revoked.changeset/fix-revoke-existing-grants.mdRFC / spec alignment
replaceaction that does exactly whatrevokeExistingGrantsdoesTest plan
prettier --check)agentsSDK MCP serverFixes #34 | Includes #157