From b8f91d0e61730bb95675b3c4497a4df0d3919410 Mon Sep 17 00:00:00 2001 From: samzong Date: Thu, 23 Jul 2026 17:30:52 +0800 Subject: [PATCH] fix(web): echo OAuth state on authorize redirect (#39459) Signed-off-by: samzong --- web/app/account/oauth/authorize/__tests__/page.spec.tsx | 9 +++++---- web/app/account/oauth/authorize/page.tsx | 2 ++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/web/app/account/oauth/authorize/__tests__/page.spec.tsx b/web/app/account/oauth/authorize/__tests__/page.spec.tsx index 003673b0225..daaa19ac68e 100644 --- a/web/app/account/oauth/authorize/__tests__/page.spec.tsx +++ b/web/app/account/oauth/authorize/__tests__/page.spec.tsx @@ -63,7 +63,8 @@ describe('OAuthAuthorize', () => { vi.clearAllMocks() mocks.searchParams = new URLSearchParams({ client_id: 'client-1', - redirect_uri: 'https://client.example.com/callback?state=state-1', + redirect_uri: 'https://client.example.com/callback', + state: 'state-1', }) mocks.request.mockImplementation(async (url: string) => { if (url.endsWith('/oauth/provider/authorize')) return jsonResponse({ code: 'oauth-code' }) @@ -86,7 +87,7 @@ describe('OAuthAuthorize', () => { vi.unstubAllGlobals() }) - it('authorizes the displayed app and redirects with the returned code', async () => { + it('authorizes the displayed app and redirects with the returned code and state', async () => { const user = userEvent.setup() renderPage() @@ -95,7 +96,7 @@ describe('OAuthAuthorize', () => { const providerTransportRequest = providerRequest?.[2]?.request as Request await expect(providerTransportRequest.clone().json()).resolves.toEqual({ client_id: 'client-1', - redirect_uri: 'https://client.example.com/callback?state=state-1', + redirect_uri: 'https://client.example.com/callback', }) await user.click(screen.getByRole('button', { name: /continue/i })) @@ -106,7 +107,7 @@ describe('OAuthAuthorize', () => { await expect(transportRequest.clone().json()).resolves.toEqual({ client_id: 'client-1' }) await waitFor(() => expect(globalThis.location.href).toBe( - 'https://client.example.com/callback?state=state-1&code=oauth-code', + 'https://client.example.com/callback?code=oauth-code&state=state-1', ), ) }) diff --git a/web/app/account/oauth/authorize/page.tsx b/web/app/account/oauth/authorize/page.tsx index a927e318bc8..337cfc6955d 100644 --- a/web/app/account/oauth/authorize/page.tsx +++ b/web/app/account/oauth/authorize/page.tsx @@ -64,6 +64,7 @@ export default function OAuthAuthorize() { const searchParams = useSearchParams() const client_id = decodeURIComponent(searchParams.get('client_id') || '') const redirect_uri = decodeURIComponent(searchParams.get('redirect_uri') || '') + const state = searchParams.get('state') const hasOAuthParams = Boolean(client_id && redirect_uri) // Probe user profile. 401 stays as `error` (legitimate "not logged in" state), // other errors throw to the nearest error.tsx; jumpTo same-pathname guard in @@ -117,6 +118,7 @@ export default function OAuthAuthorize() { const { code } = await authorize({ body: { client_id } }) const url = new URL(redirect_uri) url.searchParams.set('code', code) + if (state) url.searchParams.set('state', state) globalThis.location.href = url.toString() } catch (error: unknown) { const message = error instanceof Error ? error.message : String(error)