oauth: add tenant suffix to oauth flow (#1096)

This commit is contained in:
John Howard 2026-02-19 05:00:33 -08:00 • committed by GitHub
commit 695972a404
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 72 additions and 9 deletions

View file

@ -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;
@ -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);
@ -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]">

View file

@ -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",
@ -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 () => {

View file

@ -2,6 +2,7 @@ import {
generateOAuthErrorDescription,
parseOAuthCallbackParams,
generateOAuthState,
getAuthorizationServerMetadataDiscoveryUrl,
} from "@/utils/oauthUtils.ts";
describe("parseOAuthCallbackParams", () => {
@ -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",
);
});
});

View file

@ -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;
};