From fb6eaaf06d3a0fabaa17d91d28bad7ed4a667da1 Mon Sep 17 00:00:00 2001 From: jubnl Date: Tue, 5 May 2026 13:29:40 +0200 Subject: [PATCH] fix: open CORS for OAuth register/authorize + correct WWW-Authenticate PRM path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-up fixes after the SDK auth migration: 1. CORS for browser-based OAuth clients (ChatGPT DCR 403) The global cors({ origin: false }) intercepts OPTIONS preflight for /oauth/register and /oauth/authorize before the SDK's own cors() middleware inside clientRegistrationHandler/authorizationHandler runs, causing the browser to reject the response with no Access-Control-Allow-Origin header. ChatGPT's connector makes DCR from the browser, so this manifested as a 403. Fix: extend the open-CORS pre-middleware to also cover /oauth/register and /oauth/authorize (same pattern as /.well-known). 2. WWW-Authenticate resource_metadata URL (RFC 9728 §5) The MCP handler was advertising the base PRM path (/.well-known/oauth-protected-resource) instead of the path-aware variant (/.well-known/oauth-protected-resource/mcp). RFC 9728 requires the resource path to be appended when the resource URI has a path component. The SDK registers the path-aware URL; the WWW-Authenticate header now points to the same location. --- server/src/app.ts | 14 +++++++++++--- server/src/mcp/index.ts | 3 ++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/server/src/app.ts b/server/src/app.ts index be9694f3..e86de386 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -95,11 +95,19 @@ export function createApp(): express.Application { const hstsActive = shouldForceHttps || process.env.NODE_ENV === 'production'; const hstsIncludeSubdomains = process.env.HSTS_INCLUDE_SUBDOMAINS === 'true'; - // RFC 8414 / RFC 9728: discovery docs are world-readable — open CORS regardless of deployment config - // Covers both the base path and the RFC 9728 path-based variant (/.well-known/oauth-protected-resource/mcp) + // RFC 8414 / RFC 9728 / RFC 7591: discovery docs and DCR are world-readable/writable — + // open CORS for external MCP clients regardless of the deployment's ALLOWED_ORIGINS config. + // /oauth/register and /oauth/authorize need it because browser-based clients (ChatGPT, etc.) + // send a CORS preflight that the global cors({ origin: false }) would answer WITHOUT + // Access-Control-Allow-Origin, causing the browser to reject the response before the + // SDK's own cors() middleware inside clientRegistrationHandler/authorizationHandler runs. app.use( (req: Request, _res: Response, next: NextFunction) => { - if (req.path.startsWith('/.well-known/oauth-')) { + if ( + req.path.startsWith('/.well-known/oauth-') || + req.path === '/oauth/register' || + req.path === '/oauth/authorize' + ) { cors({ origin: '*', credentials: false })(req, _res, next); } else { next(); diff --git a/server/src/mcp/index.ts b/server/src/mcp/index.ts index e46c03db..2984811a 100644 --- a/server/src/mcp/index.ts +++ b/server/src/mcp/index.ts @@ -154,8 +154,9 @@ sessionSweepInterval.unref(); function setAuthChallenge(res: Response, error = 'invalid_token'): void { const base = (getAppUrl() || '').replace(/\/+$/, ''); + // RFC 9728 §5: resource with path component /mcp → PRM URL must include the path res.set('WWW-Authenticate', - `Bearer realm="TREK MCP", resource_metadata="${base}/.well-known/oauth-protected-resource", error="${error}"`); + `Bearer realm="TREK MCP", resource_metadata="${base}/.well-known/oauth-protected-resource/mcp", error="${error}"`); } interface VerifyTokenResult {