Skip to content

Commit 3f12a8f

Browse files
committed
Add support for OAuth via the proxy backend
1 parent 452d8dd commit 3f12a8f

24 files changed

+1761
-103
lines changed

client/src/App.tsx

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ import { OAuthTokensSchema } from "@modelcontextprotocol/sdk/shared/auth.js";
2121
import { SESSION_KEYS, getServerSpecificKey } from "./lib/constants";
2222
import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "./lib/auth-types";
2323
import { OAuthStateMachine } from "./lib/oauth-state-machine";
24+
import {
25+
createOAuthProviderForServer,
26+
setOAuthMode,
27+
} from "./lib/oauth/provider-factory";
2428
import { cacheToolOutputSchemas } from "./utils/schemaUtils";
2529
import { cleanParams } from "./utils/paramUtils";
2630
import type { JsonSchemaType } from "./utils/jsonUtils";
@@ -145,6 +149,12 @@ const App = () => {
145149
return localStorage.getItem("lastOauthClientSecret") || "";
146150
});
147151

152+
const [oauthMode, setOauthMode] = useState<"direct" | "proxy">(() => {
153+
return (
154+
(localStorage.getItem("lastOauthMode") as "direct" | "proxy") || "direct"
155+
);
156+
});
157+
148158
// Custom headers state with migration from legacy auth
149159
const [customHeaders, setCustomHeaders] = useState<CustomHeaders>(() => {
150160
const savedHeaders = localStorage.getItem("lastCustomHeaders");
@@ -399,6 +409,18 @@ const App = () => {
399409
localStorage.setItem("lastOauthScope", oauthScope);
400410
}, [oauthScope]);
401411

412+
useEffect(() => {
413+
localStorage.setItem("lastOauthMode", oauthMode);
414+
}, [oauthMode]);
415+
416+
// Sync OAuth mode to sessionStorage when server URL changes
417+
useEffect(() => {
418+
if (sseUrl) {
419+
const key = getServerSpecificKey(SESSION_KEYS.OAUTH_MODE, sseUrl);
420+
sessionStorage.setItem(key, oauthMode);
421+
}
422+
}, [sseUrl, oauthMode]);
423+
402424
useEffect(() => {
403425
localStorage.setItem("lastOauthClientSecret", oauthClientSecret);
404426
}, [oauthClientSecret]);
@@ -446,9 +468,24 @@ const App = () => {
446468
};
447469

448470
try {
449-
const stateMachine = new OAuthStateMachine(sseUrl, (updates) => {
450-
currentState = { ...currentState, ...updates };
451-
});
471+
// Set the OAuth mode in sessionStorage before creating the provider
472+
setOAuthMode(oauthMode, sseUrl);
473+
474+
const proxyAddress = getMCPProxyAddress(config);
475+
const proxyAuthObj = getMCPProxyAuthToken(config);
476+
const oauthProvider = createOAuthProviderForServer(
477+
sseUrl,
478+
proxyAddress,
479+
proxyAuthObj.token,
480+
);
481+
482+
const stateMachine = new OAuthStateMachine(
483+
sseUrl,
484+
(updates) => {
485+
currentState = { ...currentState, ...updates };
486+
},
487+
oauthProvider,
488+
);
452489

453490
while (
454491
currentState.oauthStep !== "complete" &&
@@ -486,7 +523,7 @@ const App = () => {
486523
});
487524
}
488525
},
489-
[sseUrl],
526+
[sseUrl, oauthMode, config, connectMcpServer],
490527
);
491528

