diff --git a/docs/my-website/docs/proxy/cli_sso.md b/docs/my-website/docs/proxy/cli_sso.md index ad0f033f802..a20f8a313d4 100644 --- a/docs/my-website/docs/proxy/cli_sso.md +++ b/docs/my-website/docs/proxy/cli_sso.md @@ -52,6 +52,10 @@ LITELLM_CLI_JWT_EXPIRATION_HOURS=48 EXPERIMENTAL_UI_LOGIN="True" litellm --confi - `LITELLM_CLI_JWT_EXPIRATION_HOURS=168` - Tokens expire after 7 days (168 hours) - `LITELLM_CLI_JWT_EXPIRATION_HOURS=720` - Tokens expire after 30 days (720 hours) +:::note[Experimental UI Session] +When `EXPERIMENTAL_UI_LOGIN` is enabled, the **browser UI login** session uses a fixed 10-minute expiry (not configurable). `LITELLM_UI_SESSION_DURATION` applies only to non-experimental flows. +::: + :::tip You can check your current token's age and expiration status using: ```bash diff --git a/docs/my-website/docs/proxy/config_settings.md b/docs/my-website/docs/proxy/config_settings.md index 302259179c3..e302e2171f6 100644 --- a/docs/my-website/docs/proxy/config_settings.md +++ b/docs/my-website/docs/proxy/config_settings.md @@ -777,6 +777,7 @@ router_settings: | LITELLM_HOSTED_UI | URL of the hosted UI for LiteLLM | LITELLM_UI_API_DOC_BASE_URL | Optional override for the API Reference base URL (used in sample code/docs) when the admin UI runs on a different host than the proxy. Defaults to `PROXY_BASE_URL` when unset. | LITELLM_UI_PATH | Path to directory for Admin UI files. Used when running with read-only filesystem (e.g., Kubernetes). Default is `/var/lib/litellm/ui` in Docker. +| LITELLM_UI_SESSION_DURATION | Duration for UI login session (username/password, SSO, invitation links). Format: "30s", "30m", "24h", "7d". Does not apply to EXPERIMENTAL_UI_LOGIN flow, which uses a fixed 10-minute expiry for security. Default is "24h" | LITELM_ENVIRONMENT | Environment of LiteLLM Instance, used by logging services. Currently only used by DeepEval. | LITELLM_KEY_ROTATION_ENABLED | Enable auto-key rotation for LiteLLM (boolean). Default is false. | LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS | Interval in seconds for how often to run job that auto-rotates keys. Default is 86400 (24 hours). diff --git a/litellm/__init__.py b/litellm/__init__.py index 3c61aca3b8e..2522d190570 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -12,6 +12,13 @@ warnings.filterwarnings( ### INIT VARIABLES ######################### import threading import os + +# Load .env before any other litellm imports so env vars (e.g. LITELLM_UI_SESSION_DURATION) are available +import dotenv as _dotenv + +if os.getenv("LITELLM_MODE", "DEV") == "DEV": + _dotenv.load_dotenv() + from typing import ( Callable, List, @@ -74,12 +81,9 @@ from litellm.constants import ( DEFAULT_ALLOWED_FAILS, ) import httpx -import dotenv # register_async_client_cleanup is lazy-loaded and called on first access litellm_mode = os.getenv("LITELLM_MODE", "DEV") # "PRODUCTION", "DEV" -if litellm_mode == "DEV": - dotenv.load_dotenv() #################################################### diff --git a/litellm/constants.py b/litellm/constants.py index 4c38ecd74b5..871b7e5a80b 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -1322,6 +1322,11 @@ CLI_JWT_EXPIRATION_HOURS = int( or 24 ) +########################### UI SESSION DURATION ########################### +# Duration for UI login session (username/password, SSO, invitation links). Format: "30s", "30m", "24h", "7d" +# Does NOT apply to EXPERIMENTAL_UI_LOGIN flow, which intentionally uses a fixed 10-minute expiry for security. +LITELLM_UI_SESSION_DURATION = os.getenv("LITELLM_UI_SESSION_DURATION", "24h") + ########################### DB CRON JOB NAMES ########################### DB_SPEND_UPDATE_JOB_NAME = "db_spend_update_job" PROMETHEUS_EMIT_BUDGET_METRICS_JOB_NAME = "prometheus_emit_budget_metrics" diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index 500a39d9455..ef6b0ac462c 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -1884,7 +1884,7 @@ class ExperimentalUIJWTToken: if user_info.user_role is None: raise Exception("User role is required for experimental UI login") - # Calculate expiration time (10 minutes from now) + # Experimental UI flow uses fixed 10-min expiry for security (does not use LITELLM_UI_SESSION_DURATION) expiration_time = get_utc_datetime() + timedelta(minutes=10) # Format the expiration time as ISO 8601 string diff --git a/litellm/proxy/auth/login_utils.py b/litellm/proxy/auth/login_utils.py index 4df773dec2b..c7e22516fe5 100644 --- a/litellm/proxy/auth/login_utils.py +++ b/litellm/proxy/auth/login_utils.py @@ -12,7 +12,7 @@ from typing import Literal, Optional, cast from fastapi import HTTPException import litellm -from litellm.constants import LITELLM_PROXY_ADMIN_NAME +from litellm.constants import LITELLM_PROXY_ADMIN_NAME, LITELLM_UI_SESSION_DURATION from litellm.proxy._types import ( LiteLLM_UserTable, LitellmUserRoles, @@ -178,7 +178,7 @@ async def authenticate_user( # noqa: PLR0915 request_type="key", **{ "user_role": LitellmUserRoles.PROXY_ADMIN, - "duration": "24hr", + "duration": LITELLM_UI_SESSION_DURATION, "key_max_budget": litellm.max_ui_session_budget, "models": [], "aliases": {}, @@ -264,7 +264,7 @@ async def authenticate_user( # noqa: PLR0915 request_type="key", **{ # type: ignore "user_role": user_role, - "duration": "24hr", + "duration": LITELLM_UI_SESSION_DURATION, "key_max_budget": litellm.max_ui_session_budget, "models": [], "aliases": {}, diff --git a/litellm/proxy/management_endpoints/ui_sso.py b/litellm/proxy/management_endpoints/ui_sso.py index 5a1b31aebb8..84e945e8883 100644 --- a/litellm/proxy/management_endpoints/ui_sso.py +++ b/litellm/proxy/management_endpoints/ui_sso.py @@ -25,6 +25,7 @@ from litellm._logging import verbose_proxy_logger from litellm._uuid import uuid from litellm.caching import DualCache from litellm.constants import ( + LITELLM_UI_SESSION_DURATION, MAX_SPENDLOG_ROWS_TO_QUERY, MICROSOFT_USER_DISPLAY_NAME_ATTRIBUTE, MICROSOFT_USER_EMAIL_ATTRIBUTE, @@ -2237,7 +2238,7 @@ class SSOAuthenticationHandler: # User might not be already created on first generation of key # But if it is, we want their models preferences default_ui_key_values: Dict[str, Any] = { - "duration": "24hr", + "duration": LITELLM_UI_SESSION_DURATION, "key_max_budget": litellm.max_ui_session_budget, "aliases": {}, "config": {}, diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 3390caa8731..54ad361c749 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -52,6 +52,7 @@ from litellm.constants import ( LITELLM_EMBEDDING_PROVIDERS_SUPPORTING_INPUT_ARRAY_OF_TOKENS, LITELLM_SETTINGS_SAFE_DB_OVERRIDES, LITELLM_UI_ALLOW_HEADERS, + LITELLM_UI_SESSION_DURATION, ) from litellm.litellm_core_utils.litellm_logging import ( _init_custom_logger_compatible_class, @@ -10847,7 +10848,7 @@ async def onboarding(invite_link: str, request: Request): request_type="key", **{ "user_role": user_obj.user_role, - "duration": "24hr", + "duration": LITELLM_UI_SESSION_DURATION, "key_max_budget": litellm.max_ui_session_budget, "models": [], "aliases": {}, diff --git a/tests/test_litellm/proxy/auth/test_auth_checks.py b/tests/test_litellm/proxy/auth/test_auth_checks.py index 1d8d1be58c7..4cdca2d0617 100644 --- a/tests/test_litellm/proxy/auth/test_auth_checks.py +++ b/tests/test_litellm/proxy/auth/test_auth_checks.py @@ -108,11 +108,55 @@ def test_get_experimental_ui_login_jwt_auth_token_valid(valid_sso_user_defined_v assert token_data["models"] == ["gpt-3.5-turbo"] assert token_data["max_budget"] == litellm.max_ui_session_budget - # Verify expiration time is set and valid + # Verify expiration time is set and valid (Experimental UI uses fixed 10-min expiry) assert "expires" in token_data expires = datetime.fromisoformat(token_data["expires"].replace("Z", "+00:00")) - assert expires > get_utc_datetime() - assert expires <= get_utc_datetime() + timedelta(minutes=10) + now = get_utc_datetime() + # Allow 2 second buffer for test execution timing + assert expires > now + assert expires <= now + timedelta(minutes=10, seconds=2) + + +def test_get_experimental_ui_login_jwt_auth_token_uses_10_min_expiry( + valid_sso_user_defined_values, +): + """Test that Experimental UI token uses fixed 10-minute expiry (does not use LITELLM_UI_SESSION_DURATION).""" + token = ExperimentalUIJWTToken.get_experimental_ui_login_jwt_auth_token( + valid_sso_user_defined_values + ) + decrypted_token = decrypt_value_helper( + token, key="ui_hash_key", exception_type="debug" + ) + assert decrypted_token is not None + token_data = json.loads(decrypted_token) + expires = datetime.fromisoformat(token_data["expires"].replace("Z", "+00:00")) + now = get_utc_datetime() + # Should expire in ~10 minutes (allow 2 second buffer) + assert expires > now + timedelta(minutes=9) + assert expires <= now + timedelta(minutes=10, seconds=2) + + +def test_experimental_ui_token_ignores_litellm_ui_session_duration( + valid_sso_user_defined_values, +): + """Regression test: LITELLM_UI_SESSION_DURATION must NOT affect Experimental UI token expiry. + Experimental UI intentionally uses fixed 10-min expiry. If this test fails, the constant + was incorrectly wired to the experimental flow.""" + # Default LITELLM_UI_SESSION_DURATION is "24h" - token must still expire in ~10 min + token = ExperimentalUIJWTToken.get_experimental_ui_login_jwt_auth_token( + valid_sso_user_defined_values + ) + decrypted_token = decrypt_value_helper( + token, key="ui_hash_key", exception_type="debug" + ) + assert decrypted_token is not None + token_data = json.loads(decrypted_token) + expires = datetime.fromisoformat(token_data["expires"].replace("Z", "+00:00")) + now = get_utc_datetime() + # Must be ~10 min, NOT 24h. If LITELLM_UI_SESSION_DURATION were incorrectly used, this would fail. + assert expires <= now + timedelta(minutes=11), ( + "Experimental UI must use 10-min expiry, not LITELLM_UI_SESSION_DURATION" + ) def test_get_experimental_ui_login_jwt_auth_token_invalid(