From 86f254e9391b71123b2ca0a774549b09b449b21c Mon Sep 17 00:00:00 2001 From: Stockboy Steve Date: Fri, 26 Jun 2026 12:05:38 +0000 Subject: [PATCH] fix(GRO-2572): follow Better Auth redirect URL from signIn.social response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Better Auth's signIn.social() returns { data: { redirect: true, url } } rather than issuing an HTTP 30x when using the fetch client. The LoginPage handler was discarding data.url, so the SSO button appeared to do nothing (the button disabled but the user never left /login). Fix: after the social sign-in call, if result.data.url is present, navigate via window.location.href. Also add an early return in the error branch so the two paths don't bleed into each other. Updated UAT_PLAYBOOK.md ยง5.4.1 TC-WEB-SSO-2 to require a fresh/incognito context so a stale auth cookie can't mask the regression. Co-Authored-By: Paperclip --- UAT_PLAYBOOK.md | 2 +- src/App.tsx | 6 ++++ src/__tests__/App.test.tsx | 60 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/UAT_PLAYBOOK.md b/UAT_PLAYBOOK.md index d19ae97..9bbef37 100644 --- a/UAT_PLAYBOOK.md +++ b/UAT_PLAYBOOK.md @@ -86,7 +86,7 @@ export const { signIn, signOut, useSession, changePassword } = authClient; | # | Scenario | Steps | Pass Criteria | Fail Criteria | |---|----------|-------|---------------|---------------| | TC-WEB-SSO-1 | Sign-in page shows SSO button | Navigate to app root URL | Sign-in page displayed with "Sign in with SSO" button visible | No SSO button, 403 before page loads | -| TC-WEB-SSO-2 | Click SSO redirects to Authentik | Click "Sign in with SSO" button | Browser redirected to Authentik login at auth.farh.net | No redirect, error shown, button does nothing | +| TC-WEB-SSO-2 | Click SSO redirects to Authentik (GRO-2572) | **Fresh session only (no pre-existing auth cookie).** Click "Sign in with SSO" button | Browser navigates to Authentik login at auth.farh.net within ~1 s โ€” address bar changes to auth.farh.net URL | No redirect, error shown, button stays disabled, user remains on /login. Regression: prior to GRO-2572 fix the client never followed the `data.url` returned by Better Auth. Run from a clean incognito context to avoid a stale cookie masking the defect. | | TC-WEB-SSO-3 | Valid OIDC credentials authenticate | At Authentik, enter valid credentials and authenticate | Redirected back to app with active session | Redirect loop, 403, session not established | | TC-WEB-SSO-4 | Post-login dashboard accessible | After SSO flow completes, dashboard loads | Dashboard displays correctly with user identity shown | Blank page, 403, session not active | | TC-WEB-SSO-5 | User identity displayed correctly | After SSO login, check header/nav | User name/email/initials shown in nav, role reflected in UI | No user indicator, wrong user shown | diff --git a/src/App.tsx b/src/App.tsx index d6a8057..bf98a37 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -45,6 +45,12 @@ function LoginPage() { if (result?.error) { setError(result.error.message ?? "Sign-in failed"); setIsLoading(false); + return; + } + // Better Auth returns the IdP authorize URL in data.url with redirect:true rather than + // issuing an HTTP 30x โ€” the client must follow it (GRO-2572). + if (result?.data?.url) { + window.location.href = result.data.url; } }; diff --git a/src/__tests__/App.test.tsx b/src/__tests__/App.test.tsx index edddc4f..c0d898d 100644 --- a/src/__tests__/App.test.tsx +++ b/src/__tests__/App.test.tsx @@ -1,5 +1,6 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, within, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { MemoryRouter } from "react-router-dom"; import { App } from "../App"; @@ -232,6 +233,7 @@ describe("Dev login selector", () => { }); it("does not redirect when a dev user is already selected", async () => { + localStorage.setItem("dev-user", JSON.stringify({ type: "staff", id: "s1", name: "Sarah" })); global.fetch = vi.fn((url: string) => { @@ -269,3 +271,61 @@ describe("Dev login selector", () => { ).toBeInTheDocument(); }); }); + +describe("GRO-2572 โ€” SSO button follows redirect URL", () => { + it("navigates to data.url when signIn.social returns a redirect", async () => { + // Mock signIn.social to return the redirect payload Better Auth sends + vi.mock("../lib/auth-client.js", () => ({ + useSession: () => ({ data: null, isPending: false }), + signIn: { + social: vi.fn().mockResolvedValue({ + data: { redirect: true, url: "https://auth.farh.net/application/o/authorize/?test=1" }, + error: null, + }), + }, + signOut: vi.fn(), + changePassword: vi.fn(), + })); + + const assignMock = vi.fn(); + Object.defineProperty(window, "location", { + value: { ...window.location, href: "", origin: "https://uat.groombook.dev" }, + writable: true, + }); + Object.defineProperty(window.location, "href", { + set: assignMock, + get: () => "", + }); + + global.fetch = vi.fn((url: string) => { + if (url === "/api/dev/config") { + return Promise.resolve({ ok: true, json: async () => ({ authDisabled: false }) } as Response); + } + if (url === "/api/auth/get-session") { + return Promise.resolve({ ok: true, json: async () => null } as unknown as Response); + } + if (url === "/api/setup/status") { + return Promise.resolve({ ok: true, json: async () => ({ needsSetup: false }) } as Response); + } + if (url === "/api/auth/providers") { + return Promise.resolve({ ok: true, json: async () => ({ providers: ["authentik"] }) } as Response); + } + return Promise.resolve({ ok: true, json: async () => [] } as Response); + }) as unknown as typeof fetch; + + render( + + + + ); + + const ssoButton = await screen.findByRole("button", { name: /sign in with sso/i }); + await userEvent.click(ssoButton); + + await waitFor(() => { + expect(assignMock).toHaveBeenCalledWith( + "https://auth.farh.net/application/o/authorize/?test=1" + ); + }); + }); +}); -- 2.52.0