diff --git a/client/src/components/OAuthFlowProgress.tsx b/client/src/components/OAuthFlowProgress.tsx index b9b67c6c..c8167882 100644 --- a/client/src/components/OAuthFlowProgress.tsx +++ b/client/src/components/OAuthFlowProgress.tsx @@ -4,6 +4,7 @@ import { Button } from "./ui/button"; import { DebugInspectorOAuthClientProvider } from "@/lib/auth"; import { useEffect, useMemo, useState } from "react"; import { OAuthClientInformation } from "@modelcontextprotocol/sdk/shared/auth.js"; +import { oauthAuthServerMetadataUrl } from "@/utils/oauthUtils.ts"; interface OAuthStepProps { label: string; @@ -196,10 +197,7 @@ export const OAuthFlowProgress = ({
From{" "} { - new URL( - "/.well-known/oauth-authorization-server", - authState.authServerUrl, - ).href + oauthAuthServerMetadataUrl(authState.authServerUrl).href }
)} diff --git a/client/src/utils/__tests__/oauthUtils.ts b/client/src/utils/__tests__/oauthUtils.ts index cc9674cb..295324d2 100644 --- a/client/src/utils/__tests__/oauthUtils.ts +++ b/client/src/utils/__tests__/oauthUtils.ts @@ -1,6 +1,7 @@ import { generateOAuthErrorDescription, parseOAuthCallbackParams, + oauthAuthServerMetadataUrl, } from "@/utils/oauthUtils.ts"; describe("parseOAuthCallbackParams", () => { @@ -76,3 +77,37 @@ describe("generateOAuthErrorDescription", () => { ); }); }); + +describe("oauthAuthServerMetadataUrl", () => { + it("Returns metadata URL for simple auth server URL", () => { + const input = new URL("https://auth.example.com"); + const result = oauthAuthServerMetadataUrl(input); + expect(result.href).toBe( + "https://auth.example.com/.well-known/oauth-authorization-server", + ); + }); + + it("Returns metadata URL for auth server with path", () => { + const input = new URL("https://auth.example.com/oauth/tenant/xyz"); + const result = oauthAuthServerMetadataUrl(input); + expect(result.href).toBe( + "https://auth.example.com/.well-known/oauth-authorization-server/oauth/tenant/xyz", + ); + }); + + it("Strips trailing slash from path as per spec", () => { + const input = new URL("https://auth.example.com/oauth/"); + const result = oauthAuthServerMetadataUrl(input); + expect(result.href).toBe( + "https://auth.example.com/.well-known/oauth-authorization-server/oauth", + ); + }); + + it("Handles auth server URL with port", () => { + const input = new URL("https://auth.example.com:8080"); + const result = oauthAuthServerMetadataUrl(input); + expect(result.href).toBe( + "https://auth.example.com:8080/.well-known/oauth-authorization-server", + ); + }); +}); diff --git a/client/src/utils/oauthUtils.ts b/client/src/utils/oauthUtils.ts index c971271e..81ace104 100644 --- a/client/src/utils/oauthUtils.ts +++ b/client/src/utils/oauthUtils.ts @@ -63,3 +63,19 @@ export const generateOAuthErrorDescription = ( .filter(Boolean) .join("\n"); }; + +/** + * Build the well-known OAuth 2.0 metadata URL from an authServerUrl. + * Handles auth server paths per RFC 8414 ยง3.1. + * + * @param {URL} authServerUrl e.g. new URL("https://my.auth-server.com/oauth/tenant/xyz") + * @returns {URL} e.g. new URL("https://my.auth-server.com/.well-known/oauth-authorization-server/oauth/tenant/xyz") + */ +export const oauthAuthServerMetadataUrl = (authServerUrl: URL): URL => { + // Strip a trailing slash from the path (required by the spec) + const path = authServerUrl.pathname.replace(/\/$/, ""); + + return new URL( + `${authServerUrl.origin}/.well-known/oauth-authorization-server${path}`, + ); +};