mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat(mcp): Add ODIC fallback to OAuth metadata look up (#6863)
Co-authored-by: cornmander <shikhman@google.com>
This commit is contained in:
@@ -142,6 +142,74 @@ describe('OAuthUtils', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('discoverAuthorizationServerMetadata', () => {
|
||||||
|
const mockAuthServerMetadata: OAuthAuthorizationServerMetadata = {
|
||||||
|
issuer: 'https://auth.example.com',
|
||||||
|
authorization_endpoint: 'https://auth.example.com/authorize',
|
||||||
|
token_endpoint: 'https://auth.example.com/token',
|
||||||
|
scopes_supported: ['read', 'write'],
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should handle URLs without path components correctly', async () => {
|
||||||
|
mockFetch
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
ok: false,
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve(mockAuthServerMetadata),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await OAuthUtils.discoverAuthorizationServerMetadata(
|
||||||
|
'https://auth.example.com/',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockAuthServerMetadata);
|
||||||
|
|
||||||
|
expect(mockFetch).nthCalledWith(
|
||||||
|
1,
|
||||||
|
'https://auth.example.com/.well-known/oauth-authorization-server',
|
||||||
|
);
|
||||||
|
expect(mockFetch).nthCalledWith(
|
||||||
|
2,
|
||||||
|
'https://auth.example.com/.well-known/openid-configuration',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle URLs with path components correctly', async () => {
|
||||||
|
mockFetch
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
ok: false,
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
ok: false,
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve(mockAuthServerMetadata),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await OAuthUtils.discoverAuthorizationServerMetadata(
|
||||||
|
'https://auth.example.com/mcp',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockAuthServerMetadata);
|
||||||
|
|
||||||
|
expect(mockFetch).nthCalledWith(
|
||||||
|
1,
|
||||||
|
'https://auth.example.com/.well-known/oauth-authorization-server/mcp',
|
||||||
|
);
|
||||||
|
expect(mockFetch).nthCalledWith(
|
||||||
|
2,
|
||||||
|
'https://auth.example.com/.well-known/openid-configuration/mcp',
|
||||||
|
);
|
||||||
|
expect(mockFetch).nthCalledWith(
|
||||||
|
3,
|
||||||
|
'https://auth.example.com/mcp/.well-known/openid-configuration',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('metadataToOAuthConfig', () => {
|
describe('metadataToOAuthConfig', () => {
|
||||||
it('should convert metadata to OAuth config', () => {
|
it('should convert metadata to OAuth config', () => {
|
||||||
const metadata: OAuthAuthorizationServerMetadata = {
|
const metadata: OAuthAuthorizationServerMetadata = {
|
||||||
|
|||||||
@@ -140,6 +140,76 @@ export class OAuthUtils {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discover Oauth Authorization server metadata given an Auth server URL, by
|
||||||
|
* trying the standard well-known endpoints.
|
||||||
|
*
|
||||||
|
* @param authServerUrl The authorization server URL
|
||||||
|
* @returns The authorization server metadata or null if not found
|
||||||
|
*/
|
||||||
|
static async discoverAuthorizationServerMetadata(
|
||||||
|
authServerUrl: string,
|
||||||
|
): Promise<OAuthAuthorizationServerMetadata | null> {
|
||||||
|
const authServerUrlObj = new URL(authServerUrl);
|
||||||
|
const base = `${authServerUrlObj.protocol}//${authServerUrlObj.host}`;
|
||||||
|
|
||||||
|
const endpointsToTry: string[] = [];
|
||||||
|
|
||||||
|
// With issuer URLs with path components, try the following well-known
|
||||||
|
// endpoints in order:
|
||||||
|
if (authServerUrlObj.pathname !== '/') {
|
||||||
|
// 1. OAuth 2.0 Authorization Server Metadata with path insertion
|
||||||
|
endpointsToTry.push(
|
||||||
|
new URL(
|
||||||
|
`/.well-known/oauth-authorization-server${authServerUrlObj.pathname}`,
|
||||||
|
base,
|
||||||
|
).toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. OpenID Connect Discovery 1.0 with path insertion
|
||||||
|
endpointsToTry.push(
|
||||||
|
new URL(
|
||||||
|
`/.well-known/openid-configuration${authServerUrlObj.pathname}`,
|
||||||
|
base,
|
||||||
|
).toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. OpenID Connect Discovery 1.0 with path appending
|
||||||
|
endpointsToTry.push(
|
||||||
|
new URL(
|
||||||
|
`${authServerUrlObj.pathname}/.well-known/openid-configuration`,
|
||||||
|
base,
|
||||||
|
).toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// With issuer URLs without path components, and those that failed previous
|
||||||
|
// discoveries, try the following well-known endpoints in order:
|
||||||
|
|
||||||
|
// 1. OAuth 2.0 Authorization Server Metadata
|
||||||
|
endpointsToTry.push(
|
||||||
|
new URL('/.well-known/oauth-authorization-server', base).toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. OpenID Connect Discovery 1.0
|
||||||
|
endpointsToTry.push(
|
||||||
|
new URL('/.well-known/openid-configuration', base).toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const endpoint of endpointsToTry) {
|
||||||
|
const authServerMetadata =
|
||||||
|
await this.fetchAuthorizationServerMetadata(endpoint);
|
||||||
|
if (authServerMetadata) {
|
||||||
|
return authServerMetadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug(
|
||||||
|
`Metadata discovery failed for authorization server ${authServerUrl}`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Discover OAuth configuration using the standard well-known endpoints.
|
* Discover OAuth configuration using the standard well-known endpoints.
|
||||||
*
|
*
|
||||||
@@ -172,33 +242,8 @@ export class OAuthUtils {
|
|||||||
if (resourceMetadata?.authorization_servers?.length) {
|
if (resourceMetadata?.authorization_servers?.length) {
|
||||||
// Use the first authorization server
|
// Use the first authorization server
|
||||||
const authServerUrl = resourceMetadata.authorization_servers[0];
|
const authServerUrl = resourceMetadata.authorization_servers[0];
|
||||||
|
const authServerMetadata =
|
||||||
// The authorization server URL may include a path (e.g., https://github.com/login/oauth)
|
await this.discoverAuthorizationServerMetadata(authServerUrl);
|
||||||
// We need to preserve this path when constructing the metadata URL
|
|
||||||
const authServerUrlObj = new URL(authServerUrl);
|
|
||||||
const authServerPath =
|
|
||||||
authServerUrlObj.pathname === '/' ? '' : authServerUrlObj.pathname;
|
|
||||||
|
|
||||||
// Try with the authorization server's path first
|
|
||||||
let authServerMetadataUrl = new URL(
|
|
||||||
`/.well-known/oauth-authorization-server${authServerPath}`,
|
|
||||||
`${authServerUrlObj.protocol}//${authServerUrlObj.host}`,
|
|
||||||
).toString();
|
|
||||||
|
|
||||||
let authServerMetadata = await this.fetchAuthorizationServerMetadata(
|
|
||||||
authServerMetadataUrl,
|
|
||||||
);
|
|
||||||
|
|
||||||
// If that fails, try root as fallback
|
|
||||||
if (!authServerMetadata && authServerPath) {
|
|
||||||
authServerMetadataUrl = new URL(
|
|
||||||
'/.well-known/oauth-authorization-server',
|
|
||||||
`${authServerUrlObj.protocol}//${authServerUrlObj.host}`,
|
|
||||||
).toString();
|
|
||||||
authServerMetadata = await this.fetchAuthorizationServerMetadata(
|
|
||||||
authServerMetadataUrl,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authServerMetadata) {
|
if (authServerMetadata) {
|
||||||
const config = this.metadataToOAuthConfig(authServerMetadata);
|
const config = this.metadataToOAuthConfig(authServerMetadata);
|
||||||
@@ -212,13 +257,10 @@ export class OAuthUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: try /.well-known/oauth-authorization-server at the base URL
|
// Fallback: try well-known endpoints at the base URL
|
||||||
console.debug(
|
console.debug(`Trying OAuth discovery fallback at ${serverUrl}`);
|
||||||
`Trying OAuth discovery fallback at ${wellKnownUrls.authorizationServer}`,
|
const authServerMetadata =
|
||||||
);
|
await this.discoverAuthorizationServerMetadata(serverUrl);
|
||||||
const authServerMetadata = await this.fetchAuthorizationServerMetadata(
|
|
||||||
wellKnownUrls.authorizationServer,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (authServerMetadata) {
|
if (authServerMetadata) {
|
||||||
const config = this.metadataToOAuthConfig(authServerMetadata);
|
const config = this.metadataToOAuthConfig(authServerMetadata);
|
||||||
@@ -277,34 +319,8 @@ export class OAuthUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const authServerUrl = resourceMetadata.authorization_servers[0];
|
const authServerUrl = resourceMetadata.authorization_servers[0];
|
||||||
|
const authServerMetadata =
|
||||||
// The authorization server URL may include a path (e.g., https://github.com/login/oauth)
|
await this.discoverAuthorizationServerMetadata(authServerUrl);
|
||||||
// We need to preserve this path when constructing the metadata URL
|
|
||||||
const authServerUrlObj = new URL(authServerUrl);
|
|
||||||
const authServerPath =
|
|
||||||
authServerUrlObj.pathname === '/' ? '' : authServerUrlObj.pathname;
|
|
||||||
|
|
||||||
// Build auth server metadata URL with the authorization server's path
|
|
||||||
const authServerMetadataUrl = new URL(
|
|
||||||
`/.well-known/oauth-authorization-server${authServerPath}`,
|
|
||||||
`${authServerUrlObj.protocol}//${authServerUrlObj.host}`,
|
|
||||||
).toString();
|
|
||||||
|
|
||||||
let authServerMetadata = await this.fetchAuthorizationServerMetadata(
|
|
||||||
authServerMetadataUrl,
|
|
||||||
);
|
|
||||||
|
|
||||||
// If that fails and we have a path, also try the root path as a fallback
|
|
||||||
if (!authServerMetadata && authServerPath) {
|
|
||||||
const rootAuthServerMetadataUrl = new URL(
|
|
||||||
'/.well-known/oauth-authorization-server',
|
|
||||||
`${authServerUrlObj.protocol}//${authServerUrlObj.host}`,
|
|
||||||
).toString();
|
|
||||||
|
|
||||||
authServerMetadata = await this.fetchAuthorizationServerMetadata(
|
|
||||||
rootAuthServerMetadataUrl,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authServerMetadata) {
|
if (authServerMetadata) {
|
||||||
return this.metadataToOAuthConfig(authServerMetadata);
|
return this.metadataToOAuthConfig(authServerMetadata);
|
||||||
|
|||||||
Reference in New Issue
Block a user