Skip to content

Commit 46af223

Browse files
committed
fix: add root fallback for OAuth discovery when URL has path
Some servers incorrectly set authorization_servers to an endpoint path instead of the issuer URL, violating RFC 9470. Add a fallback to try root-level OAuth discovery (/.well-known/oauth-authorization-server) when path-based discovery fails. This allows discovery to succeed with non-compliant server implementations while maintaining spec-compliant behavior as the primary path.
1 parent 4fb4d4d commit 46af223

File tree

3 files changed

+41
-4
lines changed

3 files changed

+41
-4
lines changed

package-lock.json

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/client/auth.test.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -750,7 +750,7 @@ describe('OAuth Authorization', () => {
750750
it('generates correct URLs for server with path', () => {
751751
const urls = buildDiscoveryUrls('https://auth.example.com/tenant1');
752752

753-
expect(urls).toHaveLength(3);
753+
expect(urls).toHaveLength(4);
754754
expect(urls.map(u => ({ url: u.url.toString(), type: u.type }))).toEqual([
755755
{
756756
url: 'https://auth.example.com/.well-known/oauth-authorization-server/tenant1',
@@ -763,16 +763,33 @@ describe('OAuth Authorization', () => {
763763
{
764764
url: 'https://auth.example.com/tenant1/.well-known/openid-configuration',
765765
type: 'oidc'
766+
},
767+
{
768+
url: 'https://auth.example.com/.well-known/oauth-authorization-server',
769+
type: 'oauth'
766770
}
767771
]);
768772
});
769773

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

773-
expect(urls).toHaveLength(3);
777+
expect(urls).toHaveLength(4);
774778
expect(urls[0].url.toString()).toBe('https://auth.example.com/.well-known/oauth-authorization-server/tenant1');
775779
});
780+
781+
it('includes root fallback for URLs with path (RFC 9470 workaround)', () => {
782+
// Some servers incorrectly set authorization_servers to an endpoint path
783+
// (e.g., "/api/auth") instead of the issuer URL, violating RFC 9470.
784+
// The root fallback allows discovery to succeed in these cases.
785+
const urls = buildDiscoveryUrls('https://example.com/api/auth');
786+
787+
// Last URL should be the root fallback
788+
expect(urls[urls.length - 1]).toEqual({
789+
url: new URL('https://example.com/.well-known/oauth-authorization-server'),
790+
type: 'oauth'
791+
});
792+
});
776793
});
777794

778795
describe('discoverAuthorizationServerMetadata', () => {
@@ -912,7 +929,8 @@ describe('OAuth Authorization', () => {
912929
expect(metadata).toBeUndefined();
913930

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

src/client/auth.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -816,8 +816,9 @@ export async function discoverOAuthMetadata(
816816
/**
817817
* Builds a list of discovery URLs to try for authorization server metadata.
818818
* URLs are returned in priority order:
819-
* 1. OAuth metadata at the given URL
819+
* 1. OAuth metadata at the given URL (path-suffixed if URL has path)
820820
* 2. OIDC metadata endpoints at the given URL
821+
* 3. Fallback to root OAuth metadata (when URL has path)
821822
*/
822823
export function buildDiscoveryUrls(authorizationServerUrl: string | URL): { url: URL; type: 'oauth' | 'oidc' }[] {
823824
const url = typeof authorizationServerUrl === 'string' ? new URL(authorizationServerUrl) : authorizationServerUrl;
@@ -866,6 +867,15 @@ export function buildDiscoveryUrls(authorizationServerUrl: string | URL): { url:
866867
type: 'oidc'
867868
});
868869

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

0 commit comments

Comments
 (0)