From 47ac3e2c81ff9b2a352726fde6de528a4ecfd3e7 Mon Sep 17 00:00:00 2001 From: spastorelli Date: Fri, 10 Jul 2026 15:24:33 +0200 Subject: [PATCH] Only advertise OAuth PRM doc for the auth MCP endpoint on non-VA sites (#4387) --- .../gitbook/src/lib/oauth-protected.test.ts | 29 +++++++++++++++++++ packages/gitbook/src/lib/oauth-protected.ts | 23 ++++++++++++++- packages/gitbook/src/middleware.ts | 8 +++-- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/packages/gitbook/src/lib/oauth-protected.test.ts b/packages/gitbook/src/lib/oauth-protected.test.ts index c3bad7c0e..c043902a1 100644 --- a/packages/gitbook/src/lib/oauth-protected.test.ts +++ b/packages/gitbook/src/lib/oauth-protected.test.ts @@ -5,6 +5,7 @@ import { createOAuthProtectedResourceUnauthResponse, handleUnauthedOAuthProtectedResourceRequest, isOAuthProtectedResourceMetadataRequest, + isOAuthProtectedResourceMetadataRequestForAuthEndpoint, isOAuthProtectedResourceRequest, } from './oauth-protected'; @@ -250,4 +251,32 @@ describe('OAuth protected resources flow', () => { expect(isOAuthProtectedResourceMetadataRequest(url)).toBe(expected); }); }); + + describe('isOAuthProtectedResourceMetadataRequestForAuthEndpoint', () => { + it.each([ + { + scenario: 'matches metadata doc for the authenticated resource', + input: 'https://docs.acme.org/.well-known/oauth-protected-resource/~gitbook/mcp/auth', + expected: true, + }, + { + scenario: 'does not match metadata doc for the public base resource', + input: 'https://docs.acme.org/.well-known/oauth-protected-resource/~gitbook/mcp', + expected: false, + }, + { + scenario: 'does not match the resource itself', + input: 'https://docs.acme.org/~gitbook/mcp/auth', + expected: false, + }, + { + scenario: 'does not match a non-protected path', + input: 'https://docs.acme.org/.well-known/oauth-protected-resource/~gitbook/other', + expected: false, + }, + ])('$scenario', ({ input, expected }) => { + const url = new URL(input); + expect(isOAuthProtectedResourceMetadataRequestForAuthEndpoint(url)).toBe(expected); + }); + }); }); diff --git a/packages/gitbook/src/lib/oauth-protected.ts b/packages/gitbook/src/lib/oauth-protected.ts index cff9519cc..148799a35 100644 --- a/packages/gitbook/src/lib/oauth-protected.ts +++ b/packages/gitbook/src/lib/oauth-protected.ts @@ -7,6 +7,13 @@ type OAuthProtectedResource = { endpoint: string; /** Authentication realm for this resource */ realm?: string; + /** + * Whether this resource requires authentication regardless of visitor auth. + * The base `~gitbook/mcp` endpoint is only protected when the site enforces + * visitor auth; `~gitbook/mcp/auth` always advertises auth so clients can opt + * into adaptive content on non-VA sites. + */ + authRequired?: boolean; }; /** @@ -18,7 +25,7 @@ const OAUTH_PROTECTED_RESOURCE_METADATA_PATH = '/.well-known/oauth-protected-res * List of OAuth protected resources. */ const OAUTH_PROTECTED_RESOURCES: OAuthProtectedResource[] = [ - { endpoint: '/~gitbook/mcp/auth', realm: 'mcp' }, + { endpoint: '/~gitbook/mcp/auth', realm: 'mcp', authRequired: true }, { endpoint: '/~gitbook/mcp', realm: 'mcp' }, ]; @@ -138,6 +145,20 @@ export function isOAuthProtectedResourceMetadataRequest( return Boolean(getMatchedProtectedMetadataEndpoint(siteRequestURL)); } +/** + * Check if a metadata request targets a resource that requires authentication + * regardless of visitor auth (e.g. `~gitbook/mcp/auth`). + * + * On non-VA sites the base `~gitbook/mcp` endpoint is public, so we must not + * advertise a PRM document for it: clients that discover PRM proactively would + * otherwise start an OAuth flow against an endpoint that never issues a challenge. + */ +export function isOAuthProtectedResourceMetadataRequestForAuthEndpoint( + siteRequestURL: URL | NextRequest['nextUrl'] +) { + return Boolean(getMatchedProtectedMetadataEndpoint(siteRequestURL)?.authRequired); +} + /** * Return the matched protected endpoint entry for a metadata doc request (if any). */ diff --git a/packages/gitbook/src/middleware.ts b/packages/gitbook/src/middleware.ts index 38071e2be..d5faa030d 100644 --- a/packages/gitbook/src/middleware.ts +++ b/packages/gitbook/src/middleware.ts @@ -27,7 +27,7 @@ import { MiddlewareHeaders } from '@/lib/middleware'; import { createOAuthProtectedResourceMetadataResponse, handleUnauthedOAuthProtectedResourceRequest, - isOAuthProtectedResourceMetadataRequest, + isOAuthProtectedResourceMetadataRequestForAuthEndpoint, isOAuthProtectedResourceRequest, } from '@/lib/oauth-protected'; import { removeLeadingSlash, removeTrailingSlash } from '@/lib/paths'; @@ -299,8 +299,10 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) { } // Handles OAuth protected resource metadata for non-VA adaptive content sites. - // If the requested URL resolved directly to a site, synthesize the metadata response immediately. - if (isOAuthProtectedResourceMetadataRequest(siteRequestURL)) { + // Only the `~gitbook/mcp/auth` endpoint advertises auth here; the base `~gitbook/mcp` + // endpoint stays public so clients doing proactive PRM discovery don't start an OAuth + // flow against an endpoint that never issues a challenge. + if (isOAuthProtectedResourceMetadataRequestForAuthEndpoint(siteRequestURL)) { return createOAuthProtectedResourceMetadataResponse({ siteRequestURL, siteId: siteURLData.site,