From 99f0e05187f842cf32b2321de10f2443339208e8 Mon Sep 17 00:00:00 2001 From: Isolator acm Date: Wed, 24 Jun 2026 17:08:20 +0200 Subject: [PATCH] cimd: tolerate unknown grant_types in client metadata (fixes claude.ai outage) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit claude.ai's CIMD client metadata document now publishes grant_types: ["authorization_code","refresh_token","urn:ietf:params:oauth:grant-type:jwt-bearer"]. The jwt-bearer entry hit the default branch of the grant_types validator and failed the whole CIMD resolution with "unsupported grant_type", taking every claude.ai connector down at /authorize (ChatGPT unaffected — its client.json lists no extra grants). The CIMD grant_types array advertises client capability, not what we issue: .well-known/oauth-authorization-server lists only authorization_code and /token rejects everything else, so tolerating unknown entries changes nothing about issued tokens. Require authorization_code to be present; ignore the rest. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01NTbw8cvu8efRe2XqcTSMvo --- cmd/altinity-mcp/cimd.go | 23 +++++++++++++---------- cmd/altinity-mcp/cimd_test.go | 7 +++++-- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/cmd/altinity-mcp/cimd.go b/cmd/altinity-mcp/cimd.go index b4f2599..f0d41ed 100644 --- a/cmd/altinity-mcp/cimd.go +++ b/cmd/altinity-mcp/cimd.go @@ -483,17 +483,20 @@ func parseCIMDMetadata(clientIDURL string, body []byte) (*statelessRegisteredCli switch gt { case "authorization_code": hasAuthCode = true - case "refresh_token": - // Tolerated in metadata, deliberately NOT honored in v1: a client - // publishing ["authorization_code","refresh_token"] (which - // claude.ai does today) silently gets no refresh capability — - // .well-known/oauth-authorization-server only advertises - // authorization_code and /token returns unsupported_grant_type - // for refresh. If/when refresh ships, do NOT treat the CIMD - // grant_types array as authoritative for what we issue — the AS - // metadata is the source of truth. default: - return nil, fmt.Errorf("%w: unsupported grant_type %q", errCIMDInvalidMetadata, gt) + // Any other grant_type (refresh_token, and as of 2026-06 claude.ai + // also publishes urn:ietf:params:oauth:grant-type:jwt-bearer) is + // TOLERATED but deliberately NOT honored in v1. The CIMD grant_types + // array is advertising what the *client* can do, not what we issue: + // .well-known/oauth-authorization-server only lists authorization_code + // and /token returns unsupported_grant_type for everything else, so a + // client gets exactly the capabilities the AS metadata declares + // regardless of what it lists here. Hard-rejecting unknown entries + // just breaks resolution whenever a client adds a new grant_type to + // its published doc (this is what took claude.ai connectors down when + // jwt-bearer was added). Requiring authorization_code to be present is + // the only real constraint. If refresh ever ships, the AS metadata — + // not this array — remains the source of truth for what we issue. } } if !hasAuthCode { diff --git a/cmd/altinity-mcp/cimd_test.go b/cmd/altinity-mcp/cimd_test.go index f6e9adc..e022312 100644 --- a/cmd/altinity-mcp/cimd_test.go +++ b/cmd/altinity-mcp/cimd_test.go @@ -160,7 +160,7 @@ func TestParseCIMDMetadata_OK(t *testing.T) { "client_name": "Claude", "client_uri": "https://claude.ai", "redirect_uris": ["https://claude.ai/api/mcp/auth_callback"], - "grant_types": ["authorization_code","refresh_token"], + "grant_types": ["authorization_code","refresh_token","urn:ietf:params:oauth:grant-type:jwt-bearer"], "response_types": ["code"], "token_endpoint_auth_method": "none" }`) @@ -183,7 +183,10 @@ func TestParseCIMDMetadata_Reject(t *testing.T) { "empty_redirect_uris": `{"client_id":"` + u + `","client_name":"X","redirect_uris":[],"token_endpoint_auth_method":"none"}`, "duplicate_redirect_uris": `{"client_id":"` + u + `","client_name":"X","redirect_uris":["https://x/cb","https://x/cb"],"token_endpoint_auth_method":"none"}`, "http_redirect_uri": `{"client_id":"` + u + `","client_name":"X","redirect_uris":["http://x/cb"],"token_endpoint_auth_method":"none"}`, - "unsupported_grant": `{"client_id":"` + u + `","client_name":"X","redirect_uris":["https://x/cb"],"token_endpoint_auth_method":"none","grant_types":["password"]}`, + // Unknown grant_types are now TOLERATED (claude.ai publishes jwt-bearer); + // this case still rejects, but because authorization_code is absent — not + // because "password" itself is unsupported. See parseCIMDMetadata. + "grant_types_without_authcode": `{"client_id":"` + u + `","client_name":"X","redirect_uris":["https://x/cb"],"token_endpoint_auth_method":"none","grant_types":["password"]}`, "unsupported_response": `{"client_id":"` + u + `","client_name":"X","redirect_uris":["https://x/cb"],"token_endpoint_auth_method":"none","response_types":["token"]}`, "empty_name": `{"client_id":"` + u + `","client_name":"","redirect_uris":["https://x/cb"],"token_endpoint_auth_method":"none"}`, "oversize_name": `{"client_id":"` + u + `","client_name":"` + strings.Repeat("a", cimdMaxClientNameLength+1) + `","redirect_uris":["https://x/cb"],"token_endpoint_auth_method":"none"}`,