fix(oauth): serve AS metadata at the RFC 8414 strict path
All checks were successful
Deploy to Production / deploy (push) Successful in 1m24s

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.
This commit is contained in:
Marco Sadjadi 2026-05-28 19:47:47 +02:00
parent d2b19a5439
commit 86cf89ef42

View File

@ -54,10 +54,31 @@ async function resolveServerByResource(resource: string) {
}
export async function oauthRoutes(app: FastifyInstance): Promise<void> {
// 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<void> {
'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');