oauth: add tenant suffix to oauth flow (#1096)
This commit is contained in:
parent
f2ed4ef3b9
commit
695972a404
4 changed files with 72 additions and 9 deletions
|
|
@ -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]">
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue