feat: add exchangeLoginCode for SSO code exchange, update LoginPage

- Add exchangeLoginCode() to networking.tsx for SSO callback
- LoginPage detects ?code= from SSO redirect, exchanges for JWT
- Keep backwards compat for legacy ?token= flow
This commit is contained in:
Ryan Crabbe 2026-03-19 20:42:49 -07:00
parent 0e3705d63a
commit 7ed5692597
2 changed files with 40 additions and 3 deletions

View file

@ -3,7 +3,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 { getProxyBaseUrl, switchToWorkerUrl } from "@/components/networking";
import { exchangeLoginCode, getProxyBaseUrl, switchToWorkerUrl } from "@/components/networking";
import { clearTokenCookies, getCookie } from "@/utils/cookieUtils";
import { isJwtExpired } from "@/utils/jwtUtils";
import { consumeReturnUrl, getReturnUrl, isValidReturnUrl } from "@/utils/returnUrlUtils";
@ -43,12 +43,25 @@ function LoginPageContent() {
return;
}
// Cross-origin SSO: worker redirected back with token in URL
// Cross-origin SSO: worker redirected back with a single-use code.
// Exchange it for the JWT via the worker's /v3/login/exchange endpoint.
const params = new URLSearchParams(window.location.search);
const ssoCode = params.get("code");
if (ssoCode) {
const workerUrl = localStorage.getItem("litellm_worker_url");
exchangeLoginCode(ssoCode, workerUrl).then(() => {
params.delete("code");
const cleanSearch = params.toString();
window.history.replaceState(null, "", window.location.pathname + (cleanSearch ? `?${cleanSearch}` : ""));
router.replace("/ui/?login=success");
});
return;
}
// Backwards compat: handle direct token in URL (legacy flow)
const urlToken = params.get("token");
if (urlToken && !isJwtExpired(urlToken)) {
document.cookie = `token=${urlToken}; path=/; SameSite=Lax`;
// Strip token from URL to keep it out of browser history
params.delete("token");
const cleanSearch = params.toString();
window.history.replaceState(

View file

@ -9100,6 +9100,30 @@ export const loginCall = async (username: string, password: string, useV3?: bool
return data;
};
/**
* Exchange a single-use login code for a JWT token.
* Used by the SSO callback when the worker redirects back with ?code=.
*/
export const exchangeLoginCode = async (code: string, workerBaseUrl?: string | null): Promise<string> => {
const base = workerBaseUrl || getProxyBaseUrl();
const response = await fetch(`${base}/v3/login/exchange`, {
method: "POST",
body: JSON.stringify({ code }),
headers: { "Content-Type": "application/json" },
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(deriveErrorMessage(errorData));
}
const data = await response.json();
if (data.token) {
document.cookie = `token=${data.token}; path=/; SameSite=Lax`;
}
return data.token;
};
export const getUiSettings = async () => {
const proxyBaseUrl = getProxyBaseUrl();
const url = proxyBaseUrl ? `${proxyBaseUrl}/get/ui_settings` : `/get/ui_settings`;