Skip to content

Commit 87b453d

Browse files
ochafikclaude
andcommitted
auth: fetch AS metadata in well-known subpath from serverUrl when PRM returns external AS
Co-Authored-By: Claude <noreply@anthropic.com>
1 parent d681f14 commit 87b453d

File tree

2 files changed

+78
-6
lines changed

2 files changed

+78
-6
lines changed

src/client/auth.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1476,5 +1476,63 @@ describe("OAuth Authorization", () => {
14761476
expect(body.get("grant_type")).toBe("refresh_token");
14771477
expect(body.get("refresh_token")).toBe("refresh123");
14781478
});
1479+
1480+
it("fetches AS metadata with path from serverUrl when PRM returns external AS", async () => {
1481+
// Mock PRM discovery that returns an external AS
1482+
mockFetch.mockImplementation((url) => {
1483+
const urlString = url.toString();
1484+
1485+
if (urlString === "https://my.resource.com/.well-known/oauth-protected-resource") {
1486+
return Promise.resolve({
1487+
ok: true,
1488+
status: 200,
1489+
json: async () => ({
1490+
resource: "https://my.resource.com/",
1491+
authorization_servers: ["https://auth.example.com/"],
1492+
}),
1493+
});
1494+
} else if (urlString === "https://auth.example.com/.well-known/oauth-authorization-server/path/name") {
1495+
// Path-aware discovery on AS with path from serverUrl
1496+
return Promise.resolve({
1497+
ok: true,
1498+
status: 200,
1499+
json: async () => ({
1500+
issuer: "https://auth.example.com",
1501+
authorization_endpoint: "https://auth.example.com/authorize",
1502+
token_endpoint: "https://auth.example.com/token",
1503+
response_types_supported: ["code"],
1504+
code_challenge_methods_supported: ["S256"],
1505+
}),
1506+
});
1507+
}
1508+
1509+
return Promise.resolve({ ok: false, status: 404 });
1510+
});
1511+
1512+
// Mock provider methods
1513+
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
1514+
client_id: "test-client",
1515+
client_secret: "test-secret",
1516+
});
1517+
(mockProvider.tokens as jest.Mock).mockResolvedValue(undefined);
1518+
(mockProvider.saveCodeVerifier as jest.Mock).mockResolvedValue(undefined);
1519+
(mockProvider.redirectToAuthorization as jest.Mock).mockResolvedValue(undefined);
1520+
1521+
// Call auth with serverUrl that has a path
1522+
const result = await auth(mockProvider, {
1523+
serverUrl: "https://my.resource.com/path/name",
1524+
});
1525+
1526+
expect(result).toBe("REDIRECT");
1527+
1528+
// Verify the correct URLs were fetched
1529+
const calls = mockFetch.mock.calls;
1530+
1531+
// First call should be to PRM
1532+
expect(calls[0][0].toString()).toBe("https://my.resource.com/.well-known/oauth-protected-resource");
1533+
1534+
// Second call should be to AS metadata with the path from serverUrl
1535+
expect(calls[1][0].toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server/path/name");
1536+
});
14791537
});
14801538
});

src/client/auth.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,9 @@ export async function auth(
122122

123123
const resource: URL | undefined = await selectResourceURL(serverUrl, provider, resourceMetadata);
124124

125-
const metadata = await discoverOAuthMetadata(authorizationServerUrl);
125+
const metadata = await discoverOAuthMetadata(serverUrl, {
126+
authorizationServerUrl
127+
});
126128

127129
// Handle client registration if needed
128130
let clientInformation = await Promise.resolve(provider.clientInformation());
@@ -354,15 +356,27 @@ function shouldAttemptFallback(response: Response | undefined, pathname: string)
354356
* return `undefined`. Any other errors will be thrown as exceptions.
355357
*/
356358
export async function discoverOAuthMetadata(
357-
authorizationServerUrl: string | URL,
358-
opts?: { protocolVersion?: string },
359+
issuer: string | URL,
360+
{
361+
authorizationServerUrl
362+
}: {
363+
authorizationServerUrl?: string | URL
364+
} = {},
359365
): Promise<OAuthMetadata | undefined> {
360-
const issuer = new URL(authorizationServerUrl);
361-
const protocolVersion = opts?.protocolVersion ?? LATEST_PROTOCOL_VERSION;
366+
if (typeof issuer === 'string') {
367+
issuer = new URL(issuer);
368+
}
369+
if (!authorizationServerUrl) {
370+
authorizationServerUrl = issuer;
371+
}
372+
if (typeof authorizationServerUrl === 'string') {
373+
authorizationServerUrl = new URL(authorizationServerUrl);
374+
}
375+
protocolVersion ??= LATEST_PROTOCOL_VERSION;
362376

363377
// Try path-aware discovery first (RFC 8414 compliant)
364378
const wellKnownPath = buildWellKnownPath(issuer.pathname);
365-
const pathAwareUrl = new URL(wellKnownPath, issuer);
379+
const pathAwareUrl = new URL(wellKnownPath, authorizationServerUrl);
366380
let response = await tryMetadataDiscovery(pathAwareUrl, protocolVersion);
367381

368382
// If path-aware discovery fails with 404, try fallback to root discovery

0 commit comments

Comments
 (0)