auth: fetch AS metadata in well-known subpath from serverUrl when PRM returns external AS
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d681f14b33
commit
87b453d23f
2 changed files with 78 additions and 6 deletions
|
|
@ -1476,5 +1476,63 @@ describe("OAuth Authorization", () => {
|
|||
expect(body.get("grant_type")).toBe("refresh_token");
|
||||
expect(body.get("refresh_token")).toBe("refresh123");
|
||||
});
|
||||
|
||||
it("fetches AS metadata with path from serverUrl when PRM returns external AS", async () => {
|
||||
// Mock PRM discovery that returns an external AS
|
||||
mockFetch.mockImplementation((url) => {
|
||||
const urlString = url.toString();
|
||||
|
||||
if (urlString === "https://my.resource.com/.well-known/oauth-protected-resource") {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
resource: "https://my.resource.com/",
|
||||
authorization_servers: ["https://auth.example.com/"],
|
||||
}),
|
||||
});
|
||||
} else if (urlString === "https://auth.example.com/.well-known/oauth-authorization-server/path/name") {
|
||||
// Path-aware discovery on AS with path from serverUrl
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
issuer: "https://auth.example.com",
|
||||
authorization_endpoint: "https://auth.example.com/authorize",
|
||||
token_endpoint: "https://auth.example.com/token",
|
||||
response_types_supported: ["code"],
|
||||
code_challenge_methods_supported: ["S256"],
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve({ ok: false, status: 404 });
|
||||
});
|
||||
|
||||
// Mock provider methods
|
||||
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
|
||||
client_id: "test-client",
|
||||
client_secret: "test-secret",
|
||||
});
|
||||
(mockProvider.tokens as jest.Mock).mockResolvedValue(undefined);
|
||||
(mockProvider.saveCodeVerifier as jest.Mock).mockResolvedValue(undefined);
|
||||
(mockProvider.redirectToAuthorization as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
// Call auth with serverUrl that has a path
|
||||
const result = await auth(mockProvider, {
|
||||
serverUrl: "https://my.resource.com/path/name",
|
||||
});
|
||||
|
||||
expect(result).toBe("REDIRECT");
|
||||
|
||||
// Verify the correct URLs were fetched
|
||||
const calls = mockFetch.mock.calls;
|
||||
|
||||
// First call should be to PRM
|
||||
expect(calls[0][0].toString()).toBe("https://my.resource.com/.well-known/oauth-protected-resource");
|
||||
|
||||
// Second call should be to AS metadata with the path from serverUrl
|
||||
expect(calls[1][0].toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server/path/name");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -122,7 +122,9 @@ export async function auth(
|
|||
|
||||
const resource: URL | undefined = await selectResourceURL(serverUrl, provider, resourceMetadata);
|
||||
|
||||
const metadata = await discoverOAuthMetadata(authorizationServerUrl);
|
||||
const metadata = await discoverOAuthMetadata(serverUrl, {
|
||||
authorizationServerUrl
|
||||
});
|
||||
|
||||
// Handle client registration if needed
|
||||
let clientInformation = await Promise.resolve(provider.clientInformation());
|
||||
|
|
@ -354,15 +356,27 @@ function shouldAttemptFallback(response: Response | undefined, pathname: string)
|
|||
* return `undefined`. Any other errors will be thrown as exceptions.
|
||||
*/
|
||||
export async function discoverOAuthMetadata(
|
||||
authorizationServerUrl: string | URL,
|
||||
opts?: { protocolVersion?: string },
|
||||
issuer: string | URL,
|
||||
{
|
||||
authorizationServerUrl
|
||||
}: {
|
||||
authorizationServerUrl?: string | URL
|
||||
} = {},
|
||||
): Promise<OAuthMetadata | undefined> {
|
||||
const issuer = new URL(authorizationServerUrl);
|
||||
const protocolVersion = opts?.protocolVersion ?? LATEST_PROTOCOL_VERSION;
|
||||
if (typeof issuer === 'string') {
|
||||
issuer = new URL(issuer);
|
||||
}
|
||||
if (!authorizationServerUrl) {
|
||||
authorizationServerUrl = issuer;
|
||||
}
|
||||
if (typeof authorizationServerUrl === 'string') {
|
||||
authorizationServerUrl = new URL(authorizationServerUrl);
|
||||
}
|
||||
protocolVersion ??= LATEST_PROTOCOL_VERSION;
|
||||
|
||||
// Try path-aware discovery first (RFC 8414 compliant)
|
||||
const wellKnownPath = buildWellKnownPath(issuer.pathname);
|
||||
const pathAwareUrl = new URL(wellKnownPath, issuer);
|
||||
const pathAwareUrl = new URL(wellKnownPath, authorizationServerUrl);
|
||||
let response = await tryMetadataDiscovery(pathAwareUrl, protocolVersion);
|
||||
|
||||
// If path-aware discovery fails with 404, try fallback to root discovery
|
||||
|
|
|
|||
Loading…
Reference in a new issue