Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions client/src/components/OAuthFlowProgress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useEffect, useMemo, useState } from "react";
import { OAuthClientInformation } from "@modelcontextprotocol/sdk/shared/auth.js";
import { validateRedirectUrl } from "@/utils/urlValidation";
import { useToast } from "@/lib/hooks/useToast";
import { getAuthorizationServerMetadataDiscoveryUrl } from "@/utils/oauthUtils";

interface OAuthStepProps {
label: string;
Expand Down Expand Up @@ -81,6 +82,13 @@ export const OAuthFlowProgress = ({
const [clientInfo, setClientInfo] = useState<OAuthClientInformation | null>(
null,
);
const authorizationServerMetadataDiscoveryUrl = useMemo(() => {
if (!authState.authServerUrl) {
return null;
}

return getAuthorizationServerMetadataDiscoveryUrl(authState.authServerUrl);
}, [authState.authServerUrl]);

const currentStepIdx = steps.findIndex((s) => s === authState.oauthStep);

Expand Down Expand Up @@ -197,13 +205,7 @@ export const OAuthFlowProgress = ({
<p className="font-medium">Authorization Server Metadata:</p>
{authState.authServerUrl && (
<p className="text-xs text-muted-foreground">
From{" "}
{
new URL(
"/.well-known/oauth-authorization-server",
authState.authServerUrl,
).href
}
From {authorizationServerMetadataDiscoveryUrl}
</p>
)}
<pre className="mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]">
Expand Down
10 changes: 8 additions & 2 deletions client/src/components/__tests__/AuthDebugger.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,7 @@ describe("AuthDebugger", () => {
const updateAuthState = jest.fn();
const mockResourceMetadata = {
resource: "https://example.com/mcp",
authorization_servers: ["https://custom-auth.example.com"],
authorization_servers: ["https://custom-auth.example.com/mcp/tenant"],
bearer_methods_supported: ["header", "body"],
resource_documentation: "https://example.com/mcp/docs",
resource_policy_uri: "https://example.com/mcp/policy",
Expand Down Expand Up @@ -733,11 +733,17 @@ describe("AuthDebugger", () => {
expect(updateAuthState).toHaveBeenCalledWith(
expect.objectContaining({
resourceMetadata: mockResourceMetadata,
authServerUrl: new URL("https://custom-auth.example.com"),
authServerUrl: new URL(
"https://custom-auth.example.com/mcp/tenant",
),
oauthStep: "client_registration",
}),
);
});

expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledWith(
new URL("https://custom-auth.example.com/mcp/tenant"),
);
});

it("should handle protected resource metadata fetch failure gracefully", async () => {
Expand Down
27 changes: 27 additions & 0 deletions client/src/utils/__tests__/oauthUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
generateOAuthErrorDescription,
parseOAuthCallbackParams,
generateOAuthState,
getAuthorizationServerMetadataDiscoveryUrl,
} from "@/utils/oauthUtils.ts";

describe("parseOAuthCallbackParams", () => {
Expand Down Expand Up @@ -84,3 +85,29 @@ describe("generateOAuthErrorDescription", () => {
});
});
});

describe("getAuthorizationServerMetadataDiscoveryUrl", () => {
it("uses root discovery URL for root authorization server URL", () => {
expect(
getAuthorizationServerMetadataDiscoveryUrl("https://example.com"),
).toBe("https://example.com/.well-known/oauth-authorization-server");
});

it("inserts tenant path for non-root authorization server URL", () => {
expect(
getAuthorizationServerMetadataDiscoveryUrl("https://example.com/tenant1"),
).toBe(
"https://example.com/.well-known/oauth-authorization-server/tenant1",
);
});

it("strips trailing slash before appending tenant path", () => {
expect(
getAuthorizationServerMetadataDiscoveryUrl(
"https://example.com/tenant1/",
),
).toBe(
"https://example.com/.well-known/oauth-authorization-server/tenant1",
);
});
});
28 changes: 28 additions & 0 deletions client/src/utils/oauthUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,31 @@ export const generateOAuthErrorDescription = (
.filter(Boolean)
.join("\n");
};

/**
* Returns the primary OAuth authorization server metadata discovery URL
* for a given authorization server URL, including tenant path handling.
*/
export const getAuthorizationServerMetadataDiscoveryUrl = (
authorizationServerUrl: string | URL,
): string => {
const url =
typeof authorizationServerUrl === "string"
? new URL(authorizationServerUrl)
: authorizationServerUrl;
const hasPath = url.pathname !== "/";

if (!hasPath) {
return new URL("/.well-known/oauth-authorization-server", url.origin).href;
}

// Strip trailing slash to avoid double slashes in tenant-aware discovery URLs.
const pathname = url.pathname.endsWith("/")
? url.pathname.slice(0, -1)
: url.pathname;

return new URL(
`/.well-known/oauth-authorization-server${pathname}`,
url.origin,
).href;
};