[auth] Fix march spec fallback for metadata discovery (#1108)

Co-authored-by: Felix Weinberger <fweinberger@anthropic.com>
This commit is contained in:
Paul Carleton 2025-11-13 19:31:42 +00:00 • committed by GitHub
commit 8019972832
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 75 additions and 3 deletions

View file

@ -1585,10 +1585,81 @@ describe('OAuth Authorization', () => {
// First call should be to protected resource metadata
expect(mockFetch.mock.calls[0][0].toString()).toBe('https://resource.example.com/.well-known/oauth-protected-resource');
// Second call should be to oauth metadata
// Second call should be to oauth metadata at the root path
expect(mockFetch.mock.calls[1][0].toString()).toBe('https://resource.example.com/.well-known/oauth-authorization-server');
});
it('uses base URL (with root path) as authorization server when protected-resource-metadata discovery fails', async () => {
// Setup: First call to protected resource metadata fails (404)
// When no authorization_servers are found in protected resource metadata,
// the auth server URL should be set to the base URL with "/" path
let callCount = 0;
mockFetch.mockImplementation(url => {
callCount++;
const urlString = url.toString();
if (urlString.includes('/.well-known/oauth-protected-resource')) {
// Protected resource metadata discovery attempts (both path-aware and root) fail with 404
return Promise.resolve({
ok: false,
status: 404
});
} else if (urlString === 'https://resource.example.com/.well-known/oauth-authorization-server') {
// Should fetch from base URL with root path, not the full serverUrl path
return Promise.resolve({
ok: true,
status: 200,
json: async () => ({
issuer: 'https://resource.example.com/',
authorization_endpoint: 'https://resource.example.com/authorize',
token_endpoint: 'https://resource.example.com/token',
registration_endpoint: 'https://resource.example.com/register',
response_types_supported: ['code'],
code_challenge_methods_supported: ['S256']
})
});
} else if (urlString.includes('/register')) {
// Client registration succeeds
return Promise.resolve({
ok: true,
status: 200,
json: async () => ({
client_id: 'test-client-id',
client_secret: 'test-client-secret',
client_id_issued_at: 1612137600,
client_secret_expires_at: 1612224000,
redirect_uris: ['http://localhost:3000/callback'],
client_name: 'Test Client'
})
});
}
return Promise.reject(new Error(`Unexpected fetch call #${callCount}: ${urlString}`));
});
// Mock provider methods
(mockProvider.clientInformation as jest.Mock).mockResolvedValue(undefined);
(mockProvider.tokens as jest.Mock).mockResolvedValue(undefined);
mockProvider.saveClientInformation = jest.fn();
// Call the auth function with a server URL that has a path
const result = await auth(mockProvider, {
serverUrl: 'https://resource.example.com/path/to/server'
});
// Verify the result
expect(result).toBe('REDIRECT');
// Verify that the oauth-authorization-server call uses the base URL
// This proves the fix: using new URL("/", serverUrl) instead of serverUrl
const authServerCall = mockFetch.mock.calls.find(call =>
call[0].toString().includes('/.well-known/oauth-authorization-server')
);
expect(authServerCall).toBeDefined();
expect(authServerCall[0].toString()).toBe('https://resource.example.com/.well-known/oauth-authorization-server');
});
it('passes resource parameter through authorization flow', async () => {
// Mock successful metadata discovery - need to include protected resource metadata
mockFetch.mockImplementation(url => {

View file

@ -348,6 +348,7 @@ async function authInternal(
): Promise<AuthResult> {
let resourceMetadata: OAuthProtectedResourceMetadata | undefined;
let authorizationServerUrl: string | URL | undefined;
try {
resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl }, fetchFn);
if (resourceMetadata.authorization_servers && resourceMetadata.authorization_servers.length > 0) {
@ -359,10 +360,10 @@ async function authInternal(
/**
* If we don't get a valid authorization server metadata from protected resource metadata,
* fallback to the legacy MCP spec's implementation (version 2025-03-26): MCP server acts as the Authorization server.
* fallback to the legacy MCP spec's implementation (version 2025-03-26): MCP server base URL acts as the Authorization server.
*/
if (!authorizationServerUrl) {
authorizationServerUrl = serverUrl;
authorizationServerUrl = new URL('/', serverUrl);
}
const resource: URL | undefined = await selectResourceURL(serverUrl, provider, resourceMetadata);