Skip to content

Commit cc49358

Browse files
committed
add test + prettier
1 parent 35eab58 commit cc49358

File tree

7 files changed

+139
-23
lines changed

7 files changed

+139
-23
lines changed

client/src/App.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,8 @@ const App = () => {
122122
const [isAuthDebuggerVisible, setIsAuthDebuggerVisible] = useState(false);
123123

124124
// Auth debugger state
125-
const [authState, setAuthState] = useState<AuthDebuggerState>(EMPTY_DEBUGGER_STATE);
125+
const [authState, setAuthState] =
126+
useState<AuthDebuggerState>(EMPTY_DEBUGGER_STATE);
126127

127128
// Helper function to update specific auth state properties
128129
const updateAuthState = (updates: Partial<AuthDebuggerState>) => {
@@ -268,7 +269,10 @@ const App = () => {
268269
});
269270

270271
// Continue stepping through the OAuth flow from where we left off
271-
while (currentState.oauthStep !== "complete" && currentState.oauthStep !== "authorization_code") {
272+
while (
273+
currentState.oauthStep !== "complete" &&
274+
currentState.oauthStep !== "authorization_code"
275+
) {
272276
await stateMachine.executeStep(currentState);
273277
}
274278

@@ -286,7 +290,8 @@ const App = () => {
286290
} catch (error) {
287291
console.error("OAuth continuation error:", error);
288292
updateAuthState({
289-
latestError: error instanceof Error ? error : new Error(String(error)),
293+
latestError:
294+
error instanceof Error ? error : new Error(String(error)),
290295
statusMessage: {
291296
type: "error",
292297
message: `Failed to complete OAuth flow: ${error instanceof Error ? error.message : String(error)}`,

client/src/components/AuthDebugger.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,13 @@ const AuthDebugger = ({
6767
const checkTokens = async () => {
6868
const provider = new DebugInspectorOAuthClientProvider(serverUrl);
6969
const existingTokens = await provider.tokens();
70-
70+
7171
updateAuthState({
7272
loading: false,
7373
oauthTokens: existingTokens || null,
7474
});
7575
};
76-
76+
7777
checkTokens();
7878
}
7979
}, [serverUrl, authState.loading, updateAuthState]);
@@ -162,7 +162,7 @@ const AuthDebugger = ({
162162
// Store the current auth state before redirecting
163163
sessionStorage.setItem(
164164
SESSION_KEYS.AUTH_DEBUGGER_STATE,
165-
JSON.stringify(currentState)
165+
JSON.stringify(currentState),
166166
);
167167
// Open the authorization URL automatically
168168
window.location.href = currentState.authorizationUrl;

client/src/components/OAuthDebugCallback.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@ const OAuthDebugCallback = ({ onConnect }: OAuthCallbackProps) => {
3636
}
3737

3838
const serverUrl = sessionStorage.getItem(SESSION_KEYS.SERVER_URL);
39-
39+
4040
// Try to restore the auth state
41-
const storedState = sessionStorage.getItem(SESSION_KEYS.AUTH_DEBUGGER_STATE);
41+
const storedState = sessionStorage.getItem(
42+
SESSION_KEYS.AUTH_DEBUGGER_STATE,
43+
);
4244
let restoredState = null;
4345
if (storedState) {
4446
try {

client/src/components/OAuthFlowProgress.tsx

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,13 @@ export const OAuthFlowProgress = ({
139139
<div className="mt-2">
140140
<p className="font-medium">Resource Metadata:</p>
141141
<p className="text-xs text-muted-foreground">
142-
From {new URL('/.well-known/oauth-protected-resource', serverUrl).href}
142+
From{" "}
143+
{
144+
new URL(
145+
"/.well-known/oauth-protected-resource",
146+
serverUrl,
147+
).href
148+
}
143149
</p>
144150
<pre className="mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]">
145151
{JSON.stringify(authState.resourceMetadata, null, 2)}
@@ -150,27 +156,53 @@ export const OAuthFlowProgress = ({
150156
{authState.resourceMetadataError && (
151157
<div className="mt-2 p-3 border border-blue-300 bg-blue-50 rounded-md">
152158
<p className="text-sm font-medium text-blue-700">
153-
ℹ️ No resource metadata available from {' '}
154-
<a href={new URL('/.well-known/oauth-protected-resource', serverUrl).href} target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:text-blue-700">
155-
{new URL('/.well-known/oauth-protected-resource', serverUrl).href}
159+
ℹ️ No resource metadata available from{" "}
160+
<a
161+
href={
162+
new URL(
163+
"/.well-known/oauth-protected-resource",
164+
serverUrl,
165+
).href
166+
}
167+
target="_blank"
168+
rel="noopener noreferrer"
169+
className="text-blue-500 hover:text-blue-700"
170+
>
171+
{
172+
new URL(
173+
"/.well-known/oauth-protected-resource",
174+
serverUrl,
175+
).href
176+
}
156177
</a>
157178
</p>
158179
<p className="text-xs text-blue-600 mt-1">
159-
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>
180+
Resource metadata was added in the{" "}
181+
<a href="https://modelcontextprotocol.io/specification/draft/basic/authorization#2-3-1-authorization-server-location">
182+
2025-DRAFT-v2 specification update
183+
</a>
160184
<br />
161185
{authState.resourceMetadataError.message}
162-
{authState.resourceMetadataError instanceof TypeError
163-
&& " (This could indicate the endpoint doesn't exist or does not have CORS configured)"}
186+
{authState.resourceMetadataError instanceof TypeError &&
187+
" (This could indicate the endpoint doesn't exist or does not have CORS configured)"}
164188
</p>
165189
</div>
166190
)}
167191

168192
{authState.oauthMetadata && (
169193
<div className="mt-2">
170194
<p className="font-medium">Authorization Server Metadata:</p>
171-
{authState.authServerUrl && <p className="text-xs text-muted-foreground">
172-
From {new URL('/.well-known/oauth-authorization-server', authState.authServerUrl).href}
173-
</p>}
195+
{authState.authServerUrl && (
196+
<p className="text-xs text-muted-foreground">
197+
From{" "}
198+
{
199+
new URL(
200+
"/.well-known/oauth-authorization-server",
201+
authState.authServerUrl,
202+
).href
203+
}
204+
</p>
205+
)}
174206
<pre className="mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]">
175207
{JSON.stringify(authState.oauthMetadata, null, 2)}
176208
</pre>

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
4040
registerClient: jest.fn(),
4141
startAuthorization: jest.fn(),
4242
exchangeAuthorization: jest.fn(),
43+
discoverOAuthProtectedResourceMetadata: jest.fn(),
4344
}));
4445

4546
// Import the functions to get their types
@@ -49,6 +50,7 @@ import {
4950
startAuthorization,
5051
exchangeAuthorization,
5152
auth,
53+
discoverOAuthProtectedResourceMetadata,
5254
} from "@modelcontextprotocol/sdk/client/auth.js";
5355
import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
5456
import { EMPTY_DEBUGGER_STATE } from "@/lib/auth-types";
@@ -67,6 +69,10 @@ const mockExchangeAuthorization = exchangeAuthorization as jest.MockedFunction<
6769
typeof exchangeAuthorization
6870
>;
6971
const mockAuth = auth as jest.MockedFunction<typeof auth>;
72+
const mockDiscoverOAuthProtectedResourceMetadata =
73+
discoverOAuthProtectedResourceMetadata as jest.MockedFunction<
74+
typeof discoverOAuthProtectedResourceMetadata
75+
>;
7076

7177
const sessionStorageMock = {
7278
getItem: jest.fn(),
@@ -100,6 +106,7 @@ describe("AuthDebugger", () => {
100106

101107
mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
102108
mockRegisterClient.mockResolvedValue(mockOAuthClientInfo);
109+
mockDiscoverOAuthProtectedResourceMetadata.mockResolvedValue(null);
103110
mockStartAuthorization.mockImplementation(async (_sseUrl, options) => {
104111
const authUrl = new URL("https://oauth.example.com/authorize");
105112

@@ -421,4 +428,63 @@ describe("AuthDebugger", () => {
421428
});
422429
});
423430
});
431+
432+
describe("OAuth State Persistence", () => {
433+
it("should store auth state to sessionStorage before redirect in Quick OAuth Flow", async () => {
434+
const updateAuthState = jest.fn();
435+
436+
// Mock window.location.href setter
437+
delete (window as any).location;
438+
window.location = { href: "" } as any;
439+
440+
// Setup mocks for OAuth flow
441+
mockStartAuthorization.mockResolvedValue({
442+
authorizationUrl: new URL(
443+
"https://oauth.example.com/authorize?client_id=test_client_id&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Foauth%2Fcallback%2Fdebug",
444+
),
445+
codeVerifier: "test_verifier",
446+
});
447+
448+
await act(async () => {
449+
renderAuthDebugger({
450+
updateAuthState,
451+
authState: { ...defaultAuthState, loading: false },
452+
});
453+
});
454+
455+
// Click Quick OAuth Flow
456+
await act(async () => {
457+
fireEvent.click(screen.getByText("Quick OAuth Flow"));
458+
});
459+
460+
// Wait for the flow to reach the authorization step
461+
await waitFor(() => {
462+
expect(sessionStorage.setItem).toHaveBeenCalledWith(
463+
SESSION_KEYS.AUTH_DEBUGGER_STATE,
464+
expect.stringContaining('"oauthStep":"authorization_code"'),
465+
);
466+
});
467+
468+
// Verify the stored state includes all the accumulated data
469+
const storedStateCall = (
470+
sessionStorage.setItem as jest.Mock
471+
).mock.calls.find((call) => call[0] === SESSION_KEYS.AUTH_DEBUGGER_STATE);
472+
473+
expect(storedStateCall).toBeDefined();
474+
const storedState = JSON.parse(storedStateCall![1]);
475+
476+
expect(storedState).toMatchObject({
477+
oauthStep: "authorization_code",
478+
authorizationUrl: expect.stringMatching(
479+
/^https:\/\/oauth\.example\.com\/authorize/,
480+
),
481+
oauthMetadata: expect.objectContaining({
482+
token_endpoint: "https://oauth.example.com/token",
483+
}),
484+
oauthClientInfo: expect.objectContaining({
485+
client_id: "test_client_id",
486+
}),
487+
});
488+
});
489+
});
424490
});

client/src/lib/auth-types.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ export interface AuthDebuggerState {
3030
loading: boolean;
3131
oauthStep: OAuthStep;
3232
resourceMetadata: OAuthProtectedResourceMetadata | null;
33-
resourceMetadataError: Error | { status: number; statusText: string; message: string } | null;
33+
resourceMetadataError:
34+
| Error
35+
| { status: number; statusText: string; message: string }
36+
| null;
3437
authServerUrl: URL | null;
3538
oauthMetadata: OAuthMetadata | null;
3639
oauthClientInfo: OAuthClientInformationFull | OAuthClientInformation | null;
@@ -56,4 +59,4 @@ export const EMPTY_DEBUGGER_STATE: AuthDebuggerState = {
5659
latestError: null,
5760
statusMessage: null,
5861
validationError: null,
59-
}
62+
};

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import {
77
exchangeAuthorization,
88
discoverOAuthProtectedResourceMetadata,
99
} from "@modelcontextprotocol/sdk/client/auth.js";
10-
import { OAuthMetadataSchema, OAuthProtectedResourceMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
10+
import {
11+
OAuthMetadataSchema,
12+
OAuthProtectedResourceMetadata,
13+
} from "@modelcontextprotocol/sdk/shared/auth.js";
1114

1215
export interface StateMachineContext {
1316
state: AuthDebuggerState;
@@ -30,8 +33,13 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
3033
let resourceMetadata: OAuthProtectedResourceMetadata | null = null;
3134
let resourceMetadataError: Error | null = null;
3235
try {
33-
resourceMetadata = await discoverOAuthProtectedResourceMetadata(context.serverUrl);
34-
if (resourceMetadata && resourceMetadata.authorization_servers?.length) {
36+
resourceMetadata = await discoverOAuthProtectedResourceMetadata(
37+
context.serverUrl,
38+
);
39+
if (
40+
resourceMetadata &&
41+
resourceMetadata.authorization_servers?.length
42+
) {
3543
authServerUrl = new URL(resourceMetadata.authorization_servers[0]);
3644
}
3745
} catch (e) {

0 commit comments

Comments
 (0)