mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user