diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/sso/useSSOSettings.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/sso/useSSOSettings.ts
index f03f3977115..0431a8d39f7 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/sso/useSSOSettings.ts
+++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/sso/useSSOSettings.ts
@@ -28,6 +28,7 @@ export interface SSOSettingsValues {
user_email: string | null;
ui_access_mode: string | null;
role_mappings: RoleMappings;
+ team_mappings: TeamMappings;
}
export interface RoleMappings {
@@ -39,6 +40,10 @@ export interface RoleMappings {
};
}
+export interface TeamMappings {
+ team_ids_jwt_field: string;
+}
+
export interface SSOSettingsResponse {
values: SSOSettingsValues;
field_schema: SSOFieldSchema;
diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/BaseSSOSettingsForm.test.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/BaseSSOSettingsForm.test.tsx
index a885bffa710..c68e2716f5b 100644
--- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/BaseSSOSettingsForm.test.tsx
+++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/BaseSSOSettingsForm.test.tsx
@@ -151,6 +151,117 @@ describe("BaseSSOSettingsForm", () => {
expect(screen.getByText("Default Role")).toBeInTheDocument();
});
});
+
+ it("should show team mappings checkbox for okta provider", async () => {
+ const TestWrapper = () => {
+ const [form] = Form.useForm();
+ const handleSubmit = vi.fn();
+
+ return ;
+ };
+
+ renderWithProviders();
+
+ const providerSelect = screen.getByLabelText("SSO Provider");
+ await act(async () => {
+ fireEvent.mouseDown(providerSelect);
+ });
+
+ await waitFor(() => {
+ const oktaOption = screen.getByText(/okta/i);
+ fireEvent.click(oktaOption);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("Use Team Mappings")).toBeInTheDocument();
+ });
+ });
+
+ it("should show team mappings checkbox for generic provider", async () => {
+ const TestWrapper = () => {
+ const [form] = Form.useForm();
+ const handleSubmit = vi.fn();
+
+ return ;
+ };
+
+ renderWithProviders();
+
+ const providerSelect = screen.getByLabelText("SSO Provider");
+ await act(async () => {
+ fireEvent.mouseDown(providerSelect);
+ });
+
+ await waitFor(() => {
+ const genericOption = screen.getByText(/generic sso/i);
+ fireEvent.click(genericOption);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("Use Team Mappings")).toBeInTheDocument();
+ });
+ });
+
+ it("should show team IDs JWT field when use_team_mappings is checked for okta provider", async () => {
+ const TestWrapper = () => {
+ const [form] = Form.useForm();
+ const handleSubmit = vi.fn();
+
+ return ;
+ };
+
+ renderWithProviders();
+
+ const providerSelect = screen.getByLabelText("SSO Provider");
+ await act(async () => {
+ fireEvent.mouseDown(providerSelect);
+ });
+
+ await waitFor(() => {
+ const oktaOption = screen.getByText(/okta/i);
+ fireEvent.click(oktaOption);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("Use Team Mappings")).toBeInTheDocument();
+ });
+
+ const checkbox = screen.getByLabelText("Use Team Mappings");
+ await act(async () => {
+ fireEvent.click(checkbox);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("Team IDs JWT Field")).toBeInTheDocument();
+ });
+ });
+
+ it("should not show team mappings checkbox for google provider", async () => {
+ const TestWrapper = () => {
+ const [form] = Form.useForm();
+ const handleSubmit = vi.fn();
+
+ return ;
+ };
+
+ renderWithProviders();
+
+ const providerSelect = screen.getByLabelText("SSO Provider");
+ await act(async () => {
+ fireEvent.mouseDown(providerSelect);
+ });
+
+ await waitFor(() => {
+ const googleOption = screen.getByText(/google sso/i);
+ fireEvent.click(googleOption);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("Google Client ID")).toBeInTheDocument();
+ });
+
+ expect(screen.queryByText("Use Team Mappings")).not.toBeInTheDocument();
+ });
});
describe("renderProviderFields", () => {
diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/BaseSSOSettingsForm.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/BaseSSOSettingsForm.tsx
index 6431b2dd3ac..d16b04466e0 100644
--- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/BaseSSOSettingsForm.tsx
+++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/BaseSSOSettingsForm.tsx
@@ -251,6 +251,43 @@ const BaseSSOSettingsForm: React.FC = ({ form, onFormS
) : null;
}}
+
+ prevValues.sso_provider !== currentValues.sso_provider}
+ >
+ {({ getFieldValue }) => {
+ const provider = getFieldValue("sso_provider");
+ return provider === "okta" || provider === "generic" ? (
+
+
+
+ ) : null;
+ }}
+
+
+
+ prevValues.use_team_mappings !== currentValues.use_team_mappings ||
+ prevValues.sso_provider !== currentValues.sso_provider
+ }
+ >
+ {({ getFieldValue }) => {
+ const useTeamMappings = getFieldValue("use_team_mappings");
+ const provider = getFieldValue("sso_provider");
+ const supportsTeamMappings = provider === "okta" || provider === "generic";
+ return useTeamMappings && supportsTeamMappings ? (
+
+
+
+ ) : null;
+ }}
+
);
diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/DeleteSSOSettingsModal.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/DeleteSSOSettingsModal.tsx
index 44cbf0020eb..2656c861aa8 100644
--- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/DeleteSSOSettingsModal.tsx
+++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/DeleteSSOSettingsModal.tsx
@@ -33,6 +33,7 @@ const DeleteSSOSettingsModal: React.FC = ({ isVisib
user_email: null,
sso_provider: null,
role_mappings: null,
+ team_mappings: null,
};
await editSSOSettings(clearSettings, {
diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/EditSSOSettingsModal.test.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/EditSSOSettingsModal.test.tsx
index 559d837b409..d2d54033395 100644
--- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/EditSSOSettingsModal.test.tsx
+++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/EditSSOSettingsModal.test.tsx
@@ -105,6 +105,14 @@ const createRoleMappingsSSOData = (overrides: Record = {}) =>
...overrides,
});
+const createTeamMappingsSSOData = (overrides: Record = {}) =>
+ createGenericSSOData({
+ team_mappings: {
+ team_ids_jwt_field: overrides.team_ids_jwt_field || "teams",
+ },
+ ...overrides,
+ });
+
// Mock utilities
const createMockHooks = (): {
useSSOSettings: SSOSettingsHookReturn;
@@ -577,6 +585,104 @@ describe("EditSSOSettingsModal", () => {
});
});
});
+ });
+
+ describe("Team Mappings", () => {
+ it("processes team mappings when team_mappings exists", async () => {
+ const ssoData = createTeamMappingsSSOData();
+
+ setupMocks({
+ useSSOSettings: { data: ssoData, isLoading: false, error: null },
+ });
+
+ renderComponent();
+
+ await waitFor(() => {
+ expect(mockForm.setFieldsValue).toHaveBeenCalledWith({
+ sso_provider: SSO_PROVIDERS.GENERIC,
+ ...ssoData.values,
+ use_team_mappings: true,
+ team_ids_jwt_field: "teams",
+ });
+ });
+ });
+
+ it("handles team mappings with custom JWT field name", async () => {
+ const ssoData = createTeamMappingsSSOData({
+ team_ids_jwt_field: "custom_teams_field",
+ });
+
+ setupMocks({
+ useSSOSettings: { data: ssoData, isLoading: false, error: null },
+ });
+
+ renderComponent();
+
+ await waitFor(() => {
+ expect(mockForm.setFieldsValue).toHaveBeenCalledWith({
+ sso_provider: SSO_PROVIDERS.GENERIC,
+ ...ssoData.values,
+ use_team_mappings: true,
+ team_ids_jwt_field: "custom_teams_field",
+ });
+ });
+ });
+
+ it("handles team mappings and role mappings together", async () => {
+ const ssoData = createGenericSSOData({
+ role_mappings: {
+ group_claim: "groups",
+ default_role: "internal_user",
+ roles: {
+ proxy_admin: ["admin-group"],
+ proxy_admin_viewer: [],
+ internal_user: [],
+ internal_user_viewer: [],
+ },
+ },
+ team_mappings: {
+ team_ids_jwt_field: "teams",
+ },
+ });
+
+ setupMocks({
+ useSSOSettings: { data: ssoData, isLoading: false, error: null },
+ });
+
+ renderComponent();
+
+ await waitFor(() => {
+ expect(mockForm.setFieldsValue).toHaveBeenCalledWith({
+ sso_provider: SSO_PROVIDERS.GENERIC,
+ ...ssoData.values,
+ use_role_mappings: true,
+ group_claim: "groups",
+ default_role: "internal_user",
+ proxy_admin_teams: "admin-group",
+ admin_viewer_teams: "",
+ internal_user_teams: "",
+ internal_viewer_teams: "",
+ use_team_mappings: true,
+ team_ids_jwt_field: "teams",
+ });
+ });
+ });
+
+ it("does not set team mapping fields when team_mappings is not present", async () => {
+ const ssoData = createGenericSSOData();
+
+ setupMocks({
+ useSSOSettings: { data: ssoData, isLoading: false, error: null },
+ });
+
+ renderComponent();
+
+ await waitFor(() => {
+ const callArgs = mockForm.setFieldsValue.mock.calls[0][0];
+ expect(callArgs.use_team_mappings).toBeUndefined();
+ expect(callArgs.team_ids_jwt_field).toBeUndefined();
+ });
+ });
it("handles provider detection with partial SSO data", async () => {
const ssoData = createSSOData({
diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/EditSSOSettingsModal.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/EditSSOSettingsModal.tsx
index a731af68ff1..bbae8f1451a 100644
--- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/EditSSOSettingsModal.tsx
+++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/EditSSOSettingsModal.tsx
@@ -68,11 +68,22 @@ const EditSSOSettingsModal: React.FC = ({ isVisible,
};
}
+ // Extract team mappings if they exist
+ let teamMappingFields = {};
+ if (ssoData.values.team_mappings) {
+ const teamMappings = ssoData.values.team_mappings;
+ teamMappingFields = {
+ use_team_mappings: true,
+ team_ids_jwt_field: teamMappings.team_ids_jwt_field,
+ };
+ }
+
// Set form values with existing data (excluding UI access control fields)
const formValues = {
sso_provider: selectedProvider,
...ssoData.values,
...roleMappingFields,
+ ...teamMappingFields,
};
console.log("Setting form values:", formValues); // Debug log
diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/SSOSettings.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/SSOSettings.tsx
index adc1251cde2..fdeda0ece3e 100644
--- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/SSOSettings.tsx
+++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/SSOSettings.tsx
@@ -1,7 +1,7 @@
"use client";
import { useSSOSettings, type SSOSettingsValues } from "@/app/(dashboard)/hooks/sso/useSSOSettings";
-import { Button, Card, Descriptions, Space, Typography } from "antd";
+import { Button, Card, Descriptions, Space, Tag, Typography } from "antd";
import { Edit, Shield, Trash2 } from "lucide-react";
import { useState } from "react";
import { ssoProviderDisplayNames, ssoProviderLogoMap } from "./constants";
@@ -28,6 +28,7 @@ export default function SSOSettings() {
const selectedProvider = ssoSettings?.values ? detectSSOProvider(ssoSettings.values) : null;
const isRoleMappingsEnabled = Boolean(ssoSettings?.values.role_mappings);
+ const isTeamMappingsEnabled = Boolean(ssoSettings?.values.team_mappings);
const renderEndpointValue = (value?: string | null) => (
@@ -38,6 +39,15 @@ export default function SSOSettings() {
const renderSimpleValue = (value?: string | null) =>
value ? value : Not configured;
+ const renderTeamMappingsField = (values: SSOSettingsValues) => {
+ if (!values.team_mappings?.team_ids_jwt_field) {
+ return Not configured;
+ }
+ return (
+ {values.team_mappings.team_ids_jwt_field}
+ );
+ };
+
const descriptionsConfig = {
column: {
xxl: 1,
@@ -103,6 +113,10 @@ export default function SSOSettings() {
render: (values: SSOSettingsValues) => renderEndpointValue(values.generic_userinfo_endpoint),
},
{ label: "Proxy Base URL", render: (values: SSOSettingsValues) => renderSimpleValue(values.proxy_base_url) },
+ isTeamMappingsEnabled ? {
+ label: "Team IDs JWT Field",
+ render: (values: SSOSettingsValues) => renderTeamMappingsField(values),
+ } : null,
],
},
generic: {
@@ -129,6 +143,10 @@ export default function SSOSettings() {
render: (values: SSOSettingsValues) => renderEndpointValue(values.generic_userinfo_endpoint),
},
{ label: "Proxy Base URL", render: (values: SSOSettingsValues) => renderSimpleValue(values.proxy_base_url) },
+ isTeamMappingsEnabled ? {
+ label: "Team IDs JWT Field",
+ render: (values: SSOSettingsValues) => renderTeamMappingsField(values),
+ } : null,
],
},
};
@@ -155,7 +173,7 @@ export default function SSOSettings() {
{config.providerText}
- {config.fields.map((field, index) => (
+ {config.fields.map((field, index) => field && (
{field.render(values)}
diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/utils.test.ts b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/utils.test.ts
index 718302d35fe..722d52d64f9 100644
--- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/utils.test.ts
+++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/utils.test.ts
@@ -12,6 +12,8 @@ describe("processSSOSettingsPayload", () => {
default_role: "proxy_admin",
group_claim: "groups",
use_role_mappings: false,
+ use_team_mappings: false,
+ team_ids_jwt_field: "teams",
other_field: "value",
another_field: 123,
};
@@ -23,6 +25,7 @@ describe("processSSOSettingsPayload", () => {
another_field: 123,
});
expect(result.role_mappings).toBeUndefined();
+ expect(result.team_mappings).toBeUndefined();
});
it("should return all fields except role mapping fields when use_role_mappings is not present", () => {
@@ -33,6 +36,8 @@ describe("processSSOSettingsPayload", () => {
internal_viewer_teams: "viewer1",
default_role: "proxy_admin",
group_claim: "groups",
+ use_team_mappings: false,
+ team_ids_jwt_field: "teams",
other_field: "value",
};
@@ -42,6 +47,7 @@ describe("processSSOSettingsPayload", () => {
other_field: "value",
});
expect(result.role_mappings).toBeUndefined();
+ expect(result.team_mappings).toBeUndefined();
});
});
@@ -253,6 +259,143 @@ describe("processSSOSettingsPayload", () => {
});
});
+ describe("without team mappings", () => {
+ it("should return all fields except team mapping fields when use_team_mappings is false", () => {
+ const formValues = {
+ use_team_mappings: false,
+ team_ids_jwt_field: "teams",
+ sso_provider: "okta",
+ other_field: "value",
+ };
+
+ const result = processSSOSettingsPayload(formValues);
+
+ expect(result).toEqual({
+ sso_provider: "okta",
+ other_field: "value",
+ });
+ expect(result.team_mappings).toBeUndefined();
+ });
+
+ it("should return all fields except team mapping fields when use_team_mappings is not present", () => {
+ const formValues = {
+ team_ids_jwt_field: "teams",
+ sso_provider: "generic",
+ other_field: "value",
+ };
+
+ const result = processSSOSettingsPayload(formValues);
+
+ expect(result).toEqual({
+ sso_provider: "generic",
+ other_field: "value",
+ });
+ expect(result.team_mappings).toBeUndefined();
+ });
+
+ it("should not include team mappings for unsupported providers even when use_team_mappings is true", () => {
+ const formValues = {
+ use_team_mappings: true,
+ team_ids_jwt_field: "teams",
+ sso_provider: "google",
+ other_field: "value",
+ };
+
+ const result = processSSOSettingsPayload(formValues);
+
+ expect(result).toEqual({
+ sso_provider: "google",
+ other_field: "value",
+ });
+ expect(result.team_mappings).toBeUndefined();
+ });
+
+ it("should not include team mappings for microsoft provider even when use_team_mappings is true", () => {
+ const formValues = {
+ use_team_mappings: true,
+ team_ids_jwt_field: "teams",
+ sso_provider: "microsoft",
+ other_field: "value",
+ };
+
+ const result = processSSOSettingsPayload(formValues);
+
+ expect(result).toEqual({
+ sso_provider: "microsoft",
+ other_field: "value",
+ });
+ expect(result.team_mappings).toBeUndefined();
+ });
+ });
+
+ describe("with team mappings enabled", () => {
+ it("should create team mappings for okta provider when use_team_mappings is true", () => {
+ const formValues = {
+ use_team_mappings: true,
+ team_ids_jwt_field: "teams",
+ sso_provider: "okta",
+ other_field: "value",
+ };
+
+ const result = processSSOSettingsPayload(formValues);
+
+ expect(result.other_field).toBe("value");
+ expect(result.team_mappings).toEqual({
+ team_ids_jwt_field: "teams",
+ });
+ });
+
+ it("should create team mappings for generic provider when use_team_mappings is true", () => {
+ const formValues = {
+ use_team_mappings: true,
+ team_ids_jwt_field: "custom_teams",
+ sso_provider: "generic",
+ other_field: "value",
+ };
+
+ const result = processSSOSettingsPayload(formValues);
+
+ expect(result.other_field).toBe("value");
+ expect(result.team_mappings).toEqual({
+ team_ids_jwt_field: "custom_teams",
+ });
+ });
+
+ it("should exclude team mapping fields from payload when team mappings are included", () => {
+ const formValues = {
+ use_team_mappings: true,
+ team_ids_jwt_field: "teams",
+ sso_provider: "okta",
+ other_field: "value",
+ };
+
+ const result = processSSOSettingsPayload(formValues);
+
+ expect(result.use_team_mappings).toBeUndefined();
+ expect(result.team_ids_jwt_field).toBeUndefined();
+ });
+
+ it("should handle team mappings and role mappings together", () => {
+ const formValues = {
+ use_team_mappings: true,
+ team_ids_jwt_field: "teams",
+ use_role_mappings: true,
+ group_claim: "groups",
+ default_role: "internal_user",
+ sso_provider: "okta",
+ other_field: "value",
+ };
+
+ const result = processSSOSettingsPayload(formValues);
+
+ expect(result.team_mappings).toEqual({
+ team_ids_jwt_field: "teams",
+ });
+ expect(result.role_mappings).toBeDefined();
+ expect(result.role_mappings.group_claim).toBe("groups");
+ });
+ });
+
describe("edge cases", () => {
it("should handle empty form values", () => {
const result = processSSOSettingsPayload({});
@@ -263,6 +406,7 @@ describe("processSSOSettingsPayload", () => {
it("should preserve other fields in the payload", () => {
const formValues = {
use_role_mappings: false,
+ use_team_mappings: false,
sso_provider: "google",
client_id: "123",
client_secret: "secret",
diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/utils.ts b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/utils.ts
index c199048df3e..948ed4d2bfe 100644
--- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/utils.ts
+++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/utils.ts
@@ -13,6 +13,8 @@ export const processSSOSettingsPayload = (formValues: Record): Reco
default_role,
group_claim,
use_role_mappings,
+ use_team_mappings,
+ team_ids_jwt_field,
...rest
} = formValues;
@@ -21,7 +23,9 @@ export const processSSOSettingsPayload = (formValues: Record): Reco
};
// Add role mappings only if use_role_mappings is checked AND provider supports role mappings
- if (use_role_mappings) {
+ const provider = rest.sso_provider;
+ const supportsRoleMappings = provider === "okta" || provider === "generic";
+ if (use_role_mappings && supportsRoleMappings) {
// Helper function to split comma-separated string into array
const splitTeams = (teams: string | undefined): string[] => {
if (!teams || teams.trim() === "") return [];
@@ -52,6 +56,14 @@ export const processSSOSettingsPayload = (formValues: Record): Reco
};
}
+ // Add team mappings only if use_team_mappings is checked AND provider supports team mappings
+ const supportsTeamMappings = provider === "okta" || provider === "generic";
+ if (use_team_mappings && supportsTeamMappings) {
+ payload.team_mappings = {
+ team_ids_jwt_field: team_ids_jwt_field,
+ };
+ }
+
return payload;
};