feat(cloudflare): Add hosted OAuth CIMD support#1104
Conversation
a60bd64 to
86db067
Compare
06becca to
d8268ac
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d8268ac. Configure here.
d8268ac to
e03a7f6
Compare
There was a problem hiding this comment.
isCimdClientCompatible rejects valid CIMD clients that omit grant_types or response_types (packages/mcp-cloudflare/src/server/oauth/routes/authorize.ts:99)
Using === true on optional-chained arrays means a CIMD document that legitimately omits grant_types or response_types (both default per RFC 7591 §2) evaluates to undefined === true and is rejected as incompatible.
Evidence
client.grantTypes?.includes("authorization_code") === truereturnsfalsewhengrantTypesisundefined, not just when the array lacks the value.ClientInfouses optional fields (grantTypes?,responseTypes?), soundefinedis a reachable value.- RFC 7591 §2 states that if
grant_typesis omitted it defaults to["authorization_code"], andresponse_typesdefaults to["code"]; both omitted forms are valid CIMD documents. - The GET handler also calls
isCimdClientCompatible(line ~280) and would reject such clients before displaying the consent dialog. - No test covers a CIMD client where
grantTypesorresponseTypesis absent from theClientInfoobject.
Demo chat OAuth client (public client) omits PKCE in authorization code flow (packages/mcp-cloudflare/src/server/routes/chat-oauth.ts:251)
The hosted demo chat acts as a public OAuth client to the MCP server (token_endpoint_auth_method: "none", no client_secret at the token endpoint), but it never sends code_challenge/code_challenge_method on the authorization request or code_verifier on the token exchange. OAuth 2.1 and the MCP authorization spec mandate PKCE for public authorization-code clients. Impact is limited here because the flow is first-party (client and MCP server in the same deployment), the code is exchanged server-side in the worker /callback handler, and a state cookie provides CSRF protection — but the spec deviation remains and the authorization server already advertises code_challenge_methods_supported.
Evidence
/authorizehandler builds the authorization URL at chat-oauth.ts:251-256 settingclient_id,redirect_uri,response_type,scope,state, andresource, but nocode_challenge/code_challenge_method.exchangeCodeForTokenbuilds the token body at chat-oauth.ts:196-201 withgrant_type,client_id,code,redirect_uri,resource, but nocode_verifier.- The client is registered/declared with
token_endpoint_auth_method: "none"(getChatClientMetadata and DCR registrationData), i.e. a public client, and noclient_secretis ever sent. authorization-server-metadata.ts:63advertisescode_challenge_methods_supported: ["plain", "S256"], so the server supports PKCE while this client opts out.- Mitigating context: redirect_uri is the worker's own
/api/auth/callback, code exchange runs server-side, andstateis validated against a secure cookie in the/callbackhandler.
Identified by Warden mcp-audit
Enable Client ID Metadata Documents for hosted Cloudflare OAuth while preserving Dynamic Client Registration fallback and path-scoped MCP resource behavior. Advertise CIMD support from authorization-server metadata, validate URL-client metadata before consent and upstream redirect, and show URL client identity in the consent UI. Fixes GH-1102 Co-authored-by: GPT-5 Codex <codex@openai.com>
e03a7f6 to
ab6cd82
Compare
| mockOAuthProvider.lookupClient.mockResolvedValueOnce({ | ||
| clientId, | ||
| clientName: "Example CIMD Client", | ||
| redirectUris: ["https://example.com/callback"], | ||
| grantTypes: ["authorization_code"], |
There was a problem hiding this comment.
isCimdClientCompatible rejects CIMD clients that omit grant_types, ignoring RFC 7591 default
In isCimdClientCompatible (packages/mcp-cloudflare/src/server/oauth/routes/authorize.ts), the check client.grantTypes?.includes("authorization_code") === true evaluates to false when a CIMD client's grantTypes is undefined. RFC 7591 §2 defines the default grant_types as ["authorization_code"] when the field is absent. A conformant CIMD client whose metadata document omits grant_types is therefore rejected with Invalid client (400) at both authorize entry points (lines 225 and 333), even though it should be treated as an authorization_code client. This is a fail-closed interoperability gap for a draft/SEP-tracked CIMD feature rather than a security defect.
Evidence
isCimdClientCompatibleatauthorize.ts:58-62returns false unlessclient.grantTypes?.includes("authorization_code") === true; whengrantTypesisundefinedthis isundefined === true→false.- Both call sites (
authorize.ts:225andauthorize.ts:333) returnc.text("Invalid client", 400)when the check fails, so an omittedgrant_typesblocks the flow. - RFC 7591 §2 specifies
authorization_codeas the default whengrant_typesis absent, so this rejects conformant CIMD documents. - The demo client helper
getChatClientMetadataalways emits explicitgrant_types, so the gap only affects third-party CIMD documents that rely on the default.
Identified by Warden mcp-audit · PP2-ETN
| grant_types: ["authorization_code", "refresh_token"], | ||
| response_types: ["code"], | ||
| token_endpoint_auth_method: "none", | ||
| scope: Object.keys(SCOPES).join(" "), |
There was a problem hiding this comment.
Demo chat OAuth client (BFF) declared as public client omits PKCE
The new getChatClientMetadata CIMD document declares token_endpoint_auth_method: "none" (a public OAuth 2.1 client), and the MCP authorization profile requires public authorization-code clients to use PKCE. The demo chat's own OAuth flow never sends PKCE: the /api/auth/authorize handler in chat-oauth.ts builds its authUrl with only client_id, redirect_uri, response_type, scope, state, and resource (no code_challenge/code_challenge_method), and exchangeCodeForToken posts no code_verifier. The server's authorization-server metadata advertises code_challenge_methods_supported: ["plain", "S256"], so PKCE is supported but the dogfooded client does not use it. Impact is limited because the chat is a server-side BFF: the authorization code is exchanged server-to-server and the OAuth state is bound to an httpOnly signed cookie (chat_oauth_state) for CSRF protection, and AS-side PKCE enforcement is owned by the upstream @cloudflare/workers-oauth-provider library. The residual gap is that the authorization code transits the browser redirect to /api/auth/callback without a PKCE binding on a client the server treats as public.
Evidence
getChatClientMetadata(chat-oauth.ts ~L87-100) declarestoken_endpoint_auth_method: "none", marking the demo-chat as a public client with no client secret.- The
.get("/authorize")handler (chat-oauth.ts ~L227-262) constructsauthUrlsetting onlyclient_id,redirect_uri,response_type,scope,state,resource— nocode_challenge;exchangeCodeForToken(~L186-215) sends nocode_verifier. createScopedAuthorizationServerMetadataResponse(authorization-server-metadata.ts L63) advertisescode_challenge_methods_supported, so the AS supports PKCE that this client never exercises.- Mitigations: the flow is a server-side BFF (chat.ts connects to
/mcpserver-side with the cookie-stored bearer token), andchat_oauth_stateis an httpOnly signed cookie validated in.get("/callback"), providing CSRF protection; AS-side PKCE enforcement lives in the inherited@cloudflare/workers-oauth-provider.
Identified by Warden mcp-audit · UDA-JDX

Hosted Cloudflare OAuth now supports Client ID Metadata Documents for MCP clients that use an HTTPS metadata URL as their OAuth client ID. The PR enables the Cloudflare provider option, advertises CIMD support from root and scoped authorization-server metadata, keeps Dynamic Client Registration available at
/oauth/register, and validates URL-client metadata before consent and upstream redirect.The hosted demo chat now dogfoods CIMD on HTTPS through
/.well-known/oauth-client/demo-chat.json. That document identifies only the web chat client, uses the deployment root asclient_uri, allows only/api/auth/callbackas a redirect, and keeps/mcpas the separate protected resource requested through RFC 8707resource. Local HTTP chat auth still uses DCR sopnpm run devremains simple.Local Cloudflare dev now declares required Worker secrets and seeds only
COOKIE_SECRETwith avite serve-only default before the Cloudflare plugin loads bindings. That keeps production/canary configs free of hardcoded secret vars while preventing a missing local cookie secret from blocking the OAuth consent page; Sentry/OpenAI credentials still need to come from.env,.dev.vars, or the environment. The generated Worker binding types are regenerated from the updated Wrangler config.The repo-local test client also has optional CIMD QA mode through
--client-metadata-url/MCP_CLIENT_METADATA_URL, using that HTTPS URL directly asclient_idwhile keeping DCR as the default path. Its docs now call out that CIMD QA requires HTTP transport because auto stdio mode skips OAuth whenSENTRY_ACCESS_TOKENis set. The same OAuth path sends the RFC 8707resourceparameter during token exchange, matching the authorization request.Garfield review found and fixed a callback consistency issue: loopback redirect URI matching is now shared between authorization and callback validation, so RFC 8252-style dynamic loopback ports accepted at consent are not rejected on callback.
AGENTS.mdnow documents the package roles and OAuth/transport boundaries to reduce future confusion around stdio, hosted HTTP, OAuth, demo chat, and agent mode.The OpenSpec change is included with implementation tasks. Tests cover valid and invalid CIMD flows, DCR compatibility, scoped resource metadata, the strict-public fetch Wrangler flag, loopback redirect semantics, consent UI identity, test-client CIMD/token-resource behavior, hosted demo chat CIMD isolation, and local-dev secret wiring. Validation passed with
openspec validate add-cimd-hosted-oauth,pnpm --filter @sentry/mcp-cloudflare test -- src/server/wrangler-config.test.ts src/server/oauth/authorize.test.ts,pnpm --filter @sentry/mcp-cloudflare test -- src/server/index.test.ts src/server/routes/__tests__/chat-oauth.test.ts,pnpm --filter @sentry/mcp-cloudflare test -- src/server/oauth/callback.test.ts src/server/oauth/authorize.test.ts src/server/wrangler-config.test.ts,pnpm --filter @sentry/mcp-cloudflare tsc,pnpm --filter @sentry/mcp-test-client test -- src/auth/oauth.test.ts,pnpm --filter @sentry/mcp-test-client typecheck,pnpm run tsc,pnpm run lint,pnpm run docs:check,pnpm run test, andpnpm --filter @sentry/mcp-cloudflare cf-typegen.Fixes #1102