Skip to content

Commit ce220f2

Browse files
committed
feat: integrate OAuth proxy support into OAuth flow
- Update OAuthStateMachine to accept connectionType and config parameters - Conditionally route OAuth operations through proxy when connectionType is 'proxy' - Pass connectionType and config from App.tsx through AuthDebugger to state machine - Fix auth.ts to make scope optional per RFC 7591 - Maintain backward compatibility with direct connections - Follow same pattern as existing MCP server proxy connections
1 parent b30e337 commit ce220f2

5 files changed

Lines changed: 135 additions & 36 deletions

File tree

client/src/App.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -499,9 +499,14 @@ const App = () => {
499499
};
500500

501501
try {
502-
const stateMachine = new OAuthStateMachine(sseUrl, (updates) => {
503-
currentState = { ...currentState, ...updates };
504-
});
502+
const stateMachine = new OAuthStateMachine(
503+
sseUrl,
504+
(updates) => {
505+
currentState = { ...currentState, ...updates };
506+
},
507+
connectionType,
508+
config,
509+
);
505510

506511
while (
507512
currentState.oauthStep !== "complete" &&
@@ -917,6 +922,8 @@ const App = () => {
917922
onBack={() => setIsAuthDebuggerVisible(false)}
918923
authState={authState}
919924
updateAuthState={updateAuthState}
925+
connectionType={connectionType}
926+
config={config}
920927
/>
921928
</TabsContent>
922929
);

client/src/components/AuthDebugger.tsx

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ 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 { InspectorConfig } from "../lib/configurationTypes";
1011

1112
export interface AuthDebuggerProps {
1213
serverUrl: string;
1314
onBack: () => void;
1415
authState: AuthDebuggerState;
1516
updateAuthState: (updates: Partial<AuthDebuggerState>) => void;
17+
connectionType: "direct" | "proxy";
18+
config: InspectorConfig;
1619
}
1720

1821
interface StatusMessageProps {
@@ -60,6 +63,8 @@ const AuthDebugger = ({
6063
onBack,
6164
authState,
6265
updateAuthState,
66+
connectionType,
67+
config,
6368
}: AuthDebuggerProps) => {
6469
// Check for existing tokens on mount
6570
useEffect(() => {
@@ -103,8 +108,9 @@ const AuthDebugger = ({
103108
}, [serverUrl, updateAuthState]);
104109

105110
const stateMachine = useMemo(
106-
() => new OAuthStateMachine(serverUrl, updateAuthState),
107-
[serverUrl, updateAuthState],
111+
() =>
112+
new OAuthStateMachine(serverUrl, updateAuthState, connectionType, config),
113+
[serverUrl, updateAuthState, connectionType, config],
108114
);
109115

110116
const proceedToNextStep = useCallback(async () => {
@@ -150,11 +156,16 @@ const AuthDebugger = ({
150156
latestError: null,
151157
};
152158

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-
});
159+
const oauthMachine = new OAuthStateMachine(
160+
serverUrl,
161+
(updates) => {
162+
// Update our temporary state during the process
163+
currentState = { ...currentState, ...updates };
164+
// But don't call updateAuthState yet
165+
},
166+
connectionType,
167+
config,
168+
);
158169

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

client/src/components/ui/button.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@ const buttonVariants = cva(
3535
);
3636

3737
export interface ButtonProps
38-
extends
39-
React.ButtonHTMLAttributes<HTMLButtonElement>,
38+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
4039
VariantProps<typeof buttonVariants> {
4140
asChild?: boolean;
4241
}

client/src/lib/auth.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,15 +153,21 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider {
153153
}
154154

155155
get clientMetadata(): OAuthClientMetadata {
156-
return {
156+
const metadata: OAuthClientMetadata = {
157157
redirect_uris: this.redirect_uris,
158158
token_endpoint_auth_method: "none",
159159
grant_types: ["authorization_code", "refresh_token"],
160160
response_types: ["code"],
161161
client_name: "MCP Inspector",
162162
client_uri: "https://github.com/modelcontextprotocol/inspector",
163-
scope: this.scope ?? "",
164163
};
164+
165+
// Only include scope if it has a value (RFC 7591 - scope is optional)
166+
if (this.scope) {
167+
metadata.scope = this.scope;
168+
}
169+
170+
return metadata;
165171
}
166172

167173
state(): string | Promise<string> {

client/src/lib/oauth-state-machine.ts

Lines changed: 98 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,21 @@ import {
1313
OAuthProtectedResourceMetadata,
1414
} from "@modelcontextprotocol/sdk/shared/auth.js";
1515
import { generateOAuthState } from "@/utils/oauthUtils";
16+
import { InspectorConfig } from "./configurationTypes";
17+
import {
18+
discoverAuthorizationServerMetadataViaProxy,
19+
discoverOAuthProtectedResourceMetadataViaProxy,
20+
registerClientViaProxy,
21+
exchangeAuthorizationViaProxy,
22+
} from "./oauth-proxy";
1623

1724
export interface StateMachineContext {
1825
state: AuthDebuggerState;
1926
serverUrl: string;
2027
provider: DebugInspectorOAuthClientProvider;
2128
updateState: (updates: Partial<AuthDebuggerState>) => void;
29+
connectionType: "direct" | "proxy";
30+
config: InspectorConfig;
2231
}
2332

2433
export interface StateTransition {
@@ -36,9 +45,18 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
3645
let resourceMetadata: OAuthProtectedResourceMetadata | null = null;
3746
let resourceMetadataError: Error | null = null;
3847
try {
39-
resourceMetadata = await discoverOAuthProtectedResourceMetadata(
40-
context.serverUrl,
41-
);
48+
// Use proxy if connectionType is "proxy"
49+
if (context.connectionType === "proxy") {
50+
resourceMetadata =
51+
await discoverOAuthProtectedResourceMetadataViaProxy(
52+
context.serverUrl,
53+
context.config,
54+
);
55+
} else {
56+
resourceMetadata = await discoverOAuthProtectedResourceMetadata(
57+
context.serverUrl,
58+
);
59+
}
4260
if (resourceMetadata?.authorization_servers?.length) {
4361
authServerUrl = new URL(resourceMetadata.authorization_servers[0]);
4462
}
@@ -57,7 +75,15 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
5775
resourceMetadata ?? undefined,
5876
);
5977

60-
const metadata = await discoverAuthorizationServerMetadata(authServerUrl);
78+
// Use proxy if connectionType is "proxy"
79+
const metadata =
80+
context.connectionType === "proxy"
81+
? await discoverAuthorizationServerMetadataViaProxy(
82+
authServerUrl,
83+
context.config,
84+
)
85+
: await discoverAuthorizationServerMetadata(authServerUrl);
86+
6187
if (!metadata) {
6288
throw new Error("Failed to discover OAuth metadata");
6389
}
@@ -86,19 +112,33 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
86112
const scopesSupported =
87113
context.state.resourceMetadata?.scopes_supported ||
88114
metadata.scopes_supported;
89-
// Add all supported scopes to client registration
90-
if (scopesSupported) {
115+
// Add all supported scopes to client registration (only if non-empty)
116+
if (scopesSupported && scopesSupported.length > 0) {
91117
clientMetadata.scope = scopesSupported.join(" ");
92118
}
93119
}
94120

95121
// Try Static client first, with DCR as fallback
96122
let fullInformation = await context.provider.clientInformation();
97123
if (!fullInformation) {
98-
fullInformation = await registerClient(context.serverUrl, {
99-
metadata,
100-
clientMetadata,
101-
});
124+
// Use proxy if connectionType is "proxy"
125+
if (context.connectionType === "proxy") {
126+
if (!metadata.registration_endpoint) {
127+
throw new Error(
128+
"No registration endpoint available for dynamic client registration",
129+
);
130+
}
131+
fullInformation = await registerClientViaProxy(
132+
metadata.registration_endpoint,
133+
clientMetadata,
134+
context.config,
135+
);
136+
} else {
137+
fullInformation = await registerClient(context.serverUrl, {
138+
metadata,
139+
clientMetadata,
140+
});
141+
}
102142
context.provider.saveClientInformation(fullInformation);
103143
}
104144

@@ -178,18 +218,50 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
178218
const metadata = context.provider.getServerMetadata()!;
179219
const clientInformation = (await context.provider.clientInformation())!;
180220

181-
const tokens = await exchangeAuthorization(context.serverUrl, {
182-
metadata,
183-
clientInformation,
184-
authorizationCode: context.state.authorizationCode,
185-
codeVerifier,
186-
redirectUri: context.provider.redirectUrl,
187-
resource: context.state.resource
188-
? context.state.resource instanceof URL
189-
? context.state.resource
190-
: new URL(context.state.resource)
191-
: undefined,
192-
});
221+
let tokens;
222+
223+
// Use proxy if connectionType is "proxy"
224+
if (context.connectionType === "proxy") {
225+
// Build the token request parameters
226+
const params: Record<string, string> = {
227+
grant_type: "authorization_code",
228+
code: context.state.authorizationCode,
229+
redirect_uri: context.provider.redirectUrl,
230+
code_verifier: codeVerifier,
231+
client_id: clientInformation.client_id,
232+
};
233+
234+
if (clientInformation.client_secret) {
235+
params.client_secret = clientInformation.client_secret;
236+
}
237+
238+
if (context.state.resource) {
239+
const resourceUrl =
240+
context.state.resource instanceof URL
241+
? context.state.resource.toString()
242+
: context.state.resource;
243+
params.resource = resourceUrl;
244+
}
245+
246+
tokens = await exchangeAuthorizationViaProxy(
247+
metadata.token_endpoint,
248+
params,
249+
context.config,
250+
);
251+
} else {
252+
tokens = await exchangeAuthorization(context.serverUrl, {
253+
metadata,
254+
clientInformation,
255+
authorizationCode: context.state.authorizationCode,
256+
codeVerifier,
257+
redirectUri: context.provider.redirectUrl,
258+
resource: context.state.resource
259+
? context.state.resource instanceof URL
260+
? context.state.resource
261+
: new URL(context.state.resource)
262+
: undefined,
263+
});
264+
}
193265

194266
context.provider.saveTokens(tokens);
195267
context.updateState({
@@ -211,6 +283,8 @@ export class OAuthStateMachine {
211283
constructor(
212284
private serverUrl: string,
213285
private updateState: (updates: Partial<AuthDebuggerState>) => void,
286+
private connectionType: "direct" | "proxy",
287+
private config: InspectorConfig,
214288
) {}
215289

216290
async executeStep(state: AuthDebuggerState): Promise<void> {
@@ -220,6 +294,8 @@ export class OAuthStateMachine {
220294
serverUrl: this.serverUrl,
221295
provider,
222296
updateState: this.updateState,
297+
connectionType: this.connectionType,
298+
config: this.config,
223299
};
224300

225301
const transition = oauthTransitions[state.oauthStep];

0 commit comments

Comments
 (0)