fix(sso): honor singular team_id_jwt_field in SSO callback - #27352
Conversation
The SSO callback flow (`generic_response_convertor` → `add_missing_team_member`) was reading only the plural `team_ids_jwt_field` claim. IdPs that populate the singular `team_id_jwt_field` instead (Okta/Auth0 group → primary team) had their users created via SSO with `teams: []` — never assigned to the team named in the JWT, regardless of how the user_metadata changed across logins. Adds `JWTHandler.get_all_jwt_team_ids()` returning the deduplicated union of both claim sources, and threads it through both branches that build `all_teams` in `generic_response_convertor`. JWT bearer auth path is intentionally unchanged — pending customer feedback on whether membership reconciliation there should be moved or stay additive.
Greptile SummaryThis PR fixes the SSO callback flow so that users whose IdP sets only the singular
Confidence Score: 5/5Change is safe to merge — it is a targeted, additive fix that adds a new method and updates two call-sites; no existing auth logic is modified. The new No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/proxy/auth/handle_jwt.py | Adds get_all_jwt_team_ids that unions team_ids_jwt_field (plural) and team_id_jwt_field (singular) without touching team_id_default; logic is consistent with the existing get_team_id behavior for list-valued singular fields |
| litellm/proxy/management_endpoints/ui_sso.py | Two call-sites in generic_response_convertor switched from get_team_ids_from_jwt to get_all_jwt_team_ids; minimal, targeted change with no other logic altered |
| tests/test_litellm/proxy/auth/test_handle_jwt.py | Adds two new sync unit tests covering all claim-shape combinations and explicitly guarding against team_id_default leakage; no existing tests modified |
Reviews (2): Last reviewed commit: "fix(sso): don't propagate team_id_defaul..." | Re-trigger Greptile
Greptile flagged that the helper was delegating to JWTHandler.get_team_id, which falls back to litellm_jwtauth.team_id_default when the JWT claim is missing or the field isn't configured. The plural-only get_team_ids_from_jwt that the SSO path used historically has no such fallback, so threading get_all_jwt_team_ids through generic_response_convertor would silently start adding SSO users to the default team for any tenant that had team_id_default configured — an undocumented backwards-incompatible change. Read the singular team_id_jwt_field claim directly via get_nested_value instead, so the helper returns only what's actually in the token. Added a test that pins this behavior across both configurations of the default (team_id_jwt_field set + missing claim, and team_id_jwt_field unset). Also dropped @pytest.mark.asyncio from the sync test (P2 nit).
|
@greptileai re review |
|
🤖 litellm-agent: Squash-merged into staging branch Triage Summary Merge Confidence: 5/5 ✅ READY Greptile 5/5, no blocking pattern findings, CircleCI passed. 1 check failing but unrelated to this diff: ci/circleci: local_testing_part1. 1 unrelated CI failure unique to this PR (ci/circleci: local_testing_part1) — not related to this diff but worth a glance. |
Summary
The SSO callback flow (
generic_response_convertor→add_missing_team_member) was reading only the pluralteam_ids_jwt_fieldclaim. IdPs that populate the singularteam_id_jwt_fieldinstead (Okta/Auth0 mapping a group → primary team) had their users created via SSO withteams: []— never assigned to the team named in the JWT, regardless of how the user'suser_metadata.team_idchanged across logins.This adds
JWTHandler.get_all_jwt_team_ids()returning the deduplicated union of both claim sources, and threads it through the two branches that buildall_teamsingeneric_response_convertor. The result: SSO logins now read the singular claim the same way the JWT-bearer path'sfind_and_validate_specific_team_idalways has.screenshots
Test plan
uv run pytest tests/test_litellm/proxy/auth/test_handle_jwt.py tests/test_litellm/proxy/management_endpoints/test_ui_sso.py— 240 pass.test_get_all_jwt_team_ids_unions_singular_and_pluralcovering all four input combinations (singular only, plural only, both no-overlap, both with overlap, neither).user_metadata.team_id(BEFORE:teams: []; AFTER:teams: ["team-low"]after first login, dual membership after re-login with changed metadata).uv run blackon the three changed files).