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.
This commit is contained in:
Syed Ali Abbas Rahil 2026-07-07 20:14:14 +09:00
parent 007bd43cfb
commit 7f0a5e9673
4 changed files with 41 additions and 5 deletions

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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(