diff --git a/ui/litellm-dashboard/src/app/login/LoginPage.test.tsx b/ui/litellm-dashboard/src/app/login/LoginPage.test.tsx index cd58c51a86..25233725da 100644 --- a/ui/litellm-dashboard/src/app/login/LoginPage.test.tsx +++ b/ui/litellm-dashboard/src/app/login/LoginPage.test.tsx @@ -18,7 +18,8 @@ vi.mock("@/app/(dashboard)/hooks/uiConfig/useUIConfig", () => ({ })); vi.mock("@/utils/cookieUtils", () => ({ - getCookie: vi.fn(), + clearTokenCookies: vi.fn(), + getCookieFromDocument: vi.fn(), })); vi.mock("@/utils/jwtUtils", () => ({ @@ -53,7 +54,7 @@ vi.mock("@/hooks/useWorker", () => ({ })); import { useUIConfig } from "@/app/(dashboard)/hooks/uiConfig/useUIConfig"; -import { getCookie } from "@/utils/cookieUtils"; +import { getCookieFromDocument } from "@/utils/cookieUtils"; import { isJwtExpired } from "@/utils/jwtUtils"; const createQueryClient = () => @@ -83,7 +84,7 @@ describe("LoginPage", () => { }, isLoading: false, }); - (getCookie as ReturnType).mockReturnValue(null); + (getCookieFromDocument as ReturnType).mockReturnValue(null); const queryClient = createQueryClient(); render( @@ -108,7 +109,7 @@ describe("LoginPage", () => { }, isLoading: false, }); - (getCookie as ReturnType).mockReturnValue(validToken); + (getCookieFromDocument as ReturnType).mockReturnValue(validToken); (isJwtExpired as ReturnType).mockReturnValue(false); const queryClient = createQueryClient(); @@ -134,7 +135,7 @@ describe("LoginPage", () => { }, isLoading: false, }); - (getCookie as ReturnType).mockReturnValue(invalidToken); + (getCookieFromDocument as ReturnType).mockReturnValue(invalidToken); (isJwtExpired as ReturnType).mockReturnValue(true); const queryClient = createQueryClient(); @@ -160,7 +161,7 @@ describe("LoginPage", () => { }, isLoading: false, }); - (getCookie as ReturnType).mockReturnValue(invalidToken); + (getCookieFromDocument as ReturnType).mockReturnValue(invalidToken); (isJwtExpired as ReturnType).mockReturnValue(true); const queryClient = createQueryClient(); @@ -189,7 +190,7 @@ describe("LoginPage", () => { }, isLoading: false, }); - (getCookie as ReturnType).mockReturnValue(validToken); + (getCookieFromDocument as ReturnType).mockReturnValue(validToken); (isJwtExpired as ReturnType).mockReturnValue(false); const queryClient = createQueryClient(); @@ -216,7 +217,7 @@ describe("LoginPage", () => { }, isLoading: false, }); - (getCookie as ReturnType).mockReturnValue(null); + (getCookieFromDocument as ReturnType).mockReturnValue(null); const queryClient = createQueryClient(); render( @@ -244,7 +245,7 @@ describe("LoginPage", () => { }, isLoading: false, }); - (getCookie as ReturnType).mockReturnValue(null); + (getCookieFromDocument as ReturnType).mockReturnValue(null); (isJwtExpired as ReturnType).mockReturnValue(true); const queryClient = createQueryClient(); @@ -271,7 +272,7 @@ describe("LoginPage", () => { }, isLoading: false, }); - (getCookie as ReturnType).mockReturnValue(null); + (getCookieFromDocument as ReturnType).mockReturnValue(null); (isJwtExpired as ReturnType).mockReturnValue(true); const queryClient = createQueryClient(); @@ -324,7 +325,7 @@ describe("LoginPage", () => { }, isLoading: false, }); - (getCookie as ReturnType).mockReturnValue(null); + (getCookieFromDocument as ReturnType).mockReturnValue(null); (isJwtExpired as ReturnType).mockReturnValue(false); const queryClient = createQueryClient(); @@ -352,7 +353,7 @@ describe("LoginPage", () => { }, isLoading: false, }); - (getCookie as ReturnType).mockReturnValue("legitimate-session-jwt"); + (getCookieFromDocument as ReturnType).mockReturnValue("legitimate-session-jwt"); (isJwtExpired as ReturnType).mockReturnValue(false); const queryClient = createQueryClient(); diff --git a/ui/litellm-dashboard/src/app/login/LoginPage.tsx b/ui/litellm-dashboard/src/app/login/LoginPage.tsx index 74ee9f9de5..2db9594730 100644 --- a/ui/litellm-dashboard/src/app/login/LoginPage.tsx +++ b/ui/litellm-dashboard/src/app/login/LoginPage.tsx @@ -4,7 +4,7 @@ import { useLogin } from "@/app/(dashboard)/hooks/login/useLogin"; import { useUIConfig } from "@/app/(dashboard)/hooks/uiConfig/useUIConfig"; import LoadingScreen from "@/components/common_components/LoadingScreen"; import { exchangeLoginCode, getProxyBaseUrl, switchToWorkerUrl } from "@/components/networking"; -import { clearTokenCookies, getCookie } from "@/utils/cookieUtils"; +import { clearTokenCookies, getCookieFromDocument } from "@/utils/cookieUtils"; import { isJwtExpired } from "@/utils/jwtUtils"; import { consumeReturnUrl, getReturnUrl, isValidReturnUrl } from "@/utils/returnUrlUtils"; import { InfoCircleOutlined, CloudServerOutlined } from "@ant-design/icons"; @@ -74,7 +74,7 @@ function LoginPageContent() { return; } - const rawToken = getCookie("token"); + const rawToken = getCookieFromDocument("token"); if (rawToken && !isJwtExpired(rawToken)) { // User already logged in - redirect to return URL or default const returnUrl = consumeReturnUrl(); diff --git a/ui/litellm-dashboard/src/utils/cookieUtils.ts b/ui/litellm-dashboard/src/utils/cookieUtils.ts index da232e72e2..66eb807ed2 100644 --- a/ui/litellm-dashboard/src/utils/cookieUtils.ts +++ b/ui/litellm-dashboard/src/utils/cookieUtils.ts @@ -105,22 +105,35 @@ export function storeLoginToken(token: string) { } } +/** + * Reads a cookie value directly from document.cookie with no fallback. + * + * Use this in flows that decide whether to redirect on the basis of "is the user + * still authenticated?". sessionStorage is per-origin and survives a logout + * triggered from a different origin (e.g. dev UI on :3000 cannot reach + * sessionStorage on the proxy origin :4000), which produces an infinite + * logout/login redirect. + */ +export function getCookieFromDocument(name: string) { + if (typeof document === "undefined") return null; + const row = document.cookie.split("; ").find((r) => r.startsWith(name + "=")); + if (!row) return null; + const raw = row.split("=").slice(1).join("="); + try { + return decodeURIComponent(raw); + } catch { + return raw; + } +} + /** * Gets a cookie value by name * @param name The name of the cookie to retrieve * @returns The cookie value or null if not found */ export function getCookie(name: string) { - if (typeof document === "undefined") return null; - const row = document.cookie.split("; ").find((r) => r.startsWith(name + "=")); - if (row) { - const raw = row.split("=").slice(1).join("="); - try { - return decodeURIComponent(raw); - } catch { - return raw; - } - } + const fromCookie = getCookieFromDocument(name); + if (fromCookie !== null) return fromCookie; // Fallback to sessionStorage — covers the case where a reverse proxy // added HttpOnly to the server-set cookie, making it invisible to JS. if (name === "token" && typeof window !== "undefined") {