Skip to content

feat(cloudflare): Add hosted OAuth CIMD support#1104

Draft
dcramer wants to merge 1 commit into
mainfrom
feat/hosted-oauth-cimd
Draft

feat(cloudflare): Add hosted OAuth CIMD support#1104
dcramer wants to merge 1 commit into
mainfrom
feat/hosted-oauth-cimd

Conversation

@dcramer

@dcramer dcramer commented Jun 17, 2026

Copy link
Copy Markdown
Member

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 as client_uri, allows only /api/auth/callback as a redirect, and keeps /mcp as the separate protected resource requested through RFC 8707 resource. Local HTTP chat auth still uses DCR so pnpm run dev remains simple.

Local Cloudflare dev now declares required Worker secrets and seeds only COOKIE_SECRET with a vite 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 as client_id while keeping DCR as the default path. Its docs now call out that CIMD QA requires HTTP transport because auto stdio mode skips OAuth when SENTRY_ACCESS_TOKEN is set. The same OAuth path sends the RFC 8707 resource parameter 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.md now 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, and pnpm --filter @sentry/mcp-cloudflare cf-typegen.

Fixes #1102

@dcramer dcramer force-pushed the feat/hosted-oauth-cimd branch from a60bd64 to 86db067 Compare June 17, 2026 21:08
Comment thread packages/mcp-cloudflare/src/server/oauth/routes/authorize.ts
Comment thread packages/mcp-cloudflare/src/server/oauth/routes/authorize.ts
@dcramer dcramer force-pushed the feat/hosted-oauth-cimd branch 2 times, most recently from 06becca to d8268ac Compare June 17, 2026 21:47

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ 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.

Comment thread packages/mcp-cloudflare/src/server/oauth/routes/callback.ts Outdated
@dcramer dcramer force-pushed the feat/hosted-oauth-cimd branch from d8268ac to e03a7f6 Compare June 17, 2026 22:06
Comment thread packages/mcp-cloudflare/src/server/oauth/routes/authorize.ts Outdated

@sentry-warden sentry-warden Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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") === true returns false when grantTypes is undefined, not just when the array lacks the value.
  • ClientInfo uses optional fields (grantTypes?, responseTypes?), so undefined is a reachable value.
  • RFC 7591 §2 states that if grant_types is omitted it defaults to ["authorization_code"], and response_types defaults 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 grantTypes or responseTypes is absent from the ClientInfo object.

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
  • /authorize handler builds the authorization URL at chat-oauth.ts:251-256 setting client_id, redirect_uri, response_type, scope, state, and resource, but no code_challenge/code_challenge_method.
  • exchangeCodeForToken builds the token body at chat-oauth.ts:196-201 with grant_type, client_id, code, redirect_uri, resource, but no code_verifier.
  • The client is registered/declared with token_endpoint_auth_method: "none" (getChatClientMetadata and DCR registrationData), i.e. a public client, and no client_secret is ever sent.
  • authorization-server-metadata.ts:63 advertises code_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, and state is validated against a secure cookie in the /callback handler.

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>
@dcramer dcramer force-pushed the feat/hosted-oauth-cimd branch from e03a7f6 to ab6cd82 Compare June 17, 2026 22:46
Comment on lines +929 to +933
mockOAuthProvider.lookupClient.mockResolvedValueOnce({
clientId,
clientName: "Example CIMD Client",
redirectUris: ["https://example.com/callback"],
grantTypes: ["authorization_code"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
  • isCimdClientCompatible at authorize.ts:58-62 returns false unless client.grantTypes?.includes("authorization_code") === true; when grantTypes is undefined this is undefined === truefalse.
  • Both call sites (authorize.ts:225 and authorize.ts:333) return c.text("Invalid client", 400) when the check fails, so an omitted grant_types blocks the flow.
  • RFC 7591 §2 specifies authorization_code as the default when grant_types is absent, so this rejects conformant CIMD documents.
  • The demo client helper getChatClientMetadata always emits explicit grant_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(" "),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) declares token_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) constructs authUrl setting only client_id, redirect_uri, response_type, scope, state, resource — no code_challenge; exchangeCodeForToken (~L186-215) sends no code_verifier.
  • createScopedAuthorizationServerMetadataResponse (authorization-server-metadata.ts L63) advertises code_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 /mcp server-side with the cookie-stored bearer token), and chat_oauth_state is 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add CIMD support for hosted MCP OAuth

1 participant