From 3066288c069a2a8a40f3aa58cebaaf83847cdbf7 Mon Sep 17 00:00:00 2001 From: Vijay Vasudevan Date: Fri, 23 Jan 2026 10:55:23 -0800 Subject: [PATCH] fix(core): use RFC 9728 compliant path-based OAuth protected resource discovery (#15756) Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com> --- packages/core/src/mcp/oauth-utils.test.ts | 47 ++++++++++++--------- packages/core/src/mcp/oauth-utils.ts | 50 +++++++++++------------ 2 files changed, 51 insertions(+), 46 deletions(-) diff --git a/packages/core/src/mcp/oauth-utils.test.ts b/packages/core/src/mcp/oauth-utils.test.ts index 0e17f3c32e..8184442b1a 100644 --- a/packages/core/src/mcp/oauth-utils.test.ts +++ b/packages/core/src/mcp/oauth-utils.test.ts @@ -28,21 +28,8 @@ describe('OAuthUtils', () => { }); describe('buildWellKnownUrls', () => { - it('should build standard root-based URLs by default', () => { + it('should build RFC 9728 compliant path-based URLs by default', () => { const urls = OAuthUtils.buildWellKnownUrls('https://example.com/mcp'); - expect(urls.protectedResource).toBe( - 'https://example.com/.well-known/oauth-protected-resource', - ); - expect(urls.authorizationServer).toBe( - 'https://example.com/.well-known/oauth-authorization-server', - ); - }); - - it('should build path-based URLs when includePathSuffix is true', () => { - const urls = OAuthUtils.buildWellKnownUrls( - 'https://example.com/mcp', - true, - ); expect(urls.protectedResource).toBe( 'https://example.com/.well-known/oauth-protected-resource/mcp', ); @@ -51,8 +38,21 @@ describe('OAuthUtils', () => { ); }); + it('should build root-based URLs when useRootDiscovery is true', () => { + const urls = OAuthUtils.buildWellKnownUrls( + 'https://example.com/mcp', + true, + ); + expect(urls.protectedResource).toBe( + 'https://example.com/.well-known/oauth-protected-resource', + ); + expect(urls.authorizationServer).toBe( + 'https://example.com/.well-known/oauth-authorization-server', + ); + }); + it('should handle root path correctly', () => { - const urls = OAuthUtils.buildWellKnownUrls('https://example.com', true); + const urls = OAuthUtils.buildWellKnownUrls('https://example.com'); expect(urls.protectedResource).toBe( 'https://example.com/.well-known/oauth-protected-resource', ); @@ -62,10 +62,7 @@ describe('OAuthUtils', () => { }); it('should handle trailing slash in path', () => { - const urls = OAuthUtils.buildWellKnownUrls( - 'https://example.com/mcp/', - true, - ); + const urls = OAuthUtils.buildWellKnownUrls('https://example.com/mcp/'); expect(urls.protectedResource).toBe( 'https://example.com/.well-known/oauth-protected-resource/mcp', ); @@ -73,6 +70,18 @@ describe('OAuthUtils', () => { 'https://example.com/.well-known/oauth-authorization-server/mcp', ); }); + + it('should handle deep paths per RFC 9728', () => { + const urls = OAuthUtils.buildWellKnownUrls( + 'https://app.mintmcp.com/s/g_2lj2CNDoJdf3xnbFeeF6vx/mcp', + ); + expect(urls.protectedResource).toBe( + 'https://app.mintmcp.com/.well-known/oauth-protected-resource/s/g_2lj2CNDoJdf3xnbFeeF6vx/mcp', + ); + expect(urls.authorizationServer).toBe( + 'https://app.mintmcp.com/.well-known/oauth-authorization-server/s/g_2lj2CNDoJdf3xnbFeeF6vx/mcp', + ); + }); }); describe('fetchProtectedResourceMetadata', () => { diff --git a/packages/core/src/mcp/oauth-utils.ts b/packages/core/src/mcp/oauth-utils.ts index de87838a2a..98c39f4261 100644 --- a/packages/core/src/mcp/oauth-utils.ts +++ b/packages/core/src/mcp/oauth-utils.ts @@ -55,30 +55,26 @@ export const FIVE_MIN_BUFFER_MS = 5 * 60 * 1000; */ export class OAuthUtils { /** - * Construct well-known OAuth endpoint URLs. - * By default, uses standard root-based well-known URLs. - * If includePathSuffix is true, appends any path from the base URL to the well-known endpoints. + * Construct well-known OAuth endpoint URLs per RFC 9728 §3.1. + * + * The well-known URI is constructed by inserting /.well-known/oauth-protected-resource + * between the host and any existing path component. This preserves the resource's + * path structure in the metadata URL. + * + * Examples: + * - https://example.com -> https://example.com/.well-known/oauth-protected-resource + * - https://example.com/api/resource -> https://example.com/.well-known/oauth-protected-resource/api/resource + * + * @param baseUrl The resource URL + * @param useRootDiscovery If true, ignores path and uses root-based discovery (for fallback compatibility) */ - static buildWellKnownUrls(baseUrl: string, includePathSuffix = false) { + static buildWellKnownUrls(baseUrl: string, useRootDiscovery = false) { const serverUrl = new URL(baseUrl); const base = `${serverUrl.protocol}//${serverUrl.host}`; + const pathSuffix = useRootDiscovery + ? '' + : serverUrl.pathname.replace(/\/$/, ''); // Remove trailing slash - if (!includePathSuffix) { - // Standard discovery: use root-based well-known URLs - return { - protectedResource: new URL( - '/.well-known/oauth-protected-resource', - base, - ).toString(), - authorizationServer: new URL( - '/.well-known/oauth-authorization-server', - base, - ).toString(), - }; - } - - // Path-based discovery: append path suffix to well-known URLs - const pathSuffix = serverUrl.pathname.replace(/\/$/, ''); // Remove trailing slash return { protectedResource: new URL( `/.well-known/oauth-protected-resource${pathSuffix}`, @@ -234,21 +230,21 @@ export class OAuthUtils { serverUrl: string, ): Promise { try { - // First try standard root-based discovery - const wellKnownUrls = this.buildWellKnownUrls(serverUrl, false); - - // Try to get the protected resource metadata at root + // RFC 9728 §3.1: Construct well-known URL by inserting /.well-known/oauth-protected-resource + // between the host and path. This is the RFC-compliant approach. + const wellKnownUrls = this.buildWellKnownUrls(serverUrl); let resourceMetadata = await this.fetchProtectedResourceMetadata( wellKnownUrls.protectedResource, ); - // If root discovery fails and we have a path, try path-based discovery + // Fallback: If path-based discovery fails and we have a path, try root-based discovery + // for backwards compatibility with servers that don't implement RFC 9728 path handling if (!resourceMetadata) { const url = new URL(serverUrl); if (url.pathname && url.pathname !== '/') { - const pathBasedUrls = this.buildWellKnownUrls(serverUrl, true); + const rootBasedUrls = this.buildWellKnownUrls(serverUrl, true); resourceMetadata = await this.fetchProtectedResourceMetadata( - pathBasedUrls.protectedResource, + rootBasedUrls.protectedResource, ); } }