From 71b5460e2bf2840e99db99f51b700eb6d4cc327b Mon Sep 17 00:00:00 2001 From: Vinay Dandekar Date: Sun, 2 Nov 2025 20:02:59 -0500 Subject: [PATCH 1/2] Use path-based discovery URLs before attempting to fall back on the root (if supplied) --- src/client/auth.test.ts | 18 +++++++++++------- src/client/auth.ts | 18 +++++++++++++----- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index 6c924898a..0268b497c 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -712,14 +712,14 @@ describe('OAuth Authorization', () => { it('generates correct URLs for server with path', () => { const urls = buildDiscoveryUrls('https://auth.example.com/tenant1'); - expect(urls).toHaveLength(4); + expect(urls).toHaveLength(5); expect(urls.map(u => ({ url: u.url.toString(), type: u.type }))).toEqual([ { url: 'https://auth.example.com/.well-known/oauth-authorization-server/tenant1', type: 'oauth' }, { - url: 'https://auth.example.com/.well-known/oauth-authorization-server', + url: 'https://auth.example.com/tenant1/.well-known/oauth-authorization-server', type: 'oauth' }, { @@ -729,6 +729,10 @@ 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' } ]); }); @@ -736,7 +740,7 @@ describe('OAuth Authorization', () => { it('handles URL object input', () => { const urls = buildDiscoveryUrls(new URL('https://auth.example.com/tenant1')); - expect(urls).toHaveLength(4); + expect(urls).toHaveLength(5); expect(urls[0].url.toString()).toBe('https://auth.example.com/.well-known/oauth-authorization-server/tenant1'); }); }); @@ -763,13 +767,13 @@ describe('OAuth Authorization', () => { }; it('tries URLs in order and returns first successful metadata', async () => { - // First OAuth URL fails with 404 + // First OAuth URL (path before well-known) fails with 404 mockFetch.mockResolvedValueOnce({ ok: false, status: 404 }); - // Second OAuth URL (root) succeeds + // Second OAuth URL (path after well-known) succeeds mockFetch.mockResolvedValueOnce({ ok: true, status: 200, @@ -784,7 +788,7 @@ describe('OAuth Authorization', () => { const calls = mockFetch.mock.calls; expect(calls.length).toBe(2); expect(calls[0][0].toString()).toBe('https://auth.example.com/.well-known/oauth-authorization-server/tenant1'); - expect(calls[1][0].toString()).toBe('https://auth.example.com/.well-known/oauth-authorization-server'); + expect(calls[1][0].toString()).toBe('https://auth.example.com/tenant1/.well-known/oauth-authorization-server'); }); it('continues on 4xx errors', async () => { @@ -878,7 +882,7 @@ describe('OAuth Authorization', () => { expect(metadata).toBeUndefined(); // Verify that all discovery URLs were attempted - expect(mockFetch).toHaveBeenCalledTimes(8); // 4 URLs × 2 attempts each (with and without headers) + expect(mockFetch).toHaveBeenCalledTimes(10); // 5 URLs × 2 attempts each (with and without headers) }); }); diff --git a/src/client/auth.ts b/src/client/auth.ts index 5e48345a3..018af896b 100644 --- a/src/client/auth.ts +++ b/src/client/auth.ts @@ -669,8 +669,8 @@ 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 - * 2. OAuth metadata at root (if URL has path) - * 3. OIDC metadata endpoints + * 2. OIDC metadata endpoints at the given URL + * 3. OAuth metadata at root (if URL has path) */ export function buildDiscoveryUrls(authorizationServerUrl: string | URL): { url: URL; type: 'oauth' | 'oidc' }[] { const url = typeof authorizationServerUrl === 'string' ? new URL(authorizationServerUrl) : authorizationServerUrl; @@ -706,24 +706,32 @@ export function buildDiscoveryUrls(authorizationServerUrl: string | URL): { url: type: 'oauth' }); - // Root path: https://example.com/.well-known/oauth-authorization-server + // Insert well-known after the path: https://example.com/tenant1/.well-known/oauth-authorization-server urlsToTry.push({ - url: new URL('/.well-known/oauth-authorization-server', url.origin), + url: new URL(`${pathname}/.well-known/oauth-authorization-server`, url.origin), type: 'oauth' }); - // 3. OIDC metadata endpoints + // 2. OIDC metadata endpoints // RFC 8414 style: Insert /.well-known/openid-configuration before the path urlsToTry.push({ url: new URL(`/.well-known/openid-configuration${pathname}`, url.origin), type: 'oidc' }); + // OIDC Discovery 1.0 style: Append /.well-known/openid-configuration after the path urlsToTry.push({ url: new URL(`${pathname}/.well-known/openid-configuration`, url.origin), type: 'oidc' }); + // 3. OAuth metadata at root + // Root path: https://example.com/.well-known/oauth-authorization-server + urlsToTry.push({ + url: new URL('/.well-known/oauth-authorization-server', url.origin), + type: 'oauth' + }); + return urlsToTry; } From 994a8e312c2190b32e164e672298a697fa2eef79 Mon Sep 17 00:00:00 2001 From: Vinay Dandekar Date: Mon, 3 Nov 2025 09:50:00 -0500 Subject: [PATCH 2/2] Remove fallback URL for discovery --- src/client/auth.test.ts | 22 +++++++--------------- src/client/auth.ts | 14 -------------- 2 files changed, 7 insertions(+), 29 deletions(-) diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index 0268b497c..5ba3975b6 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -712,16 +712,12 @@ describe('OAuth Authorization', () => { it('generates correct URLs for server with path', () => { const urls = buildDiscoveryUrls('https://auth.example.com/tenant1'); - expect(urls).toHaveLength(5); + expect(urls).toHaveLength(3); expect(urls.map(u => ({ url: u.url.toString(), type: u.type }))).toEqual([ { url: 'https://auth.example.com/.well-known/oauth-authorization-server/tenant1', type: 'oauth' }, - { - url: 'https://auth.example.com/tenant1/.well-known/oauth-authorization-server', - type: 'oauth' - }, { url: 'https://auth.example.com/.well-known/openid-configuration/tenant1', type: 'oidc' @@ -729,10 +725,6 @@ 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' } ]); }); @@ -740,7 +732,7 @@ describe('OAuth Authorization', () => { it('handles URL object input', () => { const urls = buildDiscoveryUrls(new URL('https://auth.example.com/tenant1')); - expect(urls).toHaveLength(5); + expect(urls).toHaveLength(3); expect(urls[0].url.toString()).toBe('https://auth.example.com/.well-known/oauth-authorization-server/tenant1'); }); }); @@ -773,22 +765,22 @@ describe('OAuth Authorization', () => { status: 404 }); - // Second OAuth URL (path after well-known) succeeds + // Second OIDC URL (path before well-known) succeeds mockFetch.mockResolvedValueOnce({ ok: true, status: 200, - json: async () => validOAuthMetadata + json: async () => validOpenIdMetadata }); const metadata = await discoverAuthorizationServerMetadata('https://auth.example.com/tenant1'); - expect(metadata).toEqual(validOAuthMetadata); + expect(metadata).toEqual(validOpenIdMetadata); // Verify it tried the URLs in the correct order const calls = mockFetch.mock.calls; expect(calls.length).toBe(2); expect(calls[0][0].toString()).toBe('https://auth.example.com/.well-known/oauth-authorization-server/tenant1'); - expect(calls[1][0].toString()).toBe('https://auth.example.com/tenant1/.well-known/oauth-authorization-server'); + expect(calls[1][0].toString()).toBe('https://auth.example.com/.well-known/openid-configuration/tenant1'); }); it('continues on 4xx errors', async () => { @@ -882,7 +874,7 @@ describe('OAuth Authorization', () => { expect(metadata).toBeUndefined(); // Verify that all discovery URLs were attempted - expect(mockFetch).toHaveBeenCalledTimes(10); // 5 URLs × 2 attempts each (with and without headers) + expect(mockFetch).toHaveBeenCalledTimes(6); // 3 URLs × 2 attempts each (with and without headers) }); }); diff --git a/src/client/auth.ts b/src/client/auth.ts index 018af896b..ea2400a25 100644 --- a/src/client/auth.ts +++ b/src/client/auth.ts @@ -670,7 +670,6 @@ export async function discoverOAuthMetadata( * URLs are returned in priority order: * 1. OAuth metadata at the given URL * 2. OIDC metadata endpoints at the given URL - * 3. OAuth metadata at root (if URL has path) */ export function buildDiscoveryUrls(authorizationServerUrl: string | URL): { url: URL; type: 'oauth' | 'oidc' }[] { const url = typeof authorizationServerUrl === 'string' ? new URL(authorizationServerUrl) : authorizationServerUrl; @@ -706,12 +705,6 @@ export function buildDiscoveryUrls(authorizationServerUrl: string | URL): { url: type: 'oauth' }); - // Insert well-known after the path: https://example.com/tenant1/.well-known/oauth-authorization-server - urlsToTry.push({ - url: new URL(`${pathname}/.well-known/oauth-authorization-server`, url.origin), - type: 'oauth' - }); - // 2. OIDC metadata endpoints // RFC 8414 style: Insert /.well-known/openid-configuration before the path urlsToTry.push({ @@ -725,13 +718,6 @@ export function buildDiscoveryUrls(authorizationServerUrl: string | URL): { url: type: 'oidc' }); - // 3. OAuth metadata at root - // Root path: https://example.com/.well-known/oauth-authorization-server - urlsToTry.push({ - url: new URL('/.well-known/oauth-authorization-server', url.origin), - type: 'oauth' - }); - return urlsToTry; }