""" Login utilities for handling user authentication in the proxy server. This module contains the core login logic that can be reused across different login endpoints (e.g., /login and /v2/login). """ import os import secrets from typing import Literal, Optional, cast from fastapi import HTTPException import litellm from litellm.constants import LITELLM_PROXY_ADMIN_NAME, LITELLM_UI_SESSION_DURATION from litellm.proxy._types import ( LiteLLM_UserTable, LitellmUserRoles, ProxyErrorTypes, ProxyException, UpdateUserRequest, UserAPIKeyAuth, hash_token, ) from litellm.proxy.management_endpoints.internal_user_endpoints import user_update from litellm.proxy.management_endpoints.key_management_endpoints import ( generate_key_helper_fn, ) from litellm.proxy.management_endpoints.ui_sso import ( get_disabled_non_admin_personal_key_creation, ) from litellm.proxy.utils import PrismaClient, get_server_root_path from litellm.secret_managers.main import get_secret_bool from litellm.types.proxy.ui_sso import ReturnedUITokenObject def get_ui_credentials(master_key: Optional[str]) -> tuple[str, str]: """ Get UI username and password from environment variables or master key. Args: master_key: Master key for the proxy (used as fallback for password) Returns: tuple[str, str]: A tuple containing (ui_username, ui_password) Raises: ProxyException: If neither UI_PASSWORD nor master_key is available """ ui_username = os.getenv("UI_USERNAME", "admin") ui_password = os.getenv("UI_PASSWORD", None) if ui_password is None: ui_password = str(master_key) if master_key is not None else None if ui_password is None: raise ProxyException( message="set Proxy master key to use UI. https://docs.litellm.ai/docs/proxy/virtual_keys. If set, use `--detailed_debug` to debug issue.", type=ProxyErrorTypes.auth_error, param="UI_PASSWORD", code=500, ) return ui_username, ui_password class LoginResult: """Result object containing authentication data from login.""" user_id: str key: str user_email: Optional[str] user_role: str login_method: Literal["sso", "username_password"] def __init__( self, user_id: str, key: str, user_email: Optional[str], user_role: str, login_method: Literal["sso", "username_password"] = "username_password", ): self.user_id = user_id self.key = key self.user_email = user_email self.user_role = user_role self.login_method = login_method async def authenticate_user( # noqa: PLR0915 username: str, password: str, master_key: Optional[str], prisma_client: Optional[PrismaClient], ) -> LoginResult: """ Authenticate a user and generate an API key for UI access. This function handles two login scenarios: 1. Admin login using UI_USERNAME and UI_PASSWORD 2. User login using email and password from database Args: username: Username or email from the login form password: Password from the login form master_key: Master key for the proxy (required) prisma_client: Prisma database client (optional) Returns: LoginResult: Object containing authentication data Raises: ProxyException: If authentication fails or required configuration is missing """ if master_key is None: raise ProxyException( message="Master Key not set for Proxy. Please set Master Key to use Admin UI. Set `LITELLM_MASTER_KEY` in .env or set general_settings:master_key in config.yaml. https://docs.litellm.ai/docs/proxy/virtual_keys. If set, use `--detailed_debug` to debug issue.", type=ProxyErrorTypes.auth_error, param="master_key", code=500, ) ui_username, ui_password = get_ui_credentials(master_key) # Check if we can find the `username` in the db. On the UI, users can enter username=their email _user_row: Optional[LiteLLM_UserTable] = None user_role: Optional[ Literal[ LitellmUserRoles.PROXY_ADMIN, LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY, LitellmUserRoles.INTERNAL_USER, LitellmUserRoles.INTERNAL_USER_VIEW_ONLY, ] ] = None if prisma_client is not None: _user_row = cast( Optional[LiteLLM_UserTable], await prisma_client.db.litellm_usertable.find_first( where={"user_email": {"equals": username, "mode": "insensitive"}} ), ) """ To login to Admin UI, we support the following - Login with UI_USERNAME and UI_PASSWORD - Login with Invite Link `user_email` and `password` combination """ if secrets.compare_digest( username.encode("utf-8"), ui_username.encode("utf-8") ) and secrets.compare_digest(password.encode("utf-8"), ui_password.encode("utf-8")): # Non SSO -> If user is using UI_USERNAME and UI_PASSWORD they are Proxy admin user_role = LitellmUserRoles.PROXY_ADMIN user_id = LITELLM_PROXY_ADMIN_NAME # we want the key created to have PROXY_ADMIN_PERMISSIONS key_user_id = LITELLM_PROXY_ADMIN_NAME if ( os.getenv("PROXY_ADMIN_ID", None) is not None and os.environ["PROXY_ADMIN_ID"] == user_id ) or user_id == LITELLM_PROXY_ADMIN_NAME: # checks if user is admin key_user_id = os.getenv("PROXY_ADMIN_ID", LITELLM_PROXY_ADMIN_NAME) # Admin is Authe'd in - generate key for the UI to access Proxy # ensure this user is set as the proxy admin, in this route there is no sso, we can assume this user is only the admin await user_update( data=UpdateUserRequest( user_id=key_user_id, user_role=user_role, ), user_api_key_dict=UserAPIKeyAuth( user_role=LitellmUserRoles.PROXY_ADMIN, ), ) if os.getenv("DATABASE_URL") is not None: response = await generate_key_helper_fn( request_type="key", **{ "user_role": LitellmUserRoles.PROXY_ADMIN, "duration": LITELLM_UI_SESSION_DURATION, "key_max_budget": litellm.max_ui_session_budget, "models": [], "aliases": {}, "config": {}, "spend": 0, "user_id": key_user_id, "team_id": "litellm-dashboard", }, # type: ignore ) else: raise ProxyException( message="No Database connected. Set DATABASE_URL in .env. If set, use `--detailed_debug` to debug issue.", type=ProxyErrorTypes.auth_error, param="DATABASE_URL", code=500, ) key = response["token"] # type: ignore if get_secret_bool("EXPERIMENTAL_UI_LOGIN"): from litellm.proxy.auth.auth_checks import ExperimentalUIJWTToken user_info: Optional[LiteLLM_UserTable] = None if _user_row is not None: user_info = _user_row elif ( user_id is not None ): # if user_id is not None, we are using the UI_USERNAME and UI_PASSWORD user_info = LiteLLM_UserTable( user_id=user_id, user_role=user_role, models=[], max_budget=litellm.max_ui_session_budget, ) if user_info is None: raise HTTPException( status_code=401, detail={ "error": "User Information is required for experimental UI login" }, ) key = ExperimentalUIJWTToken.get_experimental_ui_login_jwt_auth_token( user_info ) return LoginResult( user_id=user_id, key=key, user_email=None, user_role=user_role, login_method="username_password", ) elif _user_row is not None: """ When sharing invite links -> if the user has no role in the DB assume they are only a viewer """ user_id = getattr(_user_row, "user_id", "unknown") user_role = getattr( _user_row, "user_role", LitellmUserRoles.INTERNAL_USER_VIEW_ONLY ) user_email = getattr(_user_row, "user_email", "unknown") _password = getattr(_user_row, "password", "unknown") if _password is None: raise ProxyException( message="User has no password set. Please set a password for the user via `/user/update`.", type=ProxyErrorTypes.auth_error, param="password", code=401, ) # check if password == _user_row.password hash_password = hash_token(token=password) if secrets.compare_digest( password.encode("utf-8"), _password.encode("utf-8") ) or secrets.compare_digest( hash_password.encode("utf-8"), _password.encode("utf-8") ): if os.getenv("DATABASE_URL") is not None: response = await generate_key_helper_fn( request_type="key", **{ # type: ignore "user_role": user_role, "duration": LITELLM_UI_SESSION_DURATION, "key_max_budget": litellm.max_ui_session_budget, "models": [], "aliases": {}, "config": {}, "spend": 0, "user_id": user_id, "team_id": "litellm-dashboard", }, ) else: raise ProxyException( message="No Database connected. Set DATABASE_URL in .env. If set, use `--detailed_debug` to debug issue.", type=ProxyErrorTypes.auth_error, param="DATABASE_URL", code=500, ) key = response["token"] # type: ignore return LoginResult( user_id=user_id, key=key, user_email=user_email, user_role=cast(str, user_role), login_method="username_password", ) else: raise ProxyException( message=f"Invalid credentials used to access UI.\nNot valid credentials for {username}", type=ProxyErrorTypes.auth_error, param="invalid_credentials", code=401, ) else: raise ProxyException( message="Invalid credentials used to access UI.\nCheck 'UI_USERNAME', 'UI_PASSWORD' in .env file", type=ProxyErrorTypes.auth_error, param="invalid_credentials", code=401, ) def create_ui_token_object( login_result: LoginResult, general_settings: dict, premium_user: bool, ) -> ReturnedUITokenObject: """ Create a ReturnedUITokenObject from a LoginResult. Args: login_result: The result from authenticate_user general_settings: General proxy settings dictionary premium_user: Whether premium features are enabled Returns: ReturnedUITokenObject: Token object ready for JWT encoding """ disabled_non_admin_personal_key_creation = ( get_disabled_non_admin_personal_key_creation() ) return ReturnedUITokenObject( user_id=login_result.user_id, key=login_result.key, user_email=login_result.user_email, user_role=login_result.user_role, login_method=login_result.login_method, premium_user=premium_user, auth_header_name=general_settings.get( "litellm_key_header_name", "Authorization" ), disabled_non_admin_personal_key_creation=disabled_non_admin_personal_key_creation, server_root_path=get_server_root_path(), )