mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 14:10:37 -07:00
fix(core): use RFC 9728 compliant path-based OAuth protected resource discovery (#15756)
Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
This commit is contained in:
@@ -28,21 +28,8 @@ describe('OAuthUtils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('buildWellKnownUrls', () => {
|
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');
|
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(
|
expect(urls.protectedResource).toBe(
|
||||||
'https://example.com/.well-known/oauth-protected-resource/mcp',
|
'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', () => {
|
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(
|
expect(urls.protectedResource).toBe(
|
||||||
'https://example.com/.well-known/oauth-protected-resource',
|
'https://example.com/.well-known/oauth-protected-resource',
|
||||||
);
|
);
|
||||||
@@ -62,10 +62,7 @@ describe('OAuthUtils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle trailing slash in path', () => {
|
it('should handle trailing slash in path', () => {
|
||||||
const urls = OAuthUtils.buildWellKnownUrls(
|
const urls = OAuthUtils.buildWellKnownUrls('https://example.com/mcp/');
|
||||||
'https://example.com/mcp/',
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
expect(urls.protectedResource).toBe(
|
expect(urls.protectedResource).toBe(
|
||||||
'https://example.com/.well-known/oauth-protected-resource/mcp',
|
'https://example.com/.well-known/oauth-protected-resource/mcp',
|
||||||
);
|
);
|
||||||
@@ -73,6 +70,18 @@ describe('OAuthUtils', () => {
|
|||||||
'https://example.com/.well-known/oauth-authorization-server/mcp',
|
'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', () => {
|
describe('fetchProtectedResourceMetadata', () => {
|
||||||
|
|||||||
@@ -55,30 +55,26 @@ export const FIVE_MIN_BUFFER_MS = 5 * 60 * 1000;
|
|||||||
*/
|
*/
|
||||||
export class OAuthUtils {
|
export class OAuthUtils {
|
||||||
/**
|
/**
|
||||||
* Construct well-known OAuth endpoint URLs.
|
* Construct well-known OAuth endpoint URLs per RFC 9728 §3.1.
|
||||||
* 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.
|
* 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 serverUrl = new URL(baseUrl);
|
||||||
const base = `${serverUrl.protocol}//${serverUrl.host}`;
|
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 {
|
return {
|
||||||
protectedResource: new URL(
|
protectedResource: new URL(
|
||||||
`/.well-known/oauth-protected-resource${pathSuffix}`,
|
`/.well-known/oauth-protected-resource${pathSuffix}`,
|
||||||
@@ -234,21 +230,21 @@ export class OAuthUtils {
|
|||||||
serverUrl: string,
|
serverUrl: string,
|
||||||
): Promise<MCPOAuthConfig | null> {
|
): Promise<MCPOAuthConfig | null> {
|
||||||
try {
|
try {
|
||||||
// First try standard root-based discovery
|
// RFC 9728 §3.1: Construct well-known URL by inserting /.well-known/oauth-protected-resource
|
||||||
const wellKnownUrls = this.buildWellKnownUrls(serverUrl, false);
|
// between the host and path. This is the RFC-compliant approach.
|
||||||
|
const wellKnownUrls = this.buildWellKnownUrls(serverUrl);
|
||||||
// Try to get the protected resource metadata at root
|
|
||||||
let resourceMetadata = await this.fetchProtectedResourceMetadata(
|
let resourceMetadata = await this.fetchProtectedResourceMetadata(
|
||||||
wellKnownUrls.protectedResource,
|
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) {
|
if (!resourceMetadata) {
|
||||||
const url = new URL(serverUrl);
|
const url = new URL(serverUrl);
|
||||||
if (url.pathname && url.pathname !== '/') {
|
if (url.pathname && url.pathname !== '/') {
|
||||||
const pathBasedUrls = this.buildWellKnownUrls(serverUrl, true);
|
const rootBasedUrls = this.buildWellKnownUrls(serverUrl, true);
|
||||||
resourceMetadata = await this.fetchProtectedResourceMetadata(
|
resourceMetadata = await this.fetchProtectedResourceMetadata(
|
||||||
pathBasedUrls.protectedResource,
|
rootBasedUrls.protectedResource,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user