diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 579e90e530c..b3000609265 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -41,8 +41,33 @@ import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { cx } from "@/lib/cva.config"; function getCookie(name: string) { - const cookieValue = document.cookie.split("; ").find((row) => row.startsWith(name + "=")); - return cookieValue ? cookieValue.split("=")[1] : null; + // Safer cookie read + decoding; handles '=' inside values + const match = document.cookie.split("; ").find((row) => row.startsWith(name + "=")); + if (!match) return null; + const value = match.slice(name.length + 1); + try { + return decodeURIComponent(value); + } catch { + return value; + } +} + +function deleteCookie(name: string, path = "/") { + // Best-effort client-side clear (works for non-HttpOnly cookies without Domain) + document.cookie = `${name}=; Max-Age=0; Path=${path}`; +} + +function isJwtExpired(token: string): boolean { + try { + const decoded: any = jwtDecode(token); + if (decoded && typeof decoded.exp === "number") { + return decoded.exp * 1000 <= Date.now(); + } + return false; + } catch { + // If we can't decode, treat as invalid/expired + return true; + } } function formatUserRole(userRole: string) { @@ -149,17 +174,42 @@ export default function CreateKeyPage() { const redirectToLogin = authLoading === false && token === null && invitation_id === null; useEffect(() => { - const token = getCookie("token"); - getUiConfig().then((data) => { - // get the information for constructing the proxy base url, and then set the token and auth loading - setToken(token); - setAuthLoading(false); - }); + let cancelled = false; + + (async () => { + try { + await getUiConfig(); // ensures proxyBaseUrl etc. are ready + } catch { + // proceed regardless; we still need to decide auth state + } + + if (cancelled) return; + + const raw = getCookie("token"); + const valid = raw && !isJwtExpired(raw) ? raw : null; + + // If token exists but is invalid/expired, clear it so downstream code + // doesn't keep trying to use it and cause redirect spasms. + if (raw && !valid) { + deleteCookie("token", "/"); + } + + if (!cancelled) { + setToken(valid); + setAuthLoading(false); + } + })(); + + return () => { + cancelled = true; + }; }, []); useEffect(() => { if (redirectToLogin) { - window.location.href = (proxyBaseUrl || "") + "/sso/key/generate"; + // Replace instead of assigning to avoid back-button loops + const dest = (proxyBaseUrl || "") + "/sso/key/generate"; + window.location.replace(dest); } }, [redirectToLogin]); @@ -168,7 +218,23 @@ export default function CreateKeyPage() { return; } - const decoded = jwtDecode(token) as { [key: string]: any }; + // Defensive: re-check expiry in case cookie changed after mount + if (isJwtExpired(token)) { + deleteCookie("token", "/"); + setToken(null); + return; + } + + let decoded: any = null; + try { + decoded = jwtDecode(token); + } catch { + // Malformed token → treat as unauthenticated + deleteCookie("token", "/"); + setToken(null); + return; + } + if (decoded) { // set accessToken setAccessToken(decoded.key);