Merge pull request #752 from modelcontextprotocol/ochafik/fix-path-as

auth: fetch AS metadata in well-known subpath from serverUrl even when PRM returns external AS
This commit is contained in:
Inna Harper 2025-07-10 14:53:02 +01:00 • committed by GitHub
commit 9dfd025eb2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 87 additions and 7 deletions

View file

@ -1747,6 +1747,64 @@ 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/path/name") {
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/path/name");
// 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");
});
});
describe("exchangeAuthorization with multiple client authentication methods", () => {

View file

@ -251,7 +251,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());
@ -469,7 +471,7 @@ function shouldAttemptFallback(response: Response | undefined, pathname: string)
async function discoverMetadataWithFallback(
serverUrl: string | URL,
wellKnownType: 'oauth-authorization-server' | 'oauth-protected-resource',
opts?: { protocolVersion?: string; metadataUrl?: string | URL },
opts?: { protocolVersion?: string; metadataUrl?: string | URL, metadataServerUrl?: string | URL },
): Promise<Response | undefined> {
const issuer = new URL(serverUrl);
const protocolVersion = opts?.protocolVersion ?? LATEST_PROTOCOL_VERSION;
@ -480,7 +482,7 @@ async function discoverMetadataWithFallback(
} else {
// Try path-aware discovery first
const wellKnownPath = buildWellKnownPath(wellKnownType, issuer.pathname);
url = new URL(wellKnownPath, issuer);
url = new URL(wellKnownPath, opts?.metadataServerUrl ?? issuer);
url.search = issuer.search;
}
@ -502,13 +504,33 @@ async function discoverMetadataWithFallback(
* return `undefined`. Any other errors will be thrown as exceptions.
*/
export async function discoverOAuthMetadata(
authorizationServerUrl: string | URL,
opts?: { protocolVersion?: string },
): Promise<OAuthMetadata | undefined> {
const response = await discoverMetadataWithFallback(
issuer: string | URL,
{
authorizationServerUrl,
protocolVersion,
}: {
authorizationServerUrl?: string | URL,
protocolVersion?: string,
} = {},
): Promise<OAuthMetadata | undefined> {
if (typeof issuer === 'string') {
issuer = new URL(issuer);
}
if (!authorizationServerUrl) {
authorizationServerUrl = issuer;
}
if (typeof authorizationServerUrl === 'string') {
authorizationServerUrl = new URL(authorizationServerUrl);
}
protocolVersion ??= LATEST_PROTOCOL_VERSION;
const response = await discoverMetadataWithFallback(
issuer,
'oauth-authorization-server',
opts,
{
protocolVersion,
metadataServerUrl: authorizationServerUrl,
},
);
if (!response || response.status === 404) {