From 86cf89ef42f64a81119561cada0a2ed049077396 Mon Sep 17 00:00:00 2001 From: Marco Sadjadi Date: Thu, 28 May 2026 19:47:47 +0200 Subject: [PATCH] fix(oauth): serve AS metadata at the RFC 8414 strict path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of Claude Desktop's repeated "Registrierung beim Anmeldedienst fehlgeschlagen" reference ofid_897eda676d452435: RFC 8414 §3 constructs the well-known discovery URL by INSERTING "/.well-known/oauth-authorization-server" between the host and the issuer path. For issuer https://api.buildmymcpserver.com/oauth the correct location is https://api.buildmymcpserver.com/.well-known/oauth-authorization-server/oauth We previously served only the issuer-appended form (/oauth/.well-known/...), which is the historically common but RFC-incorrect placement. Claude Desktop's MCP SDK is strict per RFC 8414, hit the 404, and bailed out of discovery before ever reaching /oauth/register — so the DCR fix from earlier never had a chance to run. Now serves the same metadata at four paths via a single handler: - /.well-known/oauth-authorization-server/oauth (RFC 8414 strict) - /.well-known/oauth-authorization-server (root fallback) - /oauth/.well-known/oauth-authorization-server (historical) - /.well-known/openid-configuration (OIDC fallback) A single buildAsMetadata() helper keeps them in sync. --- apps/api/src/routes/oauth.ts | 40 ++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/apps/api/src/routes/oauth.ts b/apps/api/src/routes/oauth.ts index 0331c75..79a0cea 100644 --- a/apps/api/src/routes/oauth.ts +++ b/apps/api/src/routes/oauth.ts @@ -54,10 +54,31 @@ async function resolveServerByResource(resource: string) { } export async function oauthRoutes(app: FastifyInstance): Promise { - // Authorization Server Metadata (RFC 8414) — control-plane wide - app.get('/oauth/.well-known/oauth-authorization-server', async (_req, reply) => { - const base = `${config.CONTROL_PLANE_PUBLIC_URL}`; - return reply.send({ + // Authorization Server Metadata (RFC 8414). + // + // Our issuer is `${CONTROL_PLANE_PUBLIC_URL}/oauth`. RFC 8414 §3 says the + // discovery URL is constructed by inserting "/.well-known/oauth- + // authorization-server" *between the host and the issuer path*, NOT by + // appending it after the path. So for issuer `https://api.example.com/oauth` + // the canonical location is `https://api.example.com/.well-known/oauth- + // authorization-server/oauth`. + // + // Claude Desktop's MCP SDK follows that strict construction. We previously + // only served the issuer-appended path (`/oauth/.well-known/...`), which + // is the historically-common but incorrect form, so Claude Desktop 404'd + // during discovery and reported "Registrierung beim Anmeldedienst + // fehlgeschlagen" without ever reaching the registration endpoint. We + // now serve every realistic variant pointing at the same metadata: + // + // - `/.well-known/oauth-authorization-server/oauth` — RFC 8414 strict + // - `/.well-known/oauth-authorization-server` — many clients try root + // - `/oauth/.well-known/oauth-authorization-server` — historical/Okta-style + // - `/.well-known/openid-configuration` — OIDC fallback + // + // The single source of truth is buildAsMetadata() so they cannot drift. + const buildAsMetadata = () => { + const base = config.CONTROL_PLANE_PUBLIC_URL; + return { issuer: `${base}/oauth`, authorization_endpoint: `${base}/oauth/authorize`, token_endpoint: `${base}/oauth/token`, @@ -72,8 +93,15 @@ export async function oauthRoutes(app: FastifyInstance): Promise { 'none', ], scopes_supported: ['mcp:read', 'mcp:write'], - }); - }); + }; + }; + const asMetadataHandler = async (_req: unknown, reply: { send: (body: unknown) => unknown }) => + reply.send(buildAsMetadata()); + + app.get('/.well-known/oauth-authorization-server/oauth', asMetadataHandler); + app.get('/.well-known/oauth-authorization-server', asMetadataHandler); + app.get('/oauth/.well-known/oauth-authorization-server', asMetadataHandler); + app.get('/.well-known/openid-configuration', asMetadataHandler); app.get('/oauth/jwks', async (_req, reply) => { reply.header('cache-control', 'public, max-age=300');