From d28ecbc900584426dd36395e2b2403e196e73d9b Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 8 Oct 2025 18:45:13 -0700 Subject: [PATCH 1/4] feat(ui_sso.py): support mapping app roles from azure entra id to litellm user roles Closes LIT-1228 --- litellm/proxy/management_endpoints/types.py | 40 +++++++++- litellm/proxy/management_endpoints/ui_sso.py | 80 +++++++++++++++++++- 2 files changed, 115 insertions(+), 5 deletions(-) diff --git a/litellm/proxy/management_endpoints/types.py b/litellm/proxy/management_endpoints/types.py index 0e811669d1..ad2ad0a5fe 100644 --- a/litellm/proxy/management_endpoints/types.py +++ b/litellm/proxy/management_endpoints/types.py @@ -4,10 +4,48 @@ Types for the management endpoints Might include fastapi/proxy requirements.txt related imports """ -from typing import List +from typing import List, Optional, cast from fastapi_sso.sso.base import OpenID +from litellm.proxy._types import LitellmUserRoles + + +def is_valid_litellm_user_role(role_str: str) -> bool: + """ + Check if a string is a valid LitellmUserRoles enum value (case-insensitive). + + Args: + role_str: String to validate (e.g., "proxy_admin", "PROXY_ADMIN", "internal_user") + + Returns: + True if the string matches a valid LitellmUserRoles value, False otherwise + """ + try: + # Use _value2member_map_ for O(1) lookup, case-insensitive + return role_str.lower() in LitellmUserRoles._value2member_map_ + except Exception: + return False + + +def get_litellm_user_role(role_str: str) -> Optional[LitellmUserRoles]: + """ + Convert a string to a LitellmUserRoles enum if valid (case-insensitive). + + Args: + role_str: String to convert (e.g., "proxy_admin", "PROXY_ADMIN", "internal_user") + + Returns: + LitellmUserRoles enum if valid, None otherwise + """ + try: + # Use _value2member_map_ for O(1) lookup, case-insensitive + result = LitellmUserRoles._value2member_map_.get(role_str.lower()) + return cast(Optional[LitellmUserRoles], result) + except Exception: + return None + class CustomOpenID(OpenID): team_ids: List[str] + user_role: Optional[LitellmUserRoles] = None diff --git a/litellm/proxy/management_endpoints/ui_sso.py b/litellm/proxy/management_endpoints/ui_sso.py index 9eec551fb7..d8f426e1ae 100644 --- a/litellm/proxy/management_endpoints/ui_sso.py +++ b/litellm/proxy/management_endpoints/ui_sso.py @@ -58,7 +58,7 @@ from litellm.proxy.management_endpoints.sso_helper_utils import ( has_admin_ui_access, ) from litellm.proxy.management_endpoints.team_endpoints import new_team, team_member_add -from litellm.proxy.management_endpoints.types import CustomOpenID +from litellm.proxy.management_endpoints.types import CustomOpenID, get_litellm_user_role from litellm.proxy.utils import ( PrismaClient, ProxyLogging, @@ -277,6 +277,7 @@ def generic_response_convertor( last_name=response.get(generic_user_last_name_attribute_name), provider=response.get(generic_provider_attribute_name), team_ids=all_teams, + user_role=None, ) @@ -1145,7 +1146,7 @@ class SSOAuthenticationHandler: ) -> str: """ Get the redirect URL for SSO - + Note: existing_key is not added to the URL to avoid changing the callback URL. It should be passed via the state parameter instead. """ @@ -1348,7 +1349,7 @@ class SSOAuthenticationHandler: Checks the request 'source' if a cli state token was passed in This is used to authenticate through the CLI login flow. - + The state parameter format is: {PREFIX}:{key}:{existing_key} - If existing_key is provided, it's included in the state - The state parameter is used to pass data through the OAuth flow without changing the callback URL @@ -1673,22 +1674,49 @@ class MicrosoftSSOHandler: access_token=microsoft_sso.access_token ) + # Extract app roles from the id_token JWT + app_roles = MicrosoftSSOHandler.get_app_roles_from_id_token( + id_token=microsoft_sso.id_token + ) + verbose_proxy_logger.debug(f"Extracted app roles from id_token: {app_roles}") + + # Combine groups and app roles + user_role: Optional[LitellmUserRoles] = None + if app_roles: + # Check if any app role is a valid LitellmUserRoles + for role_str in app_roles: + role = get_litellm_user_role(role_str) + if role is not None: + user_role = role + verbose_proxy_logger.debug( + f"Found valid LitellmUserRoles '{role.value}' in app_roles" + ) + break + + verbose_proxy_logger.debug( + f"Combined team_ids (groups + app roles): {user_team_ids}" + ) + # if user is trying to get the raw sso response for debugging, return the raw sso response if return_raw_sso_response: original_msft_result[MicrosoftSSOHandler.GRAPH_API_RESPONSE_KEY] = ( user_team_ids ) + original_msft_result["app_roles"] = app_roles return original_msft_result or {} result = MicrosoftSSOHandler.openid_from_response( response=original_msft_result, team_ids=user_team_ids, + user_role=user_role, ) return result @staticmethod def openid_from_response( - response: Optional[dict], team_ids: List[str] + response: Optional[dict], + team_ids: List[str], + user_role: Optional[LitellmUserRoles], ) -> CustomOpenID: response = response or {} verbose_proxy_logger.debug(f"Microsoft SSO Callback Response: {response}") @@ -1700,10 +1728,54 @@ class MicrosoftSSOHandler: first_name=response.get("givenName"), last_name=response.get("surname"), team_ids=team_ids, + user_role=user_role, ) verbose_proxy_logger.debug(f"Microsoft SSO OpenID Response: {openid_response}") return openid_response + @staticmethod + def get_app_roles_from_id_token(id_token: Optional[str]) -> List[str]: + """ + Extract app roles from the Microsoft Entra ID (Azure AD) id_token JWT. + + App roles are assigned in the Azure AD Enterprise Application and appear + in the 'roles' claim of the id_token. + + Args: + id_token (Optional[str]): The JWT id_token from Microsoft SSO + + Returns: + List[str]: List of app role names assigned to the user + """ + if not id_token: + verbose_proxy_logger.debug("No id_token provided for app role extraction") + return [] + + try: + import jwt + + # Decode the JWT without signature verification + # (signature is already verified by fastapi_sso) + decoded_token = jwt.decode(id_token, options={"verify_signature": False}) + + # Extract roles claim from the token + roles = decoded_token.get("roles", []) + + if roles and isinstance(roles, list): + verbose_proxy_logger.debug( + f"Found {len(roles)} app role(s) in id_token: {roles}" + ) + return roles + else: + verbose_proxy_logger.debug( + "No app roles found in id_token or roles claim is not a list" + ) + return [] + + except Exception as e: + verbose_proxy_logger.error(f"Error extracting app roles from id_token: {e}") + return [] + @staticmethod async def get_user_groups_from_graph_api( access_token: Optional[str] = None, From ff3f356416562dc27b5648876e69e22e02f7ced4 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 8 Oct 2025 18:46:28 -0700 Subject: [PATCH 2/4] docs(docs/): add app role support to docs --- docs/my-website/docs/proxy/admin_ui_sso.md | 17 +++++++ docs/my-website/docs/tutorials/msft_sso.md | 52 ++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/docs/my-website/docs/proxy/admin_ui_sso.md b/docs/my-website/docs/proxy/admin_ui_sso.md index 32bf97410c..439aae6f96 100644 --- a/docs/my-website/docs/proxy/admin_ui_sso.md +++ b/docs/my-website/docs/proxy/admin_ui_sso.md @@ -81,6 +81,23 @@ MICROSOFT_TENANT="5a39737 http://localhost:4000/sso/callback ``` +**Using App Roles for User Permissions** + +You can assign user roles directly from Entra ID using App Roles. LiteLLM will automatically read the app roles from the JWT token and assign the corresponding role to the user. + +Supported roles: +- `proxy_admin` - Admin over the platform +- `proxy_admin_viewer` - Can login, view all keys, view all spend (read-only) +- `org_admin` - Admin over a specific organization +- `internal_user` - Can login, view/create/delete their own keys, view their spend + +To set up app roles: +1. Navigate to your App Registration on https://portal.azure.com/ +2. Go to "App roles" and create a new app role +3. Use one of the supported role names above (e.g., `proxy_admin`) +4. Assign users to these roles in your Enterprise Application +5. When users sign in via SSO, LiteLLM will automatically assign them the corresponding role + diff --git a/docs/my-website/docs/tutorials/msft_sso.md b/docs/my-website/docs/tutorials/msft_sso.md index f7ad6440f2..d24a289669 100644 --- a/docs/my-website/docs/tutorials/msft_sso.md +++ b/docs/my-website/docs/tutorials/msft_sso.md @@ -140,6 +140,58 @@ litellm_settings: +## 4. Using Entra ID App Roles for User Permissions + +You can assign user roles directly from Entra ID using App Roles. LiteLLM will automatically read the app roles from the JWT token during SSO sign-in and assign the corresponding role to the user. + +### 4.1 Supported Roles + +LiteLLM supports the following app roles (case-insensitive): + +- `proxy_admin` - Admin over the entire LiteLLM platform +- `proxy_admin_viewer` - Read-only admin access (can view all keys and spend) +- `org_admin` - Admin over a specific organization (can create teams and users within their org) +- `internal_user` - Standard user (can create/view/delete their own keys and view their own spend) + +### 4.2 Create App Roles in Entra ID + +1. Navigate to your App Registration on https://portal.azure.com/ +2. Go to **App roles** > **Create app role** + + + +3. Configure the app role: + - **Display name**: Proxy Admin (or your preferred display name) + - **Value**: `proxy_admin` (use one of the supported role values above) + - **Description**: Administrator access to LiteLLM proxy + - **Allowed member types**: Users/Groups + + + +4. Click **Apply** to save the role + +### 4.3 Assign Users to App Roles + +1. Navigate to **Enterprise Applications** on https://portal.azure.com/ +2. Select your LiteLLM application +3. Go to **Users and groups** > **Add user/group** +4. Select the user and assign them to one of the app roles you created + + + +### 4.4 Test the Role Assignment + +1. Sign in to LiteLLM UI via SSO as a user with an assigned app role +2. LiteLLM will automatically extract the app role from the JWT token +3. The user will be assigned the corresponding LiteLLM role in the database +4. The user's permissions will reflect their assigned role + +**How it works:** +- When a user signs in via Microsoft SSO, LiteLLM extracts the `roles` claim from the JWT `id_token` +- If any of the roles match a valid LiteLLM role (case-insensitive), that role is assigned to the user +- If multiple roles are present, LiteLLM uses the first valid role it finds +- This role assignment persists in the LiteLLM database and determines the user's access level + ## Video Walkthrough This walks through setting up sso auto-add for **Microsoft Entra ID** From 8eafb7a8fa93023d7b520bda3d22f4a1ac761758 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 8 Oct 2025 18:50:42 -0700 Subject: [PATCH 3/4] docs: doc improvements --- docs/my-website/docs/proxy/admin_ui_sso.md | 4 ++-- .../out/{model_hub_table.html => model_hub_table/index.html} | 0 litellm/proxy/_experimental/out/onboarding.html | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) rename litellm/proxy/_experimental/out/{model_hub_table.html => model_hub_table/index.html} (100%) delete mode 100644 litellm/proxy/_experimental/out/onboarding.html diff --git a/docs/my-website/docs/proxy/admin_ui_sso.md b/docs/my-website/docs/proxy/admin_ui_sso.md index 439aae6f96..bd18dd9c69 100644 --- a/docs/my-website/docs/proxy/admin_ui_sso.md +++ b/docs/my-website/docs/proxy/admin_ui_sso.md @@ -88,8 +88,8 @@ You can assign user roles directly from Entra ID using App Roles. LiteLLM will a Supported roles: - `proxy_admin` - Admin over the platform - `proxy_admin_viewer` - Can login, view all keys, view all spend (read-only) -- `org_admin` - Admin over a specific organization -- `internal_user` - Can login, view/create/delete their own keys, view their spend +- `internal_user` - Normal user. Can login, view spend and depending on team-member permissions - view/create/delete their own keys. + To set up app roles: 1. Navigate to your App Registration on https://portal.azure.com/ diff --git a/litellm/proxy/_experimental/out/model_hub_table.html b/litellm/proxy/_experimental/out/model_hub_table/index.html similarity index 100% rename from litellm/proxy/_experimental/out/model_hub_table.html rename to litellm/proxy/_experimental/out/model_hub_table/index.html diff --git a/litellm/proxy/_experimental/out/onboarding.html b/litellm/proxy/_experimental/out/onboarding.html deleted file mode 100644 index 04c6c88676..0000000000 --- a/litellm/proxy/_experimental/out/onboarding.html +++ /dev/null @@ -1 +0,0 @@ -LiteLLM Dashboard \ No newline at end of file From 697f99c1d5be1ffc6fc9d3dea1fb150f3860f0eb Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 8 Oct 2025 18:59:00 -0700 Subject: [PATCH 4/4] docs: doc improvements --- docs/my-website/docs/tutorials/msft_sso.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/my-website/docs/tutorials/msft_sso.md b/docs/my-website/docs/tutorials/msft_sso.md index d24a289669..2936f27297 100644 --- a/docs/my-website/docs/tutorials/msft_sso.md +++ b/docs/my-website/docs/tutorials/msft_sso.md @@ -158,15 +158,12 @@ LiteLLM supports the following app roles (case-insensitive): 1. Navigate to your App Registration on https://portal.azure.com/ 2. Go to **App roles** > **Create app role** - - 3. Configure the app role: - **Display name**: Proxy Admin (or your preferred display name) - **Value**: `proxy_admin` (use one of the supported role values above) - **Description**: Administrator access to LiteLLM proxy - **Allowed member types**: Users/Groups - 4. Click **Apply** to save the role @@ -177,7 +174,6 @@ LiteLLM supports the following app roles (case-insensitive): 3. Go to **Users and groups** > **Add user/group** 4. Select the user and assign them to one of the app roles you created - ### 4.4 Test the Role Assignment