fix: revoke the MCP OAuth refresh grant when its access token is deleted

Draft
HU
1d ago
+213-23

Summary

Revoking an MCP OAuth connection by deleting its access token now ends that connection. Before this change, the refresh grant issued with the token outlived the delete and kept minting new access tokens. The grant is revoked inside the database, on the delete itself. Every path that deletes a token therefore revokes the paired grant: a user deleting their own token, user disable or delete, removal from a workspace, workspace archive or delete, offboarding and SCIM.

Changes

  • The token delete trigger (notify_token_invalidation) now handles an unexpired mcp-oauth-* row by marking the mcp_oauth_refresh_token row issued with it as revoked. The match is on access_token_hash, so the same user's other MCP clients are untouched. Every deleted mcp-oauth-* row also emits the cross-instance auth-cache invalidation.
  • An expired access token keeps its grant: the expiry sweep deletes those rows, and their clients still have to be able to refresh.
  • The refresh grant locks the paired access-token row before it claims its own row. That is the order a delete takes them in (token row, then the trigger's grant update), so a delete racing a refresh waits instead of deadlocking.
  • delete_token also evicts the token from the local auth cache right away.
  • revoke_mcp_oauth_grants no longer sends its own notify, since the trigger now does.
  • New index on mcp_oauth_refresh_token(access_token_hash).
  • docs/auth-surface.md updated.

Known limit, unchanged by this PR: a client that has been idle longer than its access token's 7-day lifetime has no token row left to delete. Its grant is ended only by revoke_mcp_oauth_grants (a password change).

Test plan

  • mcp_oauth_token_delete integration test: deleting one client's token makes its refresh fail with invalid_grant. Another client of the same user still refreshes after its expired token is swept. The test fails without the trigger change.
  • Live, on a dev instance through DCR, PKCE, approve, token and refresh: the deleted token gets 401 immediately, its refresh gets invalid_grant, and a second client still refreshes. Disabling a user revokes their grant. A refresh issued while a token delete was still open waited for it and then got invalid_grant, with no deadlock.
  • mcp_superadmin_token (rotation path) still passes.

šŸ¤– Generated with Claude Code


Summary by cubic

Deleting an MCP OAuth access token now revokes its paired refresh grant, so the connection actually ends instead of the grant surviving the delete and minting fresh access tokens.

  • Revocation happens inside the database: the token delete trigger marks the mcp_oauth_refresh_token row issued with the deleted token as revoked, matched on access_token_hash so the user's other MCP clients are untouched.
  • This covers every delete path: user deleting their own token, user disable or delete, workspace removal, workspace archive or delete, offboarding, and SCIM.
  • Expired access tokens keep their grants — the expiry sweep deletes them and clients must still be able to refresh.
  • The refresh grant locks the paired access-token row before claiming its own, matching the delete's lock order to avoid deadlocks.
  • delete_token now evicts the deleted token from the local auth cache immediately; revoke_mcp_oauth_grants no longer sends its own invalidation since the trigger handles it.

Known limit: a client idle longer than its access token's 7-day lifetime has no token row left to delete; its grant ends only via revoke_mcp_oauth_grants (a password change).