From 7f0a5e96738e4dc3f0ec98e1205bca2ad439e4ee Mon Sep 17 00:00:00 2001 From: Syed Ali Abbas Rahil Date: Tue, 7 Jul 2026 20:14:14 +0900 Subject: [PATCH] fix(proxy): scope auth token cookie path to SERVER_ROOT_PATH When SERVER_ROOT_PATH is set, the login token cookie was written with the default path of "/", so deployments served under different root paths on the same host (for example a.com and a.com/prefix) overwrote each other's auth cookie and bounced users to /sso/key/generate. Set the cookie path to the normalized server root path so cookies stay isolated per deployment. --- litellm/proxy/management_endpoints/ui_sso.py | 3 ++- litellm/proxy/proxy_server.py | 7 ++++--- litellm/proxy/utils.py | 14 +++++++++++++ tests/test_litellm/proxy/test_proxy_utils.py | 22 +++++++++++++++++++- 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/litellm/proxy/management_endpoints/ui_sso.py b/litellm/proxy/management_endpoints/ui_sso.py index 46af5dd80e1..cba0cb4b38b 100644 --- a/litellm/proxy/management_endpoints/ui_sso.py +++ b/litellm/proxy/management_endpoints/ui_sso.py @@ -118,6 +118,7 @@ from litellm.proxy.management_endpoints.types import ( from litellm.proxy.utils import ( PrismaClient, ProxyLogging, + get_cookie_path_from_server_root_path, get_custom_url, get_server_root_path, ) @@ -3637,7 +3638,7 @@ class SSOAuthenticationHandler: litellm_dashboard_ui += "?login=success" verbose_proxy_logger.info("Redirecting to %s", litellm_dashboard_ui) redirect_response: Final = RedirectResponse(url=litellm_dashboard_ui, status_code=303) - redirect_response.set_cookie(key="token", value=jwt_token) + redirect_response.set_cookie(key="token", value=jwt_token, path=get_cookie_path_from_server_root_path()) return redirect_response @staticmethod diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 56036713fa9..dd8668dd82a 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -605,6 +605,7 @@ from litellm.proxy.utils import ( _is_valid_team_configs, evict_config_param, get_config_param, + get_cookie_path_from_server_root_path, get_custom_url, get_error_message_str, get_server_root_path, @@ -14631,7 +14632,7 @@ async def login(request: Request): # Create redirect response with cookie redirect_response: Final = RedirectResponse(url=litellm_dashboard_ui, status_code=303) - redirect_response.set_cookie(key="token", value=jwt_token) + redirect_response.set_cookie(key="token", value=jwt_token, path=get_cookie_path_from_server_root_path()) if cp_return_to: redirect_response.delete_cookie(key="litellm_cp_return_to") return redirect_response @@ -14674,7 +14675,7 @@ async def login_v2(request: Request): content={"redirect_url": litellm_dashboard_ui, "token": jwt_token}, status_code=status.HTTP_200_OK, ) - json_response.set_cookie(key="token", value=jwt_token) + json_response.set_cookie(key="token", value=jwt_token, path=get_cookie_path_from_server_root_path()) return json_response except Exception as e: verbose_proxy_logger.exception("litellm.proxy.proxy_server.login_v2(): Exception occurred - %s", e) @@ -14819,7 +14820,7 @@ async def login_v3_exchange(request: Request): }, status_code=status.HTTP_200_OK, ) - json_response.set_cookie(key="token", value=cached_data["token"]) + json_response.set_cookie(key="token", value=cached_data["token"], path=get_cookie_path_from_server_root_path()) return json_response except ProxyException: raise diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 2ad7180bd5f..483fee61d89 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -6739,6 +6739,20 @@ def get_server_root_path() -> str: return os.getenv("SERVER_ROOT_PATH", "") +def get_cookie_path_from_server_root_path() -> str: + """ + Cookie `path` scoped to SERVER_ROOT_PATH. + + Ensures auth cookies for deployments served under different root paths + (e.g. `a.com` vs `a.com/prefix`) do not overwrite each other. Defaults to + "/" when SERVER_ROOT_PATH is unset. + """ + root_path = get_server_root_path() + if not root_path or root_path == "/": + return "/" + return "/" + root_path.strip("/") + + def normalize_route_for_root_path(route: str) -> str | None: """Strip SERVER_ROOT_PATH prefix. Returns de-prefixed route, or None if route is not under root path.""" root_path: Final = get_server_root_path() diff --git a/tests/test_litellm/proxy/test_proxy_utils.py b/tests/test_litellm/proxy/test_proxy_utils.py index 1504c3c3103..7574ded5560 100644 --- a/tests/test_litellm/proxy/test_proxy_utils.py +++ b/tests/test_litellm/proxy/test_proxy_utils.py @@ -19,7 +19,7 @@ sys.path.insert( from unittest.mock import MagicMock, patch -from litellm.proxy.utils import get_custom_url, join_paths +from litellm.proxy.utils import get_cookie_path_from_server_root_path, get_custom_url, join_paths def test_get_custom_url(monkeypatch): @@ -28,6 +28,26 @@ def test_get_custom_url(monkeypatch): assert custom_url == "http://0.0.0.0:4000/litellm/ui/" +@pytest.mark.parametrize( + "server_root_path, expected", + [ + (None, "/"), + ("", "/"), + ("/", "/"), + ("/litellm", "/litellm"), + ("litellm", "/litellm"), + ("/litellm/", "/litellm"), + ("/team/a", "/team/a"), + ], +) +def test_get_cookie_path_from_server_root_path(monkeypatch, server_root_path, expected): + if server_root_path is None: + monkeypatch.delenv("SERVER_ROOT_PATH", raising=False) + else: + monkeypatch.setenv("SERVER_ROOT_PATH", server_root_path) + assert get_cookie_path_from_server_root_path() == expected + + def test_proxy_only_error_true_for_llm_route(): proxy_logging_obj = ProxyLogging(user_api_key_cache=DualCache()) assert proxy_logging_obj._is_proxy_only_llm_api_error(