inspector/client/src/components/OAuthFlowProgress.tsx
Jenn Newton fa2e6b0c40
Fix to properly open in new tab for OAuth
🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-21 15:31:53 +00:00

398 lines
14 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { AuthDebuggerState, OAuthStep } from "@/lib/auth-types";
import { CheckCircle2, Circle, ExternalLink } from "lucide-react";
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 { validateRedirectUrl } from "@/utils/urlValidation";
import { useToast } from "@/lib/hooks/useToast";
interface OAuthStepProps {
label: string;
isComplete: boolean;
isCurrent: boolean;
error?: Error | null;
children?: React.ReactNode;
}
const OAuthStepDetails = ({
label,
isComplete,
isCurrent,
error,
children,
}: OAuthStepProps) => {
return (
<div>
<div
className={`flex items-center p-2 rounded-md ${isCurrent ? "bg-accent" : ""}`}
>
{isComplete ? (
<CheckCircle2 className="h-5 w-5 text-green-500 mr-2" />
) : (
<Circle className="h-5 w-5 text-muted-foreground mr-2" />
)}
<span className={`${isCurrent ? "font-medium" : ""}`}>{label}</span>
</div>
{/* Show children if current step or complete and children exist */}
{(isCurrent || isComplete) && children && (
<div className="ml-7 mt-1">{children}</div>
)}
{/* Display error if current step and an error exists */}
{isCurrent && error && (
<div className="ml-7 mt-2 p-3 border border-red-300 bg-red-50 rounded-md">
<p className="text-sm font-medium text-red-700">Error:</p>
<p className="text-xs text-red-600 mt-1">{error.message}</p>
</div>
)}
</div>
);
};
interface OAuthFlowProgressProps {
serverUrl: string;
authState: AuthDebuggerState;
updateAuthState: (updates: Partial<AuthDebuggerState>) => void;
proceedToNextStep: () => Promise<void>;
}
const steps: Array<OAuthStep> = [
"metadata_discovery",
"client_registration",
"authorization_redirect",
"authorization_code",
"token_request",
"complete",
];
export const OAuthFlowProgress = ({
serverUrl,
authState,
updateAuthState,
proceedToNextStep,
}: OAuthFlowProgressProps) => {
const { toast } = useToast();
const provider = useMemo(
() => new DebugInspectorOAuthClientProvider(serverUrl),
[serverUrl],
);
const [clientInfo, setClientInfo] = useState<OAuthClientInformation | null>(
null,
);
const currentStepIdx = steps.findIndex((s) => s === authState.oauthStep);
useEffect(() => {
const fetchClientInfo = async () => {
if (authState.oauthClientInfo) {
setClientInfo(authState.oauthClientInfo);
} else {
try {
const info = await provider.clientInformation();
if (info) {
setClientInfo(info);
}
} catch (error) {
console.error("Failed to fetch client information:", error);
}
}
};
if (currentStepIdx > steps.indexOf("client_registration")) {
fetchClientInfo();
}
}, [
provider,
authState.oauthStep,
authState.oauthClientInfo,
currentStepIdx,
]);
// Helper to get step props
const getStepProps = (stepName: OAuthStep) => ({
isComplete:
currentStepIdx > steps.indexOf(stepName) ||
currentStepIdx === steps.length - 1, // last step is "complete"
isCurrent: authState.oauthStep === stepName,
error: authState.oauthStep === stepName ? authState.latestError : null,
});
return (
<div className="rounded-md border p-6 space-y-4 mt-4">
<h3 className="text-lg font-medium">OAuth Flow Progress</h3>
<p className="text-sm text-muted-foreground">
Follow these steps to complete OAuth authentication with the server.
</p>
<div className="space-y-3">
<OAuthStepDetails
label="Metadata Discovery"
{...getStepProps("metadata_discovery")}
>
{authState.oauthMetadata && (
<details className="text-xs mt-2">
<summary className="cursor-pointer text-muted-foreground font-medium">
OAuth Metadata Sources
{!authState.resourceMetadata && " ℹ️"}
</summary>
{authState.resourceMetadata && (
<div className="mt-2">
<p className="font-medium">Resource Metadata:</p>
<p className="text-xs text-muted-foreground">
From{" "}
{
new URL(
"/.well-known/oauth-protected-resource",
serverUrl,
).href
}
</p>
<pre className="mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]">
{JSON.stringify(authState.resourceMetadata, null, 2)}
</pre>
</div>
)}
{authState.resourceMetadataError && (
<div className="mt-2 p-3 border border-blue-300 bg-blue-50 rounded-md">
<p className="text-sm font-medium text-blue-700">
ℹ️ Problem with resource metadata from{" "}
<a
href={
new URL(
"/.well-known/oauth-protected-resource",
serverUrl,
).href
}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:text-blue-700"
>
{
new URL(
"/.well-known/oauth-protected-resource",
serverUrl,
).href
}
</a>
</p>
<p className="text-xs text-blue-600 mt-1">
Resource metadata was added in the{" "}
<a href="https://modelcontextprotocol.io/specification/draft/basic/authorization#2-3-1-authorization-server-location">
2025-DRAFT-v2 specification update
</a>
<br />
{authState.resourceMetadataError.message}
{authState.resourceMetadataError instanceof TypeError &&
" (This could indicate the endpoint doesn't exist or does not have CORS configured)"}
</p>
</div>
)}
{authState.oauthMetadata && (
<div className="mt-2">
<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
}
</p>
)}
<pre className="mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]">
{JSON.stringify(authState.oauthMetadata, null, 2)}
</pre>
</div>
)}
</details>
)}
</OAuthStepDetails>
<OAuthStepDetails
label="Client Registration"
{...getStepProps("client_registration")}
>
{clientInfo && (
<details className="text-xs mt-2">
<summary className="cursor-pointer text-muted-foreground font-medium">
Registered Client Information
</summary>
<pre className="mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]">
{JSON.stringify(clientInfo, null, 2)}
</pre>
</details>
)}
</OAuthStepDetails>
<OAuthStepDetails
label="Preparing Authorization"
{...getStepProps("authorization_redirect")}
>
{authState.authorizationUrl && (
<div className="mt-2 p-3 border rounded-md bg-muted">
<p className="font-medium mb-2 text-sm">Authorization URL:</p>
<div className="flex items-center gap-2">
<p className="text-xs break-all">
{authState.authorizationUrl}
</p>
<button
onClick={() => {
try {
validateRedirectUrl(authState.authorizationUrl!);
window.open(
authState.authorizationUrl!,
"_blank",
"noopener noreferrer",
);
} catch (error) {
toast({
title: "Invalid URL",
description:
error instanceof Error
? error.message
: "The authorization URL is not valid",
variant: "destructive",
});
}
}}
className="flex items-center text-blue-500 hover:text-blue-700"
aria-label="Open authorization URL in new tab"
title="Open authorization URL"
>
<ExternalLink className="h-4 w-4" />
</button>
</div>
<p className="text-xs text-muted-foreground mt-2">
Click the link to authorize in your browser. After
authorization, you'll be redirected back to continue the flow.
</p>
</div>
)}
</OAuthStepDetails>
<OAuthStepDetails
label="Request Authorization and acquire authorization code"
{...getStepProps("authorization_code")}
>
<div className="mt-3">
<label
htmlFor="authCode"
className="block text-sm font-medium mb-1"
>
Authorization Code
</label>
<div className="flex gap-2">
<input
id="authCode"
value={authState.authorizationCode}
onChange={(e) => {
updateAuthState({
authorizationCode: e.target.value,
validationError: null,
});
}}
placeholder="Enter the code from the authorization server"
className={`flex h-9 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${
authState.validationError ? "border-red-500" : "border-input"
}`}
/>
</div>
{authState.validationError && (
<p className="text-xs text-red-600 mt-1">
{authState.validationError}
</p>
)}
<p className="text-xs text-muted-foreground mt-1">
Once you've completed authorization in the link, paste the code
here.
</p>
</div>
</OAuthStepDetails>
<OAuthStepDetails
label="Token Request"
{...getStepProps("token_request")}
>
{authState.oauthMetadata && (
<details className="text-xs mt-2">
<summary className="cursor-pointer text-muted-foreground font-medium">
Token Request Details
</summary>
<div className="mt-2 p-2 bg-muted rounded-md">
<p className="font-medium">Token Endpoint:</p>
<code className="block mt-1 text-xs overflow-x-auto">
{authState.oauthMetadata.token_endpoint}
</code>
</div>
</details>
)}
</OAuthStepDetails>
<OAuthStepDetails
label="Authentication Complete"
{...getStepProps("complete")}
>
{authState.oauthTokens && (
<details className="text-xs mt-2">
<summary className="cursor-pointer text-muted-foreground font-medium">
Access Tokens
</summary>
<p className="mt-1 text-sm">
Authentication successful! You can now use the authenticated
connection. These tokens will be used automatically for server
requests.
</p>
<pre className="mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]">
{JSON.stringify(authState.oauthTokens, null, 2)}
</pre>
</details>
)}
</OAuthStepDetails>
</div>
<div className="flex gap-3 mt-4">
{authState.oauthStep !== "complete" && (
<>
<Button
onClick={proceedToNextStep}
disabled={authState.isInitiatingAuth}
>
{authState.isInitiatingAuth ? "Processing..." : "Continue"}
</Button>
</>
)}
{authState.oauthStep === "authorization_redirect" &&
authState.authorizationUrl && (
<Button
variant="outline"
onClick={() => {
try {
validateRedirectUrl(authState.authorizationUrl!);
window.open(authState.authorizationUrl!, "_blank");
} catch (error) {
toast({
title: "Invalid URL",
description:
error instanceof Error
? error.message
: "The authorization URL is not valid",
variant: "destructive",
});
}
}}
>
Open in New Tab
</Button>
)}
</div>
</div>
);
};