From 117fc0703d4732b55200330236272ba0767024c2 Mon Sep 17 00:00:00 2001 From: wille Date: Tue, 11 Nov 2025 00:59:29 +0100 Subject: [PATCH] Support issuer URLs with path components --- src/server/auth/router.test.ts | 18 ++++++++++++++++++ src/server/auth/router.ts | 29 +++++++++++++++++++---------- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/server/auth/router.test.ts b/src/server/auth/router.test.ts index f2091bcbe..c0dba0e01 100644 --- a/src/server/auth/router.test.ts +++ b/src/server/auth/router.test.ts @@ -266,6 +266,24 @@ describe('MCP Auth Router', () => { expect(response.body.scopes_supported).toEqual(['read', 'write']); expect(response.body.resource_name).toBe('Test API'); }); + + it('handles path components in issuer URL', async () => { + const app = express(); + + const options: AuthRouterOptions = { + provider: mockProvider, + issuerUrl: new URL('https://auth.example.com/oauth') + }; + app.use(mcpAuthRouter(options)); + + const response = await supertest(app).get('/.well-known/oauth-authorization-server/oauth'); + + expect(response.status).toBe(200); + expect(response.body.authorization_endpoint).toBe('https://auth.example.com/oauth/authorize'); + expect(response.body.token_endpoint).toBe('https://auth.example.com/oauth/token'); + expect(response.body.registration_endpoint).toBe('https://auth.example.com/oauth/register'); + expect(response.body.revocation_endpoint).toBe('https://auth.example.com/oauth/revoke'); + }); }); describe('Endpoint routing', () => { diff --git a/src/server/auth/router.ts b/src/server/auth/router.ts index dc0a85a33..974b15823 100644 --- a/src/server/auth/router.ts +++ b/src/server/auth/router.ts @@ -74,33 +74,33 @@ export const createOAuthMetadata = (options: { scopesSupported?: string[]; }): OAuthMetadata => { const issuer = options.issuerUrl; - const baseUrl = options.baseUrl; + const baseUrl = options.baseUrl || issuer; checkIssuerUrl(issuer); - const authorization_endpoint = '/authorize'; - const token_endpoint = '/token'; - const registration_endpoint = options.provider.clientsStore.registerClient ? '/register' : undefined; - const revocation_endpoint = options.provider.revokeToken ? '/revoke' : undefined; + const basePath = baseUrl.pathname.endsWith('/') ? baseUrl.pathname.slice(0, -1) : baseUrl.pathname; + + const registration_endpoint = options.provider.clientsStore.registerClient ? new URL(`${basePath}/register`, baseUrl).href : undefined; + const revocation_endpoint = options.provider.revokeToken ? new URL(`${basePath}/revoke`, baseUrl).href : undefined; const metadata: OAuthMetadata = { issuer: issuer.href, service_documentation: options.serviceDocumentationUrl?.href, - authorization_endpoint: new URL(authorization_endpoint, baseUrl || issuer).href, + authorization_endpoint: new URL(`${basePath}/authorize`, baseUrl).href, response_types_supported: ['code'], code_challenge_methods_supported: ['S256'], - token_endpoint: new URL(token_endpoint, baseUrl || issuer).href, + token_endpoint: new URL(`${basePath}/token`, baseUrl).href, token_endpoint_auth_methods_supported: ['client_secret_post'], grant_types_supported: ['authorization_code', 'refresh_token'], scopes_supported: options.scopesSupported, - revocation_endpoint: revocation_endpoint ? new URL(revocation_endpoint, baseUrl || issuer).href : undefined, + revocation_endpoint, revocation_endpoint_auth_methods_supported: revocation_endpoint ? ['client_secret_post'] : undefined, - registration_endpoint: registration_endpoint ? new URL(registration_endpoint, baseUrl || issuer).href : undefined + registration_endpoint }; return metadata; @@ -133,6 +133,7 @@ export function mcpAuthRouter(options: AuthRouterOptions): RequestHandler { router.use( mcpAuthMetadataRouter({ oauthMetadata, + baseUrl: options.baseUrl, // Prefer explicit RS; otherwise fall back to AS baseUrl, then to issuer (back-compat) resourceServerUrl: options.resourceServerUrl ?? options.baseUrl ?? new URL(oauthMetadata.issuer), serviceDocumentationUrl: options.serviceDocumentationUrl, @@ -168,6 +169,13 @@ export type AuthMetadataOptions = { */ oauthMetadata: OAuthMetadata; + /** + * The base URL of the authorization server to use for the metadata endpoints. + * + * If not provided, the issuer URL will be used as the base URL. + */ + baseUrl?: URL; + /** * The url of the MCP server, for use in protected resource metadata */ @@ -209,7 +217,8 @@ export function mcpAuthMetadataRouter(options: AuthMetadataOptions): express.Rou router.use(`/.well-known/oauth-protected-resource${rsPath === '/' ? '' : rsPath}`, metadataHandler(protectedResourceMetadata)); // Always add this for OAuth Authorization Server metadata per RFC 8414 - router.use('/.well-known/oauth-authorization-server', metadataHandler(options.oauthMetadata)); + const asPath = new URL(options.baseUrl || options.oauthMetadata.issuer).pathname; + router.use(`/.well-known/oauth-authorization-server${asPath === '/' ? '' : asPath}`, metadataHandler(options.oauthMetadata)); return router; }