diff --git a/src/__tests__/pages/auth/signin.test.jsx b/src/__tests__/pages/auth/signin.test.jsx index 7d016e8a0..2f699b5f9 100644 --- a/src/__tests__/pages/auth/signin.test.jsx +++ b/src/__tests__/pages/auth/signin.test.jsx @@ -3,10 +3,11 @@ import { render, screen, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; -const { getSettingsMock, authOptionsMock, signInMock, routerQuery } = vi.hoisted(() => ({ +const { getSettingsMock, authOptionsMock, signInMock, replaceMock, routerQuery } = vi.hoisted(() => ({ getSettingsMock: vi.fn(), authOptionsMock: vi.fn(), signInMock: vi.fn(), + replaceMock: vi.fn(), routerQuery: {}, })); @@ -23,6 +24,7 @@ vi.mock("pages/api/auth/[...nextauth]", () => ({ vi.mock("next/router", () => ({ useRouter: () => ({ query: routerQuery, + replace: replaceMock, }), })); @@ -37,6 +39,7 @@ describe("pages/auth/signin", () => { beforeEach(() => { vi.clearAllMocks(); Object.keys(routerQuery).forEach((key) => delete routerQuery[key]); + window.sessionStorage.clear(); }); it("renders an error state when no providers are configured", async () => { @@ -106,11 +109,14 @@ describe("pages/auth/signin", () => { expect(screen.getByRole("button", { name: /login via homepage oidc/i })).toBeInTheDocument(); }); - it("renders the button when the server disabled auto-login", () => { - render(); + it("stops auto-login and hands back the page when the session never sticks", () => { + routerQuery.callbackUrl = "/some/page"; - expect(signInMock).not.toHaveBeenCalled(); - expect(screen.getByRole("button", { name: /login via homepage oidc/i })).toBeInTheDocument(); + render(); + render(); + + expect(signInMock).toHaveBeenCalledTimes(1); + expect(replaceMock).toHaveBeenCalledWith("/auth/signin?autologin=0&callbackUrl=%2Fsome%2Fpage"); }); it("does not auto-login the password provider", () => { @@ -173,17 +179,6 @@ describe("pages/auth/signin", () => { vi.unstubAllEnvs(); }); - it("getServerSideProps disables auto-login while an attempt is pending", async () => { - authOptionsMock.mockReturnValueOnce({ providers: [] }); - getSettingsMock.mockReturnValueOnce({ theme: "dark" }); - vi.stubEnv("HOMEPAGE_OIDC_AUTO_LOGIN", "true"); - - const res = await getServerSideProps({ req: { cookies: { "homepage-autologin-attempt": "1" } } }); - - expect(res.props.autoLogin).toBe(false); - vi.unstubAllEnvs(); - }); - it("getServerSideProps falls back to no providers when auth options fail to load", async () => { const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}); authOptionsMock.mockImplementationOnce(() => { diff --git a/src/pages/auth/signin.jsx b/src/pages/auth/signin.jsx index d3125e38d..0fd4afcbd 100644 --- a/src/pages/auth/signin.jsx +++ b/src/pages/auth/signin.jsx @@ -7,8 +7,8 @@ import { BiShieldQuarter } from "react-icons/bi"; import { getSettings } from "utils/config/config"; const PUBLIC_SIGN_IN_SETTINGS = ["theme", "color", "title", "background", "backgroundOpacity"]; -const AUTO_LOGIN_COOKIE = "homepage-autologin-attempt"; -const AUTO_LOGIN_RETRY_SECONDS = 10; +const AUTO_LOGIN_KEY = "homepage-autologin-attempt"; +const AUTO_LOGIN_RETRY_MS = 10000; export default function SignIn({ providers, settings, autoLogin }) { const router = useRouter(); @@ -33,12 +33,21 @@ export default function SignIn({ providers, settings, autoLogin }) { useEffect(() => { if (!redirecting) return; - // getServerSideProps drops autoLogin while this is set, so a bounce loop falls back to the button - const secure = window.location.protocol === "https:" ? "; secure" : ""; - document.cookie = `${AUTO_LOGIN_COOKIE}=1; path=/auth; max-age=${AUTO_LOGIN_RETRY_SECONDS}; samesite=lax${secure}`; + let lastAttempt = 0; + try { + lastAttempt = Number(window.sessionStorage.getItem(AUTO_LOGIN_KEY)) || 0; + window.sessionStorage.setItem(AUTO_LOGIN_KEY, String(Date.now())); + } catch { + // sessionStorage throws when site data is blocked, fall through and redirect anyway + } + // Getting here quickly means the session never stuck, hand it back to the user + if (Date.now() - lastAttempt < AUTO_LOGIN_RETRY_MS) { + router.replace(`/auth/signin?autologin=0&callbackUrl=${encodeURIComponent(callbackUrl)}`); + return; + } signIn(oidcProvider.id, { callbackUrl }); - }, [redirecting, oidcProvider, callbackUrl]); + }, [redirecting, oidcProvider, callbackUrl, router]); let backgroundImage = ""; let opacity = settings?.backgroundOpacity ?? 0; @@ -243,14 +252,7 @@ export async function getServerSideProps(context) { homepageSettings[key], ]), ); - // A pending attempt means the previous redirect never established a session - const autoLoginAttempted = context.req?.cookies?.[AUTO_LOGIN_COOKIE] === "1"; - return { - props: { - providers, - settings, - autoLogin: process.env.HOMEPAGE_OIDC_AUTO_LOGIN === "true" && !autoLoginAttempted, - }, + props: { providers, settings, autoLogin: process.env.HOMEPAGE_OIDC_AUTO_LOGIN === "true" }, }; }