Only advertise OAuth PRM doc for the auth MCP endpoint on non-VA sites (#4387)

This commit is contained in:
spastorelli
2026-07-10 15:24:33 +02:00
committed by GitHub
parent e73b182925
commit 47ac3e2c81
3 changed files with 56 additions and 4 deletions
@@ -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);
});
});
});
+22 -1
View File
@@ -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).
*/
+5 -3
View File
@@ -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,