492529
useEffect(() => {
@@ -854,6 +891,8 @@ const App = () => {
854891
onBack={() => setIsAuthDebuggerVisible(false)}
855892
authState={authState}
856893
updateAuthState={updateAuthState}
894+
config={config}
895+
oauthMode={oauthMode}
857896
/>
858897
</TabsContent>
859898
);
@@ -913,6 +952,8 @@ const App = () => {
913952
setOauthClientSecret={setOauthClientSecret}
914953
oauthScope={oauthScope}
915954
setOauthScope={setOauthScope}
955+
oauthMode={oauthMode}
956+
setOauthMode={setOauthMode}
916957
onConnect={connectMcpServer}
917958
onDisconnect={disconnectMcpServer}
918959
logLevel={logLevel}

client/src/components/AuthDebugger.tsx

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,20 @@ import { OAuthFlowProgress } from "./OAuthFlowProgress";
77
import { OAuthStateMachine } from "../lib/oauth-state-machine";
88
import { SESSION_KEYS } from "../lib/constants";
99
import { validateRedirectUrl } from "@/utils/urlValidation";
10+
import {
11+
createOAuthProviderForServer,
12+
setOAuthMode,
13+
} from "../lib/oauth/provider-factory";
14+
import { InspectorConfig } from "../lib/configurationTypes";
15+
import { getMCPProxyAddress, getMCPProxyAuthToken } from "@/utils/configUtils";
1016

1117
export interface AuthDebuggerProps {
1218
serverUrl: string;
1319
onBack: () => void;
1420
authState: AuthDebuggerState;
1521
updateAuthState: (updates: Partial<AuthDebuggerState>) => void;
22+
config: InspectorConfig;
23+
oauthMode: "direct" | "proxy";
1624
}
1725

1826
interface StatusMessageProps {
@@ -60,7 +68,23 @@ const AuthDebugger = ({
6068
onBack,
6169
authState,
6270
updateAuthState,
71+
config,
72+
oauthMode,
6373
}: AuthDebuggerProps) => {
74+
// Create OAuth provider based on mode with proxy credentials
75+
const oauthProvider = useMemo(() => {
76+
// Set the OAuth mode in sessionStorage before creating the provider
77+
setOAuthMode(oauthMode, serverUrl);
78+
79+
const proxyAddress = getMCPProxyAddress(config);
80+
const proxyAuthObj = getMCPProxyAuthToken(config);
81+
return createOAuthProviderForServer(
82+
serverUrl,
83+
proxyAddress,
84+
proxyAuthObj.token,
85+
);
86+
}, [serverUrl, config, oauthMode]);
87+
6488
// Check for existing tokens on mount
6589
useEffect(() => {
6690
if (serverUrl && !authState.oauthTokens) {
@@ -103,8 +127,8 @@ const AuthDebugger = ({
103127
}, [serverUrl, updateAuthState]);
104128

105129
const stateMachine = useMemo(
106-
() => new OAuthStateMachine(serverUrl, updateAuthState),
107-
[serverUrl, updateAuthState],
130+
() => new OAuthStateMachine(serverUrl, updateAuthState, oauthProvider),
131+
[serverUrl, updateAuthState, oauthProvider],
108132
);
109133

110134
const proceedToNextStep = useCallback(async () => {
@@ -150,11 +174,15 @@ const AuthDebugger = ({
150174
latestError: null,
151175
};
152176

153-
const oauthMachine = new OAuthStateMachine(serverUrl, (updates) => {
154-
// Update our temporary state during the process
155-
currentState = { ...currentState, ...updates };
156-
// But don't call updateAuthState yet
157-
});
177+
const oauthMachine = new OAuthStateMachine(
178+
serverUrl,
179+
(updates) => {
180+
// Update our temporary state during the process
181+
currentState = { ...currentState, ...updates };
182+
// But don't call updateAuthState yet
183+
},
184+
oauthProvider,
185+
);
158186

159187
// Manually step through each stage of the OAuth flow
160188
while (currentState.oauthStep !== "complete") {

client/src/components/OAuthCallback.tsx

Lines changed: 97 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ import {
77
generateOAuthErrorDescription,
88
parseOAuthCallbackParams,
99
} from "@/utils/oauthUtils.ts";
10+
import { createOAuthProviderForServer } from "../lib/oauth/provider-factory";
11+
import { OAuthStateMachine } from "../lib/oauth-state-machine";
12+
import { AuthDebuggerState } from "../lib/auth-types";
13+
import {
14+
getMCPProxyAddress,
15+
getMCPProxyAuthToken,
16+
initializeInspectorConfig,
17+
} from "@/utils/configUtils";
1018

1119
interface OAuthCallbackProps {
1220
onConnect: (serverUrl: string) => void;
@@ -41,24 +49,97 @@ const OAuthCallback = ({ onConnect }: OAuthCallbackProps) => {
4149
return notifyError("Missing Server URL");
4250
}
4351

44-
let result;
45-
try {
46-
// Create an auth provider with the current server URL
47-
const serverAuthProvider = new InspectorOAuthClientProvider(serverUrl);
52+
// Check if there's stored auth state (for proxy mode from Connect button)
53+
const storedAuthState = sessionStorage.getItem(
54+
SESSION_KEYS.AUTH_STATE_FOR_CONNECT,
55+
);
4856

49-
result = await auth(serverAuthProvider, {
50-
serverUrl,
51-
authorizationCode: params.code,
52-
});
53-
} catch (error) {
54-
console.error("OAuth callback error:", error);
55-
return notifyError(`Unexpected error occurred: ${error}`);
56-
}
57+
if (storedAuthState) {
58+
// Proxy mode: Complete the OAuth flow using the state machine
59+
try {
60+
let restoredState: AuthDebuggerState = JSON.parse(storedAuthState);
61+
62+
// Restore URL objects
63+
if (
64+
restoredState.resource &&
65+
typeof restoredState.resource === "string"
66+
) {
67+
restoredState.resource = new URL(restoredState.resource);
68+
}
69+
if (
70+
restoredState.authorizationUrl &&
71+
typeof restoredState.authorizationUrl === "string"
72+
) {
73+
restoredState.authorizationUrl = new URL(
74+
restoredState.authorizationUrl,
75+
);
76+
}
77+
78+
// Set up state with the authorization code
79+
let currentState: AuthDebuggerState = {
80+
...restoredState,
81+
authorizationCode: params.code,
82+
oauthStep: "token_request",
83+
};
84+
85+
// Get config and create provider
86+
// Use the same config key and initialization as App.tsx
87+
const config = initializeInspectorConfig("inspectorConfig_v1");
88+
89+
const proxyAddress = getMCPProxyAddress(config);
90+
const proxyAuthObj = getMCPProxyAuthToken(config);
91+
92+
const oauthProvider = createOAuthProviderForServer(
93+
serverUrl,
94+
proxyAddress,
95+
proxyAuthObj.token,
96+
);
97+
98+
const stateMachine = new OAuthStateMachine(
99+
serverUrl,
100+
(updates) => {
101+
currentState = { ...currentState, ...updates };
102+
},
103+
oauthProvider,
104+
false, // use regular redirect URL
105+
);
106+
107+
// Complete the token exchange
108+
await stateMachine.executeStep(currentState);
109+
110+
if (currentState.oauthStep !== "complete") {
111+
return notifyError("Failed to complete OAuth token exchange");
112+
}
113+
114+
// Clean up stored state
115+
sessionStorage.removeItem(SESSION_KEYS.AUTH_STATE_FOR_CONNECT);
116+
} catch (error) {
117+
console.error("Proxy OAuth callback error:", error);
118+
sessionStorage.removeItem(SESSION_KEYS.AUTH_STATE_FOR_CONNECT);
119+
return notifyError(`Failed to complete proxy OAuth: ${error}`);
120+
}
121+
} else {
122+
// Direct mode: Use SDK's auth() function
123+
let result;
124+
try {
125+
const serverAuthProvider = new InspectorOAuthClientProvider(
126+
serverUrl,
127+
);
128+
129+
result = await auth(serverAuthProvider, {
130+
serverUrl,
131+
authorizationCode: params.code,
132+
});
133+
} catch (error) {
134+
console.error("OAuth callback error:", error);
135+
return notifyError(`Unexpected error occurred: ${error}`);
136+
}
57137

58-
if (result !== "AUTHORIZED") {
59-
return notifyError(
60-
`Expected to be authorized after providing auth code, got: ${result}`,
61-
);
138+
if (result !== "AUTHORIZED") {
139+
return notifyError(
140+
`Expected to be authorized after providing auth code, got: ${result}`,
141+
);
142+
}
62143
}
63144

64145
// Finally, trigger auto-connect

client/src/components/Sidebar.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ interface SidebarProps {
6262
setOauthClientSecret: (secret: string) => void;
6363
oauthScope: string;
6464
setOauthScope: (scope: string) => void;
65+
oauthMode: "direct" | "proxy";
66+
setOauthMode: (mode: "direct" | "proxy") => void;
6567
onConnect: () => void;
6668
onDisconnect: () => void;
6769
logLevel: LoggingLevel;
@@ -93,6 +95,8 @@ const Sidebar = ({
9395
setOauthClientSecret,
9496
oauthScope,
9597
setOauthScope,
98+
oauthMode,
99+
setOauthMode,
96100
onConnect,
97101
onDisconnect,
98102
logLevel,
@@ -552,6 +556,43 @@ const Sidebar = ({
552556
OAuth 2.0 Flow
553557
</h4>
554558
<div className="space-y-2">
559+
<div className="flex items-center justify-between">
560+
<label className="text-sm font-medium">
561+
OAuth Mode
562+
</label>
563+
<Tooltip>
564+
<TooltipTrigger asChild>
565+
<HelpCircle className="h-4 w-4 text-muted-foreground" />
566+
</TooltipTrigger>
567+
<TooltipContent className="max-w-xs">
568+
<p className="text-xs">
569+
<strong>Direct:</strong> Browser-based OAuth (may
570+
encounter CORS issues)
571+
<br />
572+
<strong>Via Proxy:</strong> Backend-proxied OAuth
573+
to avoid CORS
574+
</p>
575+
</TooltipContent>
576+
</Tooltip>
577+
</div>
578+
<Select
579+
value={oauthMode}
580+
onValueChange={(value: "direct" | "proxy") =>
581+
setOauthMode(value)
582+
}
583+
>
584+
<SelectTrigger data-testid="oauth-mode-select">
585+
<SelectValue placeholder="Select OAuth mode" />
586+
</SelectTrigger>
587+
<SelectContent>
588+
<SelectItem value="direct">
589+
Direct (browser-based)
590+
</SelectItem>
591+
<SelectItem value="proxy">
592+
Via Proxy (avoids CORS)
593+
</SelectItem>
594+
</SelectContent>
595+
</Select>
555596
<label className="text-sm font-medium">Client ID</label>
556597
<Input
557598
placeholder="Client ID"

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import "@testing-library/jest-dom";
99
import { describe, it, beforeEach, jest } from "@jest/globals";
1010
import AuthDebugger, { AuthDebuggerProps } from "../AuthDebugger";
1111
import { TooltipProvider } from "@/components/ui/tooltip";
12-
import { SESSION_KEYS } from "@/lib/constants";
12+
import { SESSION_KEYS, DEFAULT_INSPECTOR_CONFIG } from "@/lib/constants";
1313

1414
const mockOAuthTokens = {
1515
access_token: "test_access_token",
@@ -149,6 +149,8 @@ describe("AuthDebugger", () => {
149149
onBack: jest.fn(),
150150
authState: defaultAuthState,
151151
updateAuthState: jest.fn(),
152+
config: DEFAULT_INSPECTOR_CONFIG,
153+
oauthMode: "direct" as const,
152154
};
153155

154156
beforeEach(() => {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ describe("Sidebar", () => {
4848
setOauthClientSecret: jest.fn(),
4949
oauthScope: "",
5050
setOauthScope: jest.fn(),
51+
oauthMode: "direct" as const,
52+
setOauthMode: jest.fn(),
5153
env: {},
5254
setEnv: jest.fn(),
5355
customHeaders: [],

0 commit comments

Comments
 (0)