Skip to content

fix: allow CORS on OAuth token exchange/register for browser-based public clients - #1289

Open
sentry-junior[bot] wants to merge 1 commit into
mainfrom
fix/oauth-token-cors-999
Open

fix: allow CORS on OAuth token exchange/register for browser-based public clients#1289
sentry-junior[bot] wants to merge 1 commit into
mainfrom
fix/oauth-token-cors-999

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Problem

POST https://mcp.sentry.dev/oauth/token (and /oauth/register) strip all CORS response headers unconditionally. Any browser-based OAuth client running on a different origin — the Cloudflare MCP Server Portal admin UI, MCP Inspector, etc. — is blocked at the preflight step and cannot complete the authorization-code exchange. Confirmed live:

curl -is -X OPTIONS https://mcp.sentry.dev/oauth/token \
  -H 'Origin: https://dash.cloudflare.com' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: content-type'

returns 204 with no access-control-* headers.

Fix

Add a third CORS policy in packages/mcp-cloudflare/src/server/utils/cors.ts scoped to exactly /oauth/token and /oauth/register: reflect the requesting Origin (never *, with Vary: Origin), allow POST, OPTIONS / Content-Type, Authorization, and never set Access-Control-Allow-Credentials. Every other OAuth/MCP route (/oauth/authorize, /mcp, etc.) keeps the existing strip-everything default.

This is safe because both endpoints are protected by PKCE (code_verifier) rather than by CORS/cookies — the authorization code alone can't be exchanged for a token cross-origin without the verifier — matching the model other hosted MCP servers (e.g. Amplitude) already use on the same endpoint.

Wired into both the OPTIONS-preflight interception and the response-finalize path in index.ts.

Testing

  • pnpm vitest run src/server/utils/cors.test.ts src/server/index.test.ts — 41/41 passing, including new cases for origin reflection, preflight, and the no-Origin/non-public-client strip paths.
  • tsc --noEmit clean.

Fixes #999.

Requested by David Cramer.

--

View Junior Session [Sentry]

…ion for browser-based public clients

Browser-based OAuth clients on a different origin (e.g. the Cloudflare MCP
Server Portal admin UI, MCP Inspector) cannot complete the authorization-code
+ PKCE exchange against mcp.sentry.dev because /oauth/token and
/oauth/register unconditionally strip CORS headers.

These two endpoints are protected by PKCE (and, for registration, no secret
is issued to a public client), so allowing cross-origin calls does not
bypass an auth boundary the way it would for cookie-authenticated routes.
Reflect the requesting Origin (never *) with no Allow-Credentials, leaving
every other OAuth/MCP route on the existing strip-by-default policy.

Fixes #999

Co-Authored-By: David Cramer <david@sentry.io>
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.

CORS preflight on POST /oauth/token blocks browser-based OAuth clients (Cloudflare MCP Portal admin UI)

0 participants