Skip to content

Commit eb6af47

Browse files
committed
Refactor to use auth from SDK
1 parent 6d930ec commit eb6af47

File tree

3 files changed

+65
-157
lines changed

3 files changed

+65
-157
lines changed

client/src/lib/auth.ts

Lines changed: 52 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,134 +1,71 @@
1-
import pkceChallenge from "pkce-challenge";
1+
import { OAuthClientInformation, OAuthClientInformationSchema, OAuthClientProvider, OAuthTokens, OAuthTokensSchema } from "@modelcontextprotocol/sdk/client/auth.js";
22
import { SESSION_KEYS } from "./constants";
3-
import { z } from "zod";
43

5-
export const OAuthMetadataSchema = z.object({
6-
authorization_endpoint: z.string(),
7-
token_endpoint: z.string(),
8-
});
9-
10-
export type OAuthMetadata = z.infer<typeof OAuthMetadataSchema>;
11-
12-
export const OAuthTokensSchema = z.object({
13-
access_token: z.string(),
14-
refresh_token: z.string().optional(),
15-
expires_in: z.number().optional(),
16-
});
17-
18-
export type OAuthTokens = z.infer<typeof OAuthTokensSchema>;
19-
20-
export async function discoverOAuthMetadata(
21-
serverUrl: string,
22-
): Promise<OAuthMetadata> {
23-
try {
24-
const url = new URL("/.well-known/oauth-authorization-server", serverUrl);
25-
const response = await fetch(url.toString());
26-
27-
if (response.ok) {
28-
const metadata = await response.json();
29-
const validatedMetadata = OAuthMetadataSchema.parse({
30-
authorization_endpoint: metadata.authorization_endpoint,
31-
token_endpoint: metadata.token_endpoint,
32-
});
33-
return validatedMetadata;
34-
}
35-
} catch (error) {
36-
console.warn("OAuth metadata discovery failed:", error);
4+
export class InspectorOAuthClientProvider implements OAuthClientProvider {
5+
get redirectUrl() {
6+
return window.location.origin + "/oauth/callback";
377
}
388

39-
// Fall back to default endpoints
40-
const baseUrl = new URL(serverUrl);
41-
const defaultMetadata = {
42-
authorization_endpoint: new URL("/authorize", baseUrl).toString(),
43-
token_endpoint: new URL("/token", baseUrl).toString(),
44-
};
45-
return OAuthMetadataSchema.parse(defaultMetadata);
46-
}
47-
48-
export async function startOAuthFlow(serverUrl: string): Promise<string> {
49-
// Generate PKCE challenge
50-
const challenge = await pkceChallenge();
51-
const codeVerifier = challenge.code_verifier;
52-
const codeChallenge = challenge.code_challenge;
53-
54-
// Store code verifier for later use
55-
sessionStorage.setItem(SESSION_KEYS.CODE_VERIFIER, codeVerifier);
56-
57-
// Discover OAuth endpoints
58-
const metadata = await discoverOAuthMetadata(serverUrl);
9+
get clientMetadata() {
10+
return {
11+
redirect_uris: [this.redirectUrl],
12+
token_endpoint_auth_method: "none",
13+
grant_types: ["authorization_code", "refresh_token"],
14+
response_types: ["code"],
15+
client_name: "MCP Inspector",
16+
client_uri: "https://github.com/modelcontextprotocol/inspector",
17+
};
18+
}
5919

60-
// Build authorization URL
61-
const authUrl = new URL(metadata.authorization_endpoint);
62-
authUrl.searchParams.set("response_type", "code");
63-
authUrl.searchParams.set("code_challenge", codeChallenge);
64-
authUrl.searchParams.set("code_challenge_method", "S256");
65-
authUrl.searchParams.set(
66-
"redirect_uri",
67-
window.location.origin + "/oauth/callback",
68-
);
20+
async clientInformation() {
21+
const value = sessionStorage.getItem(SESSION_KEYS.CLIENT_INFORMATION);
22+
if (!value) {
23+
return undefined;
24+
}
6925

70-
return authUrl.toString();
71-
}
26+
return await OAuthClientInformationSchema.parseAsync(JSON.parse(value));
27+
}
7228

73-
export async function handleOAuthCallback(
74-
serverUrl: string,
75-
code: string,
76-
): Promise<OAuthTokens> {
77-
// Get stored code verifier
78-
const codeVerifier = sessionStorage.getItem(SESSION_KEYS.CODE_VERIFIER);
79-
if (!codeVerifier) {
80-
throw new Error("No code verifier found");
29+
saveClientInformation(clientInformation: OAuthClientInformation) {
30+
sessionStorage.setItem(
31+
SESSION_KEYS.CLIENT_INFORMATION,
32+
JSON.stringify(clientInformation),
33+
);
8134
}
8235

83-
// Discover OAuth endpoints
84-
const metadata = await discoverOAuthMetadata(serverUrl);
85-
// Exchange code for tokens
86-
const response = await fetch(metadata.token_endpoint, {
87-
method: "POST",
88-
headers: {
89-
"Content-Type": "application/json",
90-
},
91-
body: JSON.stringify({
92-
grant_type: "authorization_code",
93-
code,
94-
code_verifier: codeVerifier,
95-
redirect_uri: window.location.origin + "/oauth/callback",
96-
}),
97-
});
36+
async tokens() {
37+
const tokens = sessionStorage.getItem(SESSION_KEYS.TOKENS);
38+
if (!tokens) {
39+
return undefined;
40+
}
9841

99-
if (!response.ok) {
100-
throw new Error("Token exchange failed");
42+
return await OAuthTokensSchema.parseAsync(JSON.parse(tokens));
10143
}
10244

103-
const tokens = await response.json();
104-
return OAuthTokensSchema.parse(tokens);
105-
}
45+
saveTokens(tokens: OAuthTokens) {
46+
sessionStorage.setItem(
47+
SESSION_KEYS.TOKENS,
48+
JSON.stringify(tokens),
49+
);
50+
}
10651

107-
export async function refreshAccessToken(
108-
serverUrl: string,
109-
): Promise<OAuthTokens> {
110-
const refreshToken = sessionStorage.getItem(SESSION_KEYS.REFRESH_TOKEN);
111-
if (!refreshToken) {
112-
throw new Error("No refresh token available");
52+
redirectToAuthorization(authorizationUrl: URL) {
53+
window.location.href = authorizationUrl.href;
11354
}
11455

115-
const metadata = await discoverOAuthMetadata(serverUrl);
56+
saveCodeVerifier(codeVerifier: string) {
57+
sessionStorage.setItem(
58+
SESSION_KEYS.CODE_VERIFIER,
59+
codeVerifier,
60+
);
61+
}
11662

117-
const response = await fetch(metadata.token_endpoint, {
118-
method: "POST",
119-
headers: {
120-
"Content-Type": "application/json",
121-
},
122-
body: JSON.stringify({
123-
grant_type: "refresh_token",
124-
refresh_token: refreshToken,
125-
}),
126-
});
63+
codeVerifier() {
64+
const verifier = sessionStorage.getItem(SESSION_KEYS.CODE_VERIFIER);
65+
if (!verifier) {
66+
throw new Error("No code verifier saved for session");
67+
}
12768

128-
if (!response.ok) {
129-
throw new Error("Token refresh failed");
69+
return verifier;
13070
}
131-
132-
const tokens = await response.json();
133-
return OAuthTokensSchema.parse(tokens);
13471
}

client/src/lib/constants.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
export const SESSION_KEYS = {
33
CODE_VERIFIER: "mcp_code_verifier",
44
SERVER_URL: "mcp_server_url",
5-
ACCESS_TOKEN: "mcp_access_token",
6-
REFRESH_TOKEN: "mcp_refresh_token",
5+
TOKENS: "mcp_tokens",
6+
CLIENT_INFORMATION: "mcp_client_information",
77
} as const;

client/src/lib/hooks/useConnection.ts

Lines changed: 11 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ import {
1616
import { useState } from "react";
1717
import { toast } from "react-toastify";
1818
import { z } from "zod";
19-
import { startOAuthFlow, refreshAccessToken } from "../auth";
2019
import { SESSION_KEYS } from "../constants";
2120
import { Notification, StdErrNotificationSchema } from "../notificationTypes";
21+
import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
22+
import { InspectorOAuthClientProvider } from "../auth";
2223

2324
const DEFAULT_REQUEST_TIMEOUT_MSEC = 10000;
2425

@@ -121,45 +122,15 @@ export function useConnection({
121122
}
122123
};
123124

124-
const initiateOAuthFlow = async () => {
125-
sessionStorage.removeItem(SESSION_KEYS.ACCESS_TOKEN);
126-
sessionStorage.removeItem(SESSION_KEYS.REFRESH_TOKEN);
127-
sessionStorage.setItem(SESSION_KEYS.SERVER_URL, sseUrl);
128-
const redirectUrl = await startOAuthFlow(sseUrl);
129-
window.location.href = redirectUrl;
130-
};
131-
132-
const handleTokenRefresh = async () => {
133-
try {
134-
const tokens = await refreshAccessToken(sseUrl);
135-
sessionStorage.setItem(SESSION_KEYS.ACCESS_TOKEN, tokens.access_token);
136-
if (tokens.refresh_token) {
137-
sessionStorage.setItem(
138-
SESSION_KEYS.REFRESH_TOKEN,
139-
tokens.refresh_token,
140-
);
141-
}
142-
return tokens.access_token;
143-
} catch (error) {
144-
console.error("Token refresh failed:", error);
145-
await initiateOAuthFlow();
146-
throw error;
147-
}
148-
};
149-
125+
const authProvider = new InspectorOAuthClientProvider();
150126
const handleAuthError = async (error: unknown) => {
151127
if (error instanceof SseError && error.code === 401) {
152-
if (sessionStorage.getItem(SESSION_KEYS.REFRESH_TOKEN)) {
153-
try {
154-
await handleTokenRefresh();
155-
return true;
156-
} catch (error) {
157-
console.error("Token refresh failed:", error);
158-
}
159-
} else {
160-
await initiateOAuthFlow();
161-
}
128+
sessionStorage.setItem(SESSION_KEYS.SERVER_URL, sseUrl);
129+
130+
const result = await auth(authProvider, { serverUrl: sseUrl })
131+
return result === "AUTHORIZED";
162132
}
133+
163134
return false;
164135
};
165136

@@ -192,9 +163,9 @@ export function useConnection({
192163
}
193164

194165
const headers: HeadersInit = {};
195-
const accessToken = sessionStorage.getItem(SESSION_KEYS.ACCESS_TOKEN);
196-
if (accessToken) {
197-
headers["Authorization"] = `Bearer ${accessToken}`;
166+
const tokens = await authProvider.tokens();
167+
if (tokens) {
168+
headers["Authorization"] = `Bearer ${tokens.access_token}`;
198169
}
199170

200171
const clientTransport = new SSEClientTransport(backendUrl, {

0 commit comments

Comments
 (0)