Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 21 additions & 3 deletions src/client/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -750,7 +750,7 @@ describe('OAuth Authorization', () => {
it('generates correct URLs for server with path', () => {
const urls = buildDiscoveryUrls('https://auth.example.com/tenant1');

expect(urls).toHaveLength(3);
expect(urls).toHaveLength(4);
expect(urls.map(u => ({ url: u.url.toString(), type: u.type }))).toEqual([
{
url: 'https://auth.example.com/.well-known/oauth-authorization-server/tenant1',
Expand All @@ -763,16 +763,33 @@ describe('OAuth Authorization', () => {
{
url: 'https://auth.example.com/tenant1/.well-known/openid-configuration',
type: 'oidc'
},
{
url: 'https://auth.example.com/.well-known/oauth-authorization-server',
type: 'oauth'
}
]);
});

it('handles URL object input', () => {
const urls = buildDiscoveryUrls(new URL('https://auth.example.com/tenant1'));

expect(urls).toHaveLength(3);
expect(urls).toHaveLength(4);
expect(urls[0].url.toString()).toBe('https://auth.example.com/.well-known/oauth-authorization-server/tenant1');
});

it('includes root fallback for URLs with path (RFC 9470 workaround)', () => {
// Some servers incorrectly set authorization_servers to an endpoint path
// (e.g., "/api/auth") instead of the issuer URL, violating RFC 9470.
// The root fallback allows discovery to succeed in these cases.
const urls = buildDiscoveryUrls('https://example.com/api/auth');

// Last URL should be the root fallback
expect(urls[urls.length - 1]).toEqual({
url: new URL('https://example.com/.well-known/oauth-authorization-server'),
type: 'oauth'
});
});
});

describe('discoverAuthorizationServerMetadata', () => {
Expand Down Expand Up @@ -912,7 +929,8 @@ describe('OAuth Authorization', () => {
expect(metadata).toBeUndefined();

// Verify that all discovery URLs were attempted
expect(mockFetch).toHaveBeenCalledTimes(6); // 3 URLs × 2 attempts each (with and without headers)
// 4 URLs (3 path-based + 1 root fallback) × 2 attempts each (with and without headers)
expect(mockFetch).toHaveBeenCalledTimes(8);
});
});

Expand Down
12 changes: 11 additions & 1 deletion src/client/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -816,8 +816,9 @@ export async function discoverOAuthMetadata(
/**
* Builds a list of discovery URLs to try for authorization server metadata.
* URLs are returned in priority order:
* 1. OAuth metadata at the given URL
* 1. OAuth metadata at the given URL (path-suffixed if URL has path)
* 2. OIDC metadata endpoints at the given URL
* 3. Fallback to root OAuth metadata (when URL has path)
*/
export function buildDiscoveryUrls(authorizationServerUrl: string | URL): { url: URL; type: 'oauth' | 'oidc' }[] {
const url = typeof authorizationServerUrl === 'string' ? new URL(authorizationServerUrl) : authorizationServerUrl;
Expand Down Expand Up @@ -866,6 +867,15 @@ export function buildDiscoveryUrls(authorizationServerUrl: string | URL): { url:
type: 'oidc'
});

// Fallback: Try root path discovery when path-based discovery fails.
// This handles cases where authorization_servers contains an endpoint path
// (e.g., "/api/auth") instead of the issuer URL, which violates RFC 9470
// but occurs in practice with some MCP server implementations (better-auth) apparently.
urlsToTry.push({
url: new URL('/.well-known/oauth-authorization-server', url.origin),
type: 'oauth'
});

return urlsToTry;
}

Expand Down
Loading