From b99b3d29d9b9ed210acbe1259b42ea4671524cea Mon Sep 17 00:00:00 2001 From: 2underscores Date: Mon, 28 Jul 2025 20:18:18 +1000 Subject: [PATCH 1/5] Using static client data as DCR fallback --- client/src/lib/oauth-state-machine.ts | 36 ++++++++++++++++++++------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index 5078cfe4b..758deb23f 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -88,16 +88,34 @@ export const oauthTransitions: Record = { clientMetadata.scope = scopesSupported.join(" "); } - const fullInformation = await registerClient(context.serverUrl, { - metadata, - clientMetadata, - }); + // Try DCR first, with static client as fallback + try { + const fullInformation = await registerClient(context.serverUrl, { + metadata, + clientMetadata, + }); - context.provider.saveClientInformation(fullInformation); - context.updateState({ - oauthClientInfo: fullInformation, - oauthStep: "authorization_redirect", - }); + context.provider.saveClientInformation(fullInformation); + context.updateState({ + oauthClientInfo: fullInformation, + oauthStep: "authorization_redirect", + }); + console.log({ fullInformation }); + return; + } catch (dcrError) { + console.error(dcrError); + + // DCR failed, fallback to preregistered client + const existingClientInfo = await context.provider.clientInformation(); + if (!existingClientInfo) { + console.error("Neither dynamic client registration or preregistered client information was found"); + throw dcrError; + } + context.updateState({ + oauthClientInfo: existingClientInfo, + oauthStep: "authorization_redirect", + }); + } }, }, From 6a9505673121efed5a527d5e3b405fa8bea35043 Mon Sep 17 00:00:00 2001 From: 2underscores Date: Wed, 6 Aug 2025 08:47:21 +1000 Subject: [PATCH 2/5] Refactor --- client/src/lib/oauth-state-machine.ts | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index 758deb23f..b8ecab51a 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -89,33 +89,26 @@ export const oauthTransitions: Record = { } // Try DCR first, with static client as fallback + let fullInformation; try { - const fullInformation = await registerClient(context.serverUrl, { + fullInformation = await registerClient(context.serverUrl, { metadata, clientMetadata, }); - context.provider.saveClientInformation(fullInformation); - context.updateState({ - oauthClientInfo: fullInformation, - oauthStep: "authorization_redirect", - }); - console.log({ fullInformation }); - return; } catch (dcrError) { - console.error(dcrError); - // DCR failed, fallback to preregistered client - const existingClientInfo = await context.provider.clientInformation(); - if (!existingClientInfo) { + fullInformation = await context.provider.clientInformation(); + if (!fullInformation) { console.error("Neither dynamic client registration or preregistered client information was found"); throw dcrError; } - context.updateState({ - oauthClientInfo: existingClientInfo, - oauthStep: "authorization_redirect", - }); } + + context.updateState({ + oauthClientInfo: fullInformation, + oauthStep: "authorization_redirect", + }); }, }, From 37f634d6512f2088583f48286bf71066e04b348c Mon Sep 17 00:00:00 2001 From: 2underscores Date: Mon, 11 Aug 2025 14:41:41 +1000 Subject: [PATCH 3/5] rm console.log --- client/src/lib/oauth-state-machine.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index b8ecab51a..b110e148c 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -100,7 +100,6 @@ export const oauthTransitions: Record = { // DCR failed, fallback to preregistered client fullInformation = await context.provider.clientInformation(); if (!fullInformation) { - console.error("Neither dynamic client registration or preregistered client information was found"); throw dcrError; } } From 0b123acdac3e749fd972e6b5aa9047fdf0d23e87 Mon Sep 17 00:00:00 2001 From: 2underscores Date: Tue, 12 Aug 2025 09:20:28 +1000 Subject: [PATCH 4/5] Static client attempt before DCR --- client/src/lib/oauth-state-machine.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index b110e148c..49a775302 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -88,20 +88,14 @@ export const oauthTransitions: Record = { clientMetadata.scope = scopesSupported.join(" "); } - // Try DCR first, with static client as fallback - let fullInformation; - try { + // Try Static client first, with DCR as fallback + let fullInformation = await context.provider.clientInformation(); + if (!fullInformation) { fullInformation = await registerClient(context.serverUrl, { metadata, clientMetadata, }); context.provider.saveClientInformation(fullInformation); - } catch (dcrError) { - // DCR failed, fallback to preregistered client - fullInformation = await context.provider.clientInformation(); - if (!fullInformation) { - throw dcrError; - } } context.updateState({ From e9f1da383e76f7ffaa8d51dd980d89b39bb4e96c Mon Sep 17 00:00:00 2001 From: 2underscores Date: Tue, 12 Aug 2025 13:22:09 +1000 Subject: [PATCH 5/5] Tests for client discovery --- .../__tests__/AuthDebugger.test.tsx | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/client/src/components/__tests__/AuthDebugger.test.tsx b/client/src/components/__tests__/AuthDebugger.test.tsx index fa9f2f7da..28c687fd1 100644 --- a/client/src/components/__tests__/AuthDebugger.test.tsx +++ b/client/src/components/__tests__/AuthDebugger.test.tsx @@ -439,6 +439,103 @@ describe("AuthDebugger", () => { }); }); + describe("Client Registration behavior", () => { + it("uses preregistered (static) client information without calling DCR", async () => { + const preregClientInfo = { + client_id: "static_client_id", + client_secret: "static_client_secret", + redirect_uris: ["http://localhost:3000/oauth/callback/debug"], + }; + + // Return preregistered client info for the server-specific key + sessionStorageMock.getItem.mockImplementation((key) => { + if ( + key === + `[${defaultProps.serverUrl}] ${SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION}` + ) { + return JSON.stringify(preregClientInfo); + } + return null; + }); + + const updateAuthState = jest.fn(); + + await act(async () => { + renderAuthDebugger({ + updateAuthState, + authState: { + ...defaultAuthState, + isInitiatingAuth: false, + oauthStep: "client_registration", + oauthMetadata: mockOAuthMetadata as unknown as OAuthMetadata, + }, + }); + }); + + // Proceed from client_registration → authorization_redirect + await act(async () => { + fireEvent.click(screen.getByText("Continue")); + }); + + // Should NOT attempt dynamic client registration + expect(mockRegisterClient).not.toHaveBeenCalled(); + + // Should advance with the preregistered client info + expect(updateAuthState).toHaveBeenCalledWith( + expect.objectContaining({ + oauthClientInfo: expect.objectContaining({ + client_id: "static_client_id", + }), + oauthStep: "authorization_redirect", + }), + ); + }); + + it("falls back to DCR when no static client information is available", async () => { + // No preregistered or dynamic client info present in session storage + sessionStorageMock.getItem.mockImplementation(() => null); + + // DCR returns a new client + mockRegisterClient.mockResolvedValueOnce(mockOAuthClientInfo); + + const updateAuthState = jest.fn(); + + await act(async () => { + renderAuthDebugger({ + updateAuthState, + authState: { + ...defaultAuthState, + isInitiatingAuth: false, + oauthStep: "client_registration", + oauthMetadata: mockOAuthMetadata as unknown as OAuthMetadata, + }, + }); + }); + + await act(async () => { + fireEvent.click(screen.getByText("Continue")); + }); + + expect(mockRegisterClient).toHaveBeenCalledTimes(1); + + // Should save and advance with the DCR client info + expect(updateAuthState).toHaveBeenCalledWith( + expect.objectContaining({ + oauthClientInfo: expect.objectContaining({ + client_id: "test_client_id", + }), + oauthStep: "authorization_redirect", + }), + ); + + // Verify the dynamically registered client info was persisted + expect(sessionStorage.setItem).toHaveBeenCalledWith( + `[${defaultProps.serverUrl}] ${SESSION_KEYS.CLIENT_INFORMATION}`, + expect.any(String), + ); + }); + }); + describe("OAuth State Persistence", () => { it("should store auth state to sessionStorage before redirect in Quick OAuth Flow", async () => { const updateAuthState = jest.fn();