Skip to content

feat(client): revoke OAuth tokens on Disconnect (RFC 7009)#1308

Open
jblz wants to merge 2 commits into
modelcontextprotocol:mainfrom
jblz:rfc7009-revoke
Open

feat(client): revoke OAuth tokens on Disconnect (RFC 7009)#1308
jblz wants to merge 2 commits into
modelcontextprotocol:mainfrom
jblz:rfc7009-revoke

Conversation

@jblz
Copy link
Copy Markdown

@jblz jblz commented May 15, 2026

Summary

  • On Disconnect, send an RFC 7009 token revocation request to the AS's 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.
  • Best-effort: no revocation_endpoint advertised → silent no-op; network error or non-2xx → swallowed with a console.warn; 3 s AbortSignal.timeout so a slow AS can't hang the UI. Disconnect always completes.
  • Adds an MCP_OAUTH_REVOKE_ON_DISCONNECT config toggle (default true, mirrors the existing MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS pattern). 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:

  1. local clear (already implemented, refined by fix: Disconnecting should clear oauth state #280)
  2. AS token revocation (this PR)
  3. IdP session logout (Feature request: Support OIDC end_session_endpoint #1046, future)

This PR addresses (2) only.

Behavior change

  • No advertised revocation_endpoint → no behavior change (silent no-op). Any AS that doesn't implement RFC 7009 will continue to work exactly as before.
  • AS advertises revocation_endpoint → on Disconnect, a POST <revocation_endpoint> with application/x-www-form-urlencoded body containing token=<refresh|access>, token_type_hint, and (when available) client_id. Routed through createProxyFetch when connectionType === "proxy", matching every other OAuth call in useConnection.
  • Opt-out: the new MCP_OAUTH_REVOKE_ON_DISCONNECT setting (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-level revokeTokens({ serverUrl, fetchFn }) helper. Module-level (not a method) keeps it decoupled from InspectorOAuthClientProvider.clear(), which is also reached from auth-error paths where calling /revoke would be redundant or even error.
  • client/src/lib/hooks/useConnection.tsdisconnect() awaits revokeTokens before the existing authProvider.clear().
  • client/src/lib/configurationTypes.ts, client/src/lib/constants.ts, client/src/utils/configUtils.ts — new MCP_OAUTH_REVOKE_ON_DISCONNECT ConfigItem + getter.
  • Tests: 8 new in client/src/lib/__tests__/auth.test.ts, 2 new in client/src/lib/hooks/__tests__/useConnection.test.tsx.

Test plan

  • npm test — 525/525 pass
  • npm run lint — clean
  • npm run build — clean
  • Manual E2E against a real OAuth-protected MCP server: connect → complete OAuth → click Disconnect → confirm POST /revoke in the AS access logs returning 200, confirm the token is no longer accepted on a follow-up request, confirm local session storage is cleared.
  • Manual E2E against an AS that does not advertise revocation_endpoint: confirm Disconnect still completes promptly, session storage clears, no error in the console.
  • Manual: flip MCP_OAUTH_REVOKE_ON_DISCONNECT to False in Settings → click Disconnect → confirm no revocation request is sent, local clear still happens.

Out of scope

  • OIDC RP-Initiated Logout (Feature request: Support OIDC end_session_endpoint #1046) — different lifecycle (IdP session, not OAuth token). Could be a follow-up that adds a third call alongside this one.
  • Revoking on browser-close / beforeunload — no graceful await available there; intentional Disconnect is the right hook.
  • Any AS-side fixes. RFC 7009 support is a SHOULD on the client side; whether your AS implements it is an AS concern.

🤖 Generated with Claude Code

jblz and others added 2 commits May 15, 2026 00:27
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>
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