From e9bf2def4edfedcca724f1275f1007d120eb2381 Mon Sep 17 00:00:00 2001 From: mehmet turac Date: Mon, 29 Jun 2026 02:45:44 +0300 Subject: [PATCH] fix: preserve OAuth DCR client info --- client/src/App.tsx | 3 ++ client/src/__tests__/App.routing.test.tsx | 53 ++++++++++++++++++- .../__tests__/AuthDebugger.test.tsx | 40 ++++++++++++++ client/src/lib/oauth-state-machine.ts | 21 ++++++-- 4 files changed, 110 insertions(+), 7 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 7cf6d751a..79c64d35d 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -285,6 +285,9 @@ const App = () => { }); const updateAuthState = (updates: Partial) => { + if (updates.oauthClientInfo?.client_id) { + setOauthClientId(updates.oauthClientInfo.client_id); + } setAuthState((prev) => ({ ...prev, ...updates })); }; diff --git a/client/src/__tests__/App.routing.test.tsx b/client/src/__tests__/App.routing.test.tsx index 4713bef9a..5b555ab7a 100644 --- a/client/src/__tests__/App.routing.test.tsx +++ b/client/src/__tests__/App.routing.test.tsx @@ -1,4 +1,5 @@ -import { render, waitFor } from "@testing-library/react"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import "@testing-library/jest-dom"; import App from "../App"; import { useConnection } from "../lib/hooks/useConnection"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; @@ -79,7 +80,35 @@ jest.mock("../lib/hooks/useDraggablePane", () => ({ jest.mock("../components/Sidebar", () => ({ __esModule: true, - default: () =>
Sidebar
, + default: ({ oauthClientId }: { oauthClientId: string }) => ( +
+
{oauthClientId}
+
+ ), +})); + +jest.mock("../components/AuthDebugger", () => ({ + __esModule: true, + default: ({ + updateAuthState, + }: { + updateAuthState: ( + updates: Partial, + ) => void; + }) => ( + + ), })); // Mock fetch @@ -95,6 +124,8 @@ describe("App - URL Fragment Routing", () => { beforeEach(() => { jest.restoreAllMocks(); + localStorage.clear(); + window.location.hash = ""; // Inspector starts disconnected. mockUseConnection.mockReturnValue(disconnectedConnectionState); @@ -158,4 +189,22 @@ describe("App - URL Fragment Routing", () => { expect(window.location.hash).toBe(""); }); }); + + test("syncs dynamically registered OAuth client ID into the sidebar field", async () => { + render(); + + fireEvent.click( + screen.getByRole("button", { name: /open auth settings/i }), + ); + fireEvent.click( + screen.getByRole("button", { name: /simulate dcr client/i }), + ); + + await waitFor(() => { + expect(screen.getByTestId("sidebar-oauth-client-id")).toHaveTextContent( + "dcr_client_id", + ); + expect(localStorage.getItem("lastOauthClientId")).toBe("dcr_client_id"); + }); + }); }); diff --git a/client/src/components/__tests__/AuthDebugger.test.tsx b/client/src/components/__tests__/AuthDebugger.test.tsx index 71eec04aa..44f5aa5bc 100644 --- a/client/src/components/__tests__/AuthDebugger.test.tsx +++ b/client/src/components/__tests__/AuthDebugger.test.tsx @@ -683,6 +683,46 @@ describe("AuthDebugger", () => { }); }); + describe("Token Request behavior", () => { + it("uses OAuth client information restored with auth state when storage is empty", async () => { + sessionStorageMock.getItem.mockImplementation(() => null); + mockExchangeAuthorization.mockResolvedValueOnce(mockOAuthTokens); + + const updateAuthState = jest.fn(); + + await act(async () => { + renderAuthDebugger({ + updateAuthState, + authState: { + ...defaultAuthState, + isInitiatingAuth: false, + oauthStep: "token_request", + oauthMetadata: mockOAuthMetadata as unknown as OAuthMetadata, + oauthClientInfo: mockOAuthClientInfo, + authorizationCode: "test_authorization_code", + }, + }); + }); + + await act(async () => { + fireEvent.click(screen.getByText("Continue")); + }); + + expect(mockExchangeAuthorization).toHaveBeenCalledWith( + defaultProps.serverUrl, + expect.objectContaining({ + metadata: mockOAuthMetadata, + clientInformation: mockOAuthClientInfo, + authorizationCode: "test_authorization_code", + }), + ); + expect(updateAuthState).toHaveBeenCalledWith({ + oauthTokens: mockOAuthTokens, + oauthStep: "complete", + }); + }); + }); + describe("OAuth State Persistence", () => { it("should store auth state to sessionStorage before redirect in Quick OAuth Flow", async () => { const updateAuthState = jest.fn(); diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index 6628b9ad5..bc162c8f4 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -175,16 +175,27 @@ export const oauthTransitions: Record = { token_request: { canTransition: async (context) => { + const metadata = + context.provider.getServerMetadata() ?? context.state.oauthMetadata; + const clientInformation = + (await context.provider.clientInformation()) ?? + context.state.oauthClientInfo; + return ( - !!context.state.authorizationCode && - !!context.provider.getServerMetadata() && - !!(await context.provider.clientInformation()) + !!context.state.authorizationCode && !!metadata && !!clientInformation ); }, execute: async (context) => { const codeVerifier = context.provider.codeVerifier(); - const metadata = context.provider.getServerMetadata()!; - const clientInformation = (await context.provider.clientInformation())!; + const metadata = + context.provider.getServerMetadata() ?? context.state.oauthMetadata; + const clientInformation = + (await context.provider.clientInformation()) ?? + context.state.oauthClientInfo; + + if (!metadata || !clientInformation) { + throw new Error("Missing OAuth metadata or client information"); + } const tokens = await exchangeAuthorization(context.serverUrl, { metadata,