diff --git a/client/src/App.tsx b/client/src/App.tsx index a2dc8f0ee..082a9ce61 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -128,6 +128,14 @@ const App = () => { return localStorage.getItem("lastHeaderName") || ""; }); + const [oauthClientId, setOauthClientId] = useState(() => { + return localStorage.getItem("lastOauthClientId") || ""; + }); + + const [oauthParams, setOauthParams] = useState(() => { + return localStorage.getItem("lastOauthParams") || ""; + }); + const [pendingSampleRequests, setPendingSampleRequests] = useState< Array< PendingRequest & { @@ -181,6 +189,8 @@ const App = () => { env, bearerToken, headerName, + oauthClientId, + oauthParams, config, onNotification: (notification) => { setNotifications((prev) => [...prev, notification as ServerNotification]); @@ -224,6 +234,14 @@ const App = () => { localStorage.setItem("lastHeaderName", headerName); }, [headerName]); + useEffect(() => { + localStorage.setItem("lastOauthClientId", oauthClientId); + }, [oauthClientId]); + + useEffect(() => { + localStorage.setItem("lastOauthParams", oauthParams); + }, [oauthParams]); + useEffect(() => { localStorage.setItem(CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config)); }, [config]); @@ -502,6 +520,10 @@ const App = () => { setBearerToken={setBearerToken} headerName={headerName} setHeaderName={setHeaderName} + oauthClientId={oauthClientId} + setOauthClientId={setOauthClientId} + oauthParams={oauthParams} + setOauthParams={setOauthParams} onConnect={connectMcpServer} onDisconnect={disconnectMcpServer} stdErrNotifications={stdErrNotifications} diff --git a/client/src/components/OAuthCallback.tsx b/client/src/components/OAuthCallback.tsx index 6bfa8a3bc..9c19e6957 100644 --- a/client/src/components/OAuthCallback.tsx +++ b/client/src/components/OAuthCallback.tsx @@ -1,5 +1,9 @@ import { useEffect, useRef } from "react"; -import { InspectorOAuthClientProvider } from "../lib/auth"; +import { + InspectorOAuthClientProvider, + getAuthParamsFromSessionStorage, + getClientInformationFromSessionStorage, +} from "../lib/auth"; import { SESSION_KEYS } from "../lib/constants"; import { auth } from "@modelcontextprotocol/sdk/client/auth.js"; import { useToast } from "@/hooks/use-toast.ts"; @@ -32,6 +36,9 @@ const OAuthCallback = ({ onConnect }: OAuthCallbackProps) => { }); const params = parseOAuthCallbackParams(window.location.search); + // Extract state from query params + const urlParams = new URLSearchParams(window.location.search); + const returnedState = urlParams.get("state"); if (!params.successful) { return notifyError(generateOAuthErrorDescription(params)); } @@ -41,10 +48,33 @@ const OAuthCallback = ({ onConnect }: OAuthCallbackProps) => { return notifyError("Missing Server URL"); } + // Validate state parameter + const serverAuthProvider = new InspectorOAuthClientProvider( + serverUrl, + undefined, + undefined, + ); + const expectedState = serverAuthProvider.getState(); + serverAuthProvider.clearState(); // Always clear after checking + if (!returnedState || !expectedState || returnedState !== expectedState) { + return notifyError( + "Invalid or missing OAuth state parameter. Please try logging in again.", + ); + } + + const clientInformation = + await getClientInformationFromSessionStorage(serverUrl); + + const authParams = getAuthParamsFromSessionStorage(serverUrl); + let result; try { // Create an auth provider with the current server URL - const serverAuthProvider = new InspectorOAuthClientProvider(serverUrl); + const serverAuthProvider = new InspectorOAuthClientProvider( + serverUrl, + clientInformation, + authParams, + ); result = await auth(serverAuthProvider, { serverUrl, diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index bc6af52f0..7a963f43f 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -36,6 +36,7 @@ import { TooltipTrigger, TooltipContent, } from "@/components/ui/tooltip"; +import { Textarea } from "./ui/textarea"; interface SidebarProps { connectionStatus: ConnectionStatus; @@ -53,6 +54,10 @@ interface SidebarProps { setBearerToken: (token: string) => void; headerName?: string; setHeaderName?: (name: string) => void; + oauthClientId: string; + setOauthClientId: (id: string) => void; + oauthParams: string; + setOauthParams: (params: string) => void; onConnect: () => void; onDisconnect: () => void; stdErrNotifications: StdErrNotification[]; @@ -80,6 +85,10 @@ const Sidebar = ({ setBearerToken, headerName, setHeaderName, + oauthClientId, + setOauthClientId, + oauthParams, + setOauthParams, onConnect, onDisconnect, stdErrNotifications, @@ -95,6 +104,7 @@ const Sidebar = ({ const [showBearerToken, setShowBearerToken] = useState(false); const [showConfig, setShowConfig] = useState(false); const [shownEnvVars, setShownEnvVars] = useState>(new Set()); + const [showOauthConfig, setShowOauthConfig] = useState(false); return (
@@ -221,6 +231,52 @@ const Sidebar = ({
)} + {/* OAuth Configuration */} +
+ + {showOauthConfig && ( +
+ + setOauthClientId(e.target.value)} + value={oauthClientId} + data-testid="oauth-client-id-input" + className="font-mono" + /> + + + +