feat: revoke MCP server OAuth grants at the provider on disconnect (#27300)

Closes
[CODAGT-805](https://linear.app/codercom/issue/CODAGT-805/revoke-oauth-grants-at-the-source-for-mcp-servers).

The experimental MCP server OAuth2 disconnect endpoint previously
deleted only the stored token row, leaving the grant active at the OAuth
provider. This PR adds provider-side token revocation while keeping
local disconnect independent of provider availability.

## Changes

- Add `mcp_server_configs.oauth2_revocation_url` in migration `000547`.
The value can be configured manually, discovered from RFC 8414 metadata,
and managed through the MCP server settings UI. Non-admin responses
redact it with the other OAuth2 fields.
- Revoke the refresh token first through the RFC 7009 endpoint, then
fall back to the access token only for `unsupported_token_type`. Public
clients send `client_id`; confidential clients use
`client_secret_basic`.
- Delete the local token transactionally before best-effort provider
revocation. Callers without a token receive the same response for hidden
and nonexistent config IDs, and provider failures return a generic
warning without exposing provider response bodies.
- Require HTTPS revocation endpoints except for HTTP loopback URLs.
Redirects must preserve the POST and remain on the configured origin.
Redirect errors omit provider-controlled paths and query strings so
reflected token material cannot enter logs.
- Treat `200 OK` and `204 No Content` as completed revocations. `202
Accepted` remains a failure because it does not confirm completion.
- Prevent an in-flight refresh from recreating a token deleted by
disconnect. Refresh persistence now uses an optimistic update keyed by
token ID and `updated_at`; only the OAuth callback can create a token
row. Refresh conflicts reload the current row or clear in-memory auth
when disconnect deleted it.
- Return `{token_revoked, token_revocation_error}` from disconnect,
while retaining SDK compatibility with the legacy `204` response. The UI
surfaces provider revocation failures as warning toasts.
- Document revocation endpoint discovery, HTTPS requirements, and
best-effort disconnect behavior.

No token or no configured revocation URL returns `token_revoked: false`
without an error, so disconnect remains idempotent.

> Updated by Mux, an AI coding agent, on Mike's behalf.
This commit is contained in:
Michael Suchacz
2026-07-20 00:14:03 +02:00
committed by GitHub
parent 33fcc9de68
commit 9f4ddea571
31 changed files with 1810 additions and 200 deletions
@@ -67,6 +67,7 @@ INSERT INTO mcp_server_configs (
oauth2_client_secret_key_id,
oauth2_auth_url,
oauth2_token_url,
oauth2_revocation_url,
oauth2_scopes,
api_key_header,
api_key_value,
@@ -95,6 +96,7 @@ INSERT INTO mcp_server_configs (
sqlc.narg('oauth2_client_secret_key_id')::text,
@oauth2_auth_url::text,
@oauth2_token_url::text,
@oauth2_revocation_url::text,
@oauth2_scopes::text,
@api_key_header::text,
@api_key_value::text,
@@ -130,6 +132,7 @@ SET
oauth2_client_secret_key_id = sqlc.narg('oauth2_client_secret_key_id')::text,
oauth2_auth_url = @oauth2_auth_url::text,
oauth2_token_url = @oauth2_token_url::text,
oauth2_revocation_url = @oauth2_revocation_url::text,
oauth2_scopes = @oauth2_scopes::text,
api_key_header = @api_key_header::text,
api_key_value = @api_key_value::text,
@@ -207,6 +210,25 @@ ON CONFLICT (mcp_server_config_id, user_id) DO UPDATE SET
RETURNING
*;
-- name: UpdateMCPServerUserTokenFromRefresh :one
-- Refresh persistence must not recreate a token deleted by disconnect.
-- The optimistic lock also prevents stale refreshes from replacing newer tokens.
UPDATE mcp_server_user_tokens
SET
access_token = @access_token::text,
access_token_key_id = sqlc.narg('access_token_key_id')::text,
refresh_token = @refresh_token::text,
refresh_token_key_id = sqlc.narg('refresh_token_key_id')::text,
token_type = @token_type::text,
expiry = sqlc.narg('expiry')::timestamptz,
oauth_refresh_failure_reason = '',
updated_at = NOW()
WHERE
id = @id::uuid
AND updated_at = @updated_at::timestamptz
RETURNING
*;
-- name: MarkMCPServerUserTokenRefreshFailure :one
-- Records a permanent refresh failure (e.g. revoked grant) and clears
-- the dead token material so it is never attached to a request again.