Skip to content

Commit 695972a

Browse files
authored
oauth: add tenant suffix to oauth flow (#1096)
1 parent f2ed4ef commit 695972a

4 files changed

Lines changed: 72 additions & 9 deletions

File tree

client/src/components/OAuthFlowProgress.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useEffect, useMemo, useState } from "react";
66
import { OAuthClientInformation } from "@modelcontextprotocol/sdk/shared/auth.js";
77
import { validateRedirectUrl } from "@/utils/urlValidation";
88
import { useToast } from "@/lib/hooks/useToast";
9+
import { getAuthorizationServerMetadataDiscoveryUrl } from "@/utils/oauthUtils";
910

1011
interface OAuthStepProps {
1112
label: string;
@@ -81,6 +82,13 @@ export const OAuthFlowProgress = ({
8182
const [clientInfo, setClientInfo] = useState<OAuthClientInformation | null>(
8283
null,
8384
);
85+
const authorizationServerMetadataDiscoveryUrl = useMemo(() => {
86+
if (!authState.authServerUrl) {
87+
return null;
88+
}
89+
90+
return getAuthorizationServerMetadataDiscoveryUrl(authState.authServerUrl);
91+
}, [authState.authServerUrl]);
8492

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

@@ -197,13 +205,7 @@ export const OAuthFlowProgress = ({
197205
<p className="font-medium">Authorization Server Metadata:</p>
198206
{authState.authServerUrl && (
199207
<p className="text-xs text-muted-foreground">
200-
From{" "}
201-
{
202-
new URL(
203-
"/.well-known/oauth-authorization-server",
204-
authState.authServerUrl,
205-
).href
206-
}
208+
From {authorizationServerMetadataDiscoveryUrl}
207209
</p>
208210
)}
209211
<pre className="mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]">

client/src/components/__tests__/AuthDebugger.test.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -681,7 +681,7 @@ describe("AuthDebugger", () => {
681681
const updateAuthState = jest.fn();
682682
const mockResourceMetadata = {
683683
resource: "https://example.com/mcp",
684-
authorization_servers: ["https://custom-auth.example.com"],
684+
authorization_servers: ["https://custom-auth.example.com/mcp/tenant"],
685685
bearer_methods_supported: ["header", "body"],
686686
resource_documentation: "https://example.com/mcp/docs",
687687
resource_policy_uri: "https://example.com/mcp/policy",
@@ -733,11 +733,17 @@ describe("AuthDebugger", () => {
733733
expect(updateAuthState).toHaveBeenCalledWith(
734734
expect.objectContaining({
735735
resourceMetadata: mockResourceMetadata,
736-
authServerUrl: new URL("https://custom-auth.example.com"),
736+
authServerUrl: new URL(
737+
"https://custom-auth.example.com/mcp/tenant",
738+
),
737739
oauthStep: "client_registration",
738740
}),
739741
);
740742
});
743+
744+
expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledWith(
745+
new URL("https://custom-auth.example.com/mcp/tenant"),
746+
);
741747
});
742748

743749
it("should handle protected resource metadata fetch failure gracefully", async () => {

client/src/utils/__tests__/oauthUtils.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
generateOAuthErrorDescription,
33
parseOAuthCallbackParams,
44
generateOAuthState,
5+
getAuthorizationServerMetadataDiscoveryUrl,
56
} from "@/utils/oauthUtils.ts";
67

78
describe("parseOAuthCallbackParams", () => {
@@ -84,3 +85,29 @@ describe("generateOAuthErrorDescription", () => {
8485
});
8586
});
8687
});
88+
89+
describe("getAuthorizationServerMetadataDiscoveryUrl", () => {
90+
it("uses root discovery URL for root authorization server URL", () => {
91+
expect(
92+
getAuthorizationServerMetadataDiscoveryUrl("https://example.com"),
93+
).toBe("https://example.com/.well-known/oauth-authorization-server");
94+
});
95+
96+
it("inserts tenant path for non-root authorization server URL", () => {
97+
expect(
98+
getAuthorizationServerMetadataDiscoveryUrl("https://example.com/tenant1"),
99+
).toBe(
100+
"https://example.com/.well-known/oauth-authorization-server/tenant1",
101+
);
102+
});
103+
104+
it("strips trailing slash before appending tenant path", () => {
105+
expect(
106+
getAuthorizationServerMetadataDiscoveryUrl(
107+
"https://example.com/tenant1/",
108+
),
109+
).toBe(
110+
"https://example.com/.well-known/oauth-authorization-server/tenant1",
111+
);
112+
});
113+
});

client/src/utils/oauthUtils.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,31 @@ export const generateOAuthErrorDescription = (
8787
.filter(Boolean)
8888
.join("\n");
8989
};
90+
91+
/**
92+
* Returns the primary OAuth authorization server metadata discovery URL
93+
* for a given authorization server URL, including tenant path handling.
94+
*/
95+
export const getAuthorizationServerMetadataDiscoveryUrl = (
96+
authorizationServerUrl: string | URL,
97+
): string => {
98+
const url =
99+
typeof authorizationServerUrl === "string"
100+
? new URL(authorizationServerUrl)
101+
: authorizationServerUrl;
102+
const hasPath = url.pathname !== "/";
103+
104+
if (!hasPath) {
105+
return new URL("/.well-known/oauth-authorization-server", url.origin).href;
106+
}
107+
108+
// Strip trailing slash to avoid double slashes in tenant-aware discovery URLs.
109+
const pathname = url.pathname.endsWith("/")
110+
? url.pathname.slice(0, -1)
111+
: url.pathname;
112+
113+
return new URL(
114+
`/.well-known/oauth-authorization-server${pathname}`,
115+
url.origin,
116+
).href;
117+
};

0 commit comments

Comments
 (0)