feat(client): revoke OAuth tokens on Disconnect (RFC 7009)#1308
Open
jblz wants to merge 2 commits into
Open
Conversation
The Disconnect button wipes local OAuth state but never tells the authorization server. Tokens stay valid until natural expiry, leaving "tombstone" sessions for downstream services to clean up. Add a best-effort `revokeTokens` helper that POSTs to the AS's `revocation_endpoint` (RFC 8414) on disconnect, preferring the refresh token so the AS cascade-invalidates associated access tokens per RFC 7009 §2.1. A 3s timeout and swallowed errors keep an unresponsive AS from blocking the UI; if the AS doesn't advertise a `revocation_endpoint`, the helper no-ops. Routes through `createProxyFetch` when `connectionType === "proxy"`, matching how every other OAuth call in `useConnection` is made. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Inspector is a testing tool, so users may legitimately want to exercise the inverse scenario: how does an authorization server behave when a client disconnects without revoking? Mirror the existing pattern used for MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS — boolean ConfigItem, default to the spec-compliant value (`true`), expose via the Settings panel (auto-rendered for any ConfigItem). The local clear() still runs when revocation is disabled, so users still get a clean slate in the Inspector even when opting out of the remote revoke. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.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
revocation_endpoint(discovered via RFC 8414) before wiping local OAuth state. Prefers the refresh token so the AS cascade-invalidates associated access tokens per RFC 7009 §2.1; falls back to the access token if no refresh token is held.revocation_endpointadvertised → silent no-op; network error or non-2xx → swallowed with aconsole.warn; 3 sAbortSignal.timeoutso a slow AS can't hang the UI. Disconnect always completes.MCP_OAUTH_REVOKE_ON_DISCONNECTconfig toggle (defaulttrue, mirrors the existingMCP_REQUEST_TIMEOUT_RESET_ON_PROGRESSpattern). Off is useful when testing how a server behaves with un-revoked tokens after a client walks away — Inspector's whole point being to poke at server behavior.Why
Currently the Disconnect button clears local session storage but never tells the authorization server, so every disconnected session leaves a still-valid access token (and refresh token, if issued) sitting on the AS until natural expiry. Server-side this manifests as "tombstone" sessions for downstream services to clean up. RFC 7009 §2 explicitly says clients SHOULD revoke tokens when they're no longer needed; the Inspector's Disconnect is exactly that moment.
This builds on #280 (local-clear-on-disconnect) by adding the missing remote-revocation step in front of the existing local clear. It's adjacent to but distinct from #1046 (OIDC
end_session_endpoint) — that issue's author lays out the three-layer model cleanly:This PR addresses (2) only.
Behavior change
revocation_endpoint→ no behavior change (silent no-op). Any AS that doesn't implement RFC 7009 will continue to work exactly as before.revocation_endpoint→ on Disconnect, aPOST <revocation_endpoint>withapplication/x-www-form-urlencodedbody containingtoken=<refresh|access>,token_type_hint, and (when available)client_id. Routed throughcreateProxyFetchwhenconnectionType === "proxy", matching every other OAuth call inuseConnection.MCP_OAUTH_REVOKE_ON_DISCONNECTsetting (Settings panel → "Revoke OAuth Tokens on Disconnect") toggles the call. Local clear still runs either way, so the user still gets a clean slate in the Inspector even with revocation disabled.Files changed
client/src/lib/auth.ts— new module-levelrevokeTokens({ serverUrl, fetchFn })helper. Module-level (not a method) keeps it decoupled fromInspectorOAuthClientProvider.clear(), which is also reached from auth-error paths where calling/revokewould be redundant or even error.client/src/lib/hooks/useConnection.ts—disconnect()awaitsrevokeTokensbefore the existingauthProvider.clear().client/src/lib/configurationTypes.ts,client/src/lib/constants.ts,client/src/utils/configUtils.ts— newMCP_OAUTH_REVOKE_ON_DISCONNECTConfigItem + getter.client/src/lib/__tests__/auth.test.ts, 2 new inclient/src/lib/hooks/__tests__/useConnection.test.tsx.Test plan
npm test— 525/525 passnpm run lint— cleannpm run build— cleanPOST /revokein the AS access logs returning 200, confirm the token is no longer accepted on a follow-up request, confirm local session storage is cleared.revocation_endpoint: confirm Disconnect still completes promptly, session storage clears, no error in the console.MCP_OAUTH_REVOKE_ON_DISCONNECTto False in Settings → click Disconnect → confirm no revocation request is sent, local clear still happens.Out of scope
beforeunload— no graceful await available there; intentional Disconnect is the right hook.🤖 Generated with Claude Code