Skip to content

fix(proxy): single-team DB fallback when JWT has no team_id - #26418

Merged
ishaan-berri merged 3 commits into
litellm_internal_stagingfrom
litellm_jwt_single_team_fallback
Apr 25, 2026
Merged

fix(proxy): single-team DB fallback when JWT has no team_id#26418
ishaan-berri merged 3 commits into
litellm_internal_stagingfrom
litellm_jwt_single_team_fallback

Conversation

@milan-berri

@milan-berri milan-berri commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

JWT calls with no team in the token left team_id unset even when the user existed in LiteLLM on a single team, so spend/metadata did not attach to that team. This change infers that team only when unambiguous (exactly one team on the user row).

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have Added testing in the tests/test_litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

CI (LiteLLM team)

CI status guideline:

  • 50-55 passing tests: main is stable with minor issues.
  • 45-49 passing tests: acceptable but needs attention
  • <= 40 passing tests: unstable; be careful with your merges and assess the risk.
  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Screenshots / Proof of Fix

Manual verification: with enable_jwt_auth, JWT with only sub (no team claims), and a LiteLLM user in exactly one team, spend/metadata should show that team_id / user_api_key_team_id. With two teams on the user, no team is inferred (ambiguous). Unit tests in test_handle_jwt.py cover these cases with mocks (no real LLM calls).
image
image
image
image

Type

🆕 New Feature
✅ Test

Changes

  • auth_builder (JWT): If team_id is still unset after the normal JWT/team resolution flow, and user_object.teams has exactly length 1, attempt to load that team via get_team_object and (when user_id is set) get_team_membership. Both calls are wrapped in a single try/except Exception — any error (including HTTPException(404) when the team row is missing, which is what get_team_object raises in production) is debug-logged and silently skipped; the request continues without a team_id. No new failure path is introduced. If the user has 0 or 2+ teams, the fallback is not attempted.
  • Tests: Parametrized test_auth_builder_single_team_db_fallback_when_jwt_has_no_team — covers: single team resolves correctly; two teams / zero teams / get_team_object raising HTTPException(404) / get_team_object raising HTTPException(500) all leave team_id unset without failing auth. Added test_auth_builder_single_team_fallback_membership_error_skips_no_raiseget_team_object succeeds but get_team_membership raises, result has no team context and no exception.

@greptile-apps

greptile-apps Bot commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a single-team DB fallback to JWTAuthManager.auth_builder: when a JWT token carries no team claim and the authenticated user belongs to exactly one team in LiteLLM's DB, the new _resolve_single_team_fallback helper fetches that team (and its membership) so spend/metadata can be attributed correctly. The previous P1 issues flagged in earlier review rounds are now resolved — get_team_object failures are caught by a broad try/except Exception, and the orphaned-team test case correctly uses side_effect = HTTPException(status_code=404) to match real behaviour.

Confidence Score: 5/5

Safe to merge; the previously flagged P1 issues are addressed and only a dead-code style nit remains.

Both P1 bugs from prior review rounds are fixed: the try/except Exception now properly swallows HTTPException(404) from get_team_object, and the test mock for the orphaned-team case now raises instead of returning None. The sole remaining finding is a P2 dead-code guard (if team_row is None) that is unreachable but harmless.

litellm/proxy/auth/handle_jwt.py — the unreachable if team_row is None guard in _resolve_single_team_fallback (line 1492)

Important Files Changed

Filename Overview
litellm/proxy/auth/handle_jwt.py Adds _resolve_single_team_fallback static method and wires it into auth_builder after team resolution; the try/except Exception correctly swallows HTTPException(404/500) from get_team_object, but the if team_row is None guard inside is dead code since that function never returns None.
tests/test_litellm/proxy/auth/test_handle_jwt.py Adds parametrized tests for the single-team DB fallback; the one_team_id_but_row_missing_in_db case now correctly uses side_effect = HTTPException(status_code=404, ...) matching the real get_team_object behaviour, and a separate test covers the membership-error path.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[JWT auth_builder called] --> B[Resolve team_id from token/header/routing]
    B --> C{team_id resolved?}
    C -- Yes --> G[get_objects: user, org, end_user, membership]
    C -- No --> D[_resolve_single_team_fallback]
    D --> E{user_object.teams length == 1?}
    E -- No / 0 or 2+ teams --> F[return None, None, None]
    E -- Yes --> H[get_team_object team_id]
    H --> I{raises Exception?}
    I -- Yes --> J[debug log, return None, None, None]
    I -- No --> K{user_id set?}
    K -- No --> L[return team_id, team_row, None]
    K -- Yes --> M[get_team_membership]
    M --> N{raises Exception?}
    N -- Yes --> J
    N -- No --> O[return team_id, team_row, membership]
    F --> G
    L --> G
    O --> G
    J --> G
    G --> P[map_user_to_teams]
    P --> Q[validate_object_id]
    Q --> R[Return JWTAuthBuilderResult]
Loading

Reviews (5): Last reviewed commit: "refactor(jwt): extract single-team fallb..." | Re-trigger Greptile

Comment thread litellm/proxy/auth/handle_jwt.py Outdated
Comment thread tests/test_litellm/proxy/auth/test_handle_jwt.py
@veria-ai

veria-ai Bot commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Low: No security issues found

This PR adds a single-team DB fallback in the JWT auth flow so that when a JWT token doesn't carry a team_id claim, but the authenticated user belongs to exactly one team in the database, that team is automatically associated. The fallback reads from the user's own trusted DB record (not attacker-controlled input), the team is validated via get_team_object, and all downstream authorization checks (budget, model access, RBAC) still apply. The removed scopes assignment was a duplicate — scopes is still correctly assigned and used for scope-based access checks and admin access checks.


Status: 0 open
Risk: 2/10

Posted by Veria AI · 2026-04-24T23:52:33.773Z

@milan-berri
milan-berri temporarily deployed to integration-postgres April 24, 2026 12:28 — with GitHub Actions Inactive
@milan-berri
milan-berri temporarily deployed to integration-postgres April 24, 2026 12:28 — with GitHub Actions Inactive
@milan-berri
milan-berri temporarily deployed to integration-postgres April 24, 2026 12:33 — with GitHub Actions Inactive
@milan-berri
milan-berri temporarily deployed to integration-postgres April 24, 2026 12:33 — with GitHub Actions Inactive
@milan-berri
milan-berri temporarily deployed to integration-postgres April 24, 2026 12:33 — with GitHub Actions Inactive
@milan-berri
milan-berri temporarily deployed to integration-postgres April 24, 2026 12:33 — with GitHub Actions Inactive
@milan-berri
milan-berri temporarily deployed to integration-postgres April 24, 2026 12:33 — with GitHub Actions Inactive
@codecov

codecov Bot commented Apr 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.88889% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/proxy/auth/handle_jwt.py 88.88% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

Comment thread litellm/proxy/auth/handle_jwt.py Outdated
user_api_key_cache=user_api_key_cache,
)

# If JWT did not resolve team_id, but the user belongs to exactly one team in

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@milan-berri no do not allow noqa: PLR0915 instead place this logic in it's own helper

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed!

@milan-berri
milan-berri temporarily deployed to integration-postgres April 24, 2026 21:18 — with GitHub Actions Inactive
@milan-berri
milan-berri temporarily deployed to integration-postgres April 24, 2026 21:18 — with GitHub Actions Inactive
@milan-berri
milan-berri temporarily deployed to integration-postgres April 24, 2026 21:18 — with GitHub Actions Inactive
@milan-berri
milan-berri temporarily deployed to integration-postgres April 24, 2026 21:18 — with GitHub Actions Inactive
@milan-berri
milan-berri temporarily deployed to integration-postgres April 24, 2026 21:18 — with GitHub Actions Inactive
@milan-berri
milan-berri temporarily deployed to integration-postgres April 24, 2026 21:34 — with GitHub Actions Inactive
@milan-berri
milan-berri temporarily deployed to integration-postgres April 24, 2026 21:34 — with GitHub Actions Inactive
@milan-berri
milan-berri temporarily deployed to integration-postgres April 24, 2026 21:34 — with GitHub Actions Inactive
@milan-berri
milan-berri temporarily deployed to integration-postgres April 24, 2026 21:34 — with GitHub Actions Inactive
@milan-berri
milan-berri temporarily deployed to integration-postgres April 24, 2026 21:34 — with GitHub Actions Inactive
…team

- When team_id is unset after JWT auth but the user row has exactly one
  team, set team_id, team_object, and team_membership from DB.
- Skip when zero or multiple teams (ambiguous).
- Add parametrized unit tests in test_handle_jwt.py.

Made-with: Cursor
…t_team_object

- Wrap get_team_object + get_team_membership in one try/except; log and skip on failure (stale/missing team id no longer fails auth).
- Parametrize tests: HTTP 404/500, membership error; use side_effect not return_value=None for missing team row.

Made-with: Cursor
@milan-berri
milan-berri force-pushed the litellm_jwt_single_team_fallback branch from 78a32fa to bfc4bad Compare April 24, 2026 23:50
@ishaan-berri
ishaan-berri merged commit 3c0d172 into litellm_internal_staging Apr 25, 2026
117 checks passed
@ishaan-berri
ishaan-berri deleted the litellm_jwt_single_team_fallback branch April 25, 2026 00:51
milan-berri added a commit that referenced this pull request May 26, 2026
Extends the single-team DB fallback introduced in #26418 to two more
cases where it previously could not run:

* `find_and_validate_specific_team_id`: when `team_id_jwt_field` is
  configured and a claim value is present in the token but the team
  does not exist in the LiteLLM DB (HTTPException 404 from
  `get_team_object`), return `(None, None)` instead of raising — the
  auth_builder fallback then attributes the request to the user's
  single DB team. Only HTTPException is caught; other errors (e.g.
  "No DB Connected") still propagate.

* `find_team_with_model_access`: when none of the `team_ids_jwt_field`
  groups resolve to a real LiteLLM team, return `(None, None)` instead
  of raising 403 so the same fallback path runs. If at least one group
  DID resolve to a team but none granted the requested model, the
  original 403 is preserved (legitimate access denial — not a claim
  mismatch). Tracked via the new `any_claim_team_resolved` flag.

The strict `is_required_team_id` raise and `enforce_team_based_model_access`
raise remain unchanged. Unit tests cover both new soft-fail paths and
guard each preserved path (strict required, enforce_team_based, the
preserved 403, and the non-HTTPException propagation).

Co-authored-by: Cursor <cursoragent@cursor.com>
milan-berri added a commit that referenced this pull request Jun 8, 2026
…28913)

* fix(jwt-auth): defer to single-team DB fallback on claim mismatch

Extends the single-team DB fallback introduced in #26418 to two more
cases where it previously could not run:

* `find_and_validate_specific_team_id`: when `team_id_jwt_field` is
  configured and a claim value is present in the token but the team
  does not exist in the LiteLLM DB (HTTPException 404 from
  `get_team_object`), return `(None, None)` instead of raising — the
  auth_builder fallback then attributes the request to the user's
  single DB team. Only HTTPException is caught; other errors (e.g.
  "No DB Connected") still propagate.

* `find_team_with_model_access`: when none of the `team_ids_jwt_field`
  groups resolve to a real LiteLLM team, return `(None, None)` instead
  of raising 403 so the same fallback path runs. If at least one group
  DID resolve to a team but none granted the requested model, the
  original 403 is preserved (legitimate access denial — not a claim
  mismatch). Tracked via the new `any_claim_team_resolved` flag.

The strict `is_required_team_id` raise and `enforce_team_based_model_access`
raise remain unchanged. Unit tests cover both new soft-fail paths and
guard each preserved path (strict required, enforce_team_based, the
preserved 403, and the non-HTTPException propagation).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(jwt-auth): narrow HTTPException catch to 404 (greptile review)

Address Greptile review comments on #28913:

* `find_and_validate_specific_team_id`: re-raise HTTPException when
  `status_code != 404`, pinning the catch to the "team doesn't exist
  in db" path documented for `get_team_object`. A future change that
  introduces a different status code (e.g. 403 for a blocked team)
  will now propagate instead of silently falling through to the
  single-team DB fallback.

* Add `test_find_and_validate_specific_team_id_non_404_http_exception_propagates`
  parametrised over 400 / 403 / 500 to lock in the contract.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(jwt-auth): gate claim-mismatch fallback behind opt-in flag

The unresolved-team-claim fallback added in the previous commit
weakened the strict claim-based authorization contract by default —
an authenticated user whose JWT carries a stale or invalid team
claim could still consume their single DB team's models/quota via
the fallback.

Gate both soft-fail paths in `find_and_validate_specific_team_id`
and `find_team_with_model_access` behind a new opt-in flag
`team_claim_fallback` on `LiteLLM_JWTAuth` (default False).

Default-off preserves the pre-existing strict behavior. Operators
who intentionally treat IdP team claims as advisory (e.g. machine
tokens whose group claims live in a separate namespace from
LiteLLM team_ids) opt in via config.

Adds two regression tests guarding the default-off behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
michaelxer pushed a commit to michaelxer/litellm that referenced this pull request Jun 17, 2026
…erriAI#28913)

* fix(jwt-auth): defer to single-team DB fallback on claim mismatch

Extends the single-team DB fallback introduced in BerriAI#26418 to two more
cases where it previously could not run:

* `find_and_validate_specific_team_id`: when `team_id_jwt_field` is
  configured and a claim value is present in the token but the team
  does not exist in the LiteLLM DB (HTTPException 404 from
  `get_team_object`), return `(None, None)` instead of raising — the
  auth_builder fallback then attributes the request to the user's
  single DB team. Only HTTPException is caught; other errors (e.g.
  "No DB Connected") still propagate.

* `find_team_with_model_access`: when none of the `team_ids_jwt_field`
  groups resolve to a real LiteLLM team, return `(None, None)` instead
  of raising 403 so the same fallback path runs. If at least one group
  DID resolve to a team but none granted the requested model, the
  original 403 is preserved (legitimate access denial — not a claim
  mismatch). Tracked via the new `any_claim_team_resolved` flag.

The strict `is_required_team_id` raise and `enforce_team_based_model_access`
raise remain unchanged. Unit tests cover both new soft-fail paths and
guard each preserved path (strict required, enforce_team_based, the
preserved 403, and the non-HTTPException propagation).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(jwt-auth): narrow HTTPException catch to 404 (greptile review)

Address Greptile review comments on BerriAI#28913:

* `find_and_validate_specific_team_id`: re-raise HTTPException when
  `status_code != 404`, pinning the catch to the "team doesn't exist
  in db" path documented for `get_team_object`. A future change that
  introduces a different status code (e.g. 403 for a blocked team)
  will now propagate instead of silently falling through to the
  single-team DB fallback.

* Add `test_find_and_validate_specific_team_id_non_404_http_exception_propagates`
  parametrised over 400 / 403 / 500 to lock in the contract.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(jwt-auth): gate claim-mismatch fallback behind opt-in flag

The unresolved-team-claim fallback added in the previous commit
weakened the strict claim-based authorization contract by default —
an authenticated user whose JWT carries a stale or invalid team
claim could still consume their single DB team's models/quota via
the fallback.

Gate both soft-fail paths in `find_and_validate_specific_team_id`
and `find_team_with_model_access` behind a new opt-in flag
`team_claim_fallback` on `LiteLLM_JWTAuth` (default False).

Default-off preserves the pre-existing strict behavior. Operators
who intentionally treat IdP team claims as advisory (e.g. machine
tokens whose group claims live in a separate namespace from
LiteLLM team_ids) opt in via config.

Adds two regression tests guarding the default-off behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
michaelxer pushed a commit to michaelxer/litellm that referenced this pull request Jun 17, 2026
…erriAI#28913)

* fix(jwt-auth): defer to single-team DB fallback on claim mismatch

Extends the single-team DB fallback introduced in BerriAI#26418 to two more
cases where it previously could not run:

* `find_and_validate_specific_team_id`: when `team_id_jwt_field` is
  configured and a claim value is present in the token but the team
  does not exist in the LiteLLM DB (HTTPException 404 from
  `get_team_object`), return `(None, None)` instead of raising — the
  auth_builder fallback then attributes the request to the user's
  single DB team. Only HTTPException is caught; other errors (e.g.
  "No DB Connected") still propagate.

* `find_team_with_model_access`: when none of the `team_ids_jwt_field`
  groups resolve to a real LiteLLM team, return `(None, None)` instead
  of raising 403 so the same fallback path runs. If at least one group
  DID resolve to a team but none granted the requested model, the
  original 403 is preserved (legitimate access denial — not a claim
  mismatch). Tracked via the new `any_claim_team_resolved` flag.

The strict `is_required_team_id` raise and `enforce_team_based_model_access`
raise remain unchanged. Unit tests cover both new soft-fail paths and
guard each preserved path (strict required, enforce_team_based, the
preserved 403, and the non-HTTPException propagation).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(jwt-auth): narrow HTTPException catch to 404 (greptile review)

Address Greptile review comments on BerriAI#28913:

* `find_and_validate_specific_team_id`: re-raise HTTPException when
  `status_code != 404`, pinning the catch to the "team doesn't exist
  in db" path documented for `get_team_object`. A future change that
  introduces a different status code (e.g. 403 for a blocked team)
  will now propagate instead of silently falling through to the
  single-team DB fallback.

* Add `test_find_and_validate_specific_team_id_non_404_http_exception_propagates`
  parametrised over 400 / 403 / 500 to lock in the contract.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(jwt-auth): gate claim-mismatch fallback behind opt-in flag

The unresolved-team-claim fallback added in the previous commit
weakened the strict claim-based authorization contract by default —
an authenticated user whose JWT carries a stale or invalid team
claim could still consume their single DB team's models/quota via
the fallback.

Gate both soft-fail paths in `find_and_validate_specific_team_id`
and `find_team_with_model_access` behind a new opt-in flag
`team_claim_fallback` on `LiteLLM_JWTAuth` (default False).

Default-off preserves the pre-existing strict behavior. Operators
who intentionally treat IdP team claims as advisory (e.g. machine
tokens whose group claims live in a separate namespace from
LiteLLM team_ids) opt in via config.

Adds two regression tests guarding the default-off behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
koladefaj pushed a commit to koladefaj/litellm that referenced this pull request Jun 17, 2026
…erriAI#28913)

* fix(jwt-auth): defer to single-team DB fallback on claim mismatch

Extends the single-team DB fallback introduced in BerriAI#26418 to two more
cases where it previously could not run:

* `find_and_validate_specific_team_id`: when `team_id_jwt_field` is
  configured and a claim value is present in the token but the team
  does not exist in the LiteLLM DB (HTTPException 404 from
  `get_team_object`), return `(None, None)` instead of raising — the
  auth_builder fallback then attributes the request to the user's
  single DB team. Only HTTPException is caught; other errors (e.g.
  "No DB Connected") still propagate.

* `find_team_with_model_access`: when none of the `team_ids_jwt_field`
  groups resolve to a real LiteLLM team, return `(None, None)` instead
  of raising 403 so the same fallback path runs. If at least one group
  DID resolve to a team but none granted the requested model, the
  original 403 is preserved (legitimate access denial — not a claim
  mismatch). Tracked via the new `any_claim_team_resolved` flag.

The strict `is_required_team_id` raise and `enforce_team_based_model_access`
raise remain unchanged. Unit tests cover both new soft-fail paths and
guard each preserved path (strict required, enforce_team_based, the
preserved 403, and the non-HTTPException propagation).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(jwt-auth): narrow HTTPException catch to 404 (greptile review)

Address Greptile review comments on BerriAI#28913:

* `find_and_validate_specific_team_id`: re-raise HTTPException when
  `status_code != 404`, pinning the catch to the "team doesn't exist
  in db" path documented for `get_team_object`. A future change that
  introduces a different status code (e.g. 403 for a blocked team)
  will now propagate instead of silently falling through to the
  single-team DB fallback.

* Add `test_find_and_validate_specific_team_id_non_404_http_exception_propagates`
  parametrised over 400 / 403 / 500 to lock in the contract.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(jwt-auth): gate claim-mismatch fallback behind opt-in flag

The unresolved-team-claim fallback added in the previous commit
weakened the strict claim-based authorization contract by default —
an authenticated user whose JWT carries a stale or invalid team
claim could still consume their single DB team's models/quota via
the fallback.

Gate both soft-fail paths in `find_and_validate_specific_team_id`
and `find_team_with_model_access` behind a new opt-in flag
`team_claim_fallback` on `LiteLLM_JWTAuth` (default False).

Default-off preserves the pre-existing strict behavior. Operators
who intentionally treat IdP team claims as advisory (e.g. machine
tokens whose group claims live in a separate namespace from
LiteLLM team_ids) opt in via config.

Adds two regression tests guarding the default-off behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
factnn pushed a commit to factnn/litellm that referenced this pull request Jun 18, 2026
…erriAI#28913)

* fix(jwt-auth): defer to single-team DB fallback on claim mismatch

Extends the single-team DB fallback introduced in BerriAI#26418 to two more
cases where it previously could not run:

* `find_and_validate_specific_team_id`: when `team_id_jwt_field` is
  configured and a claim value is present in the token but the team
  does not exist in the LiteLLM DB (HTTPException 404 from
  `get_team_object`), return `(None, None)` instead of raising — the
  auth_builder fallback then attributes the request to the user's
  single DB team. Only HTTPException is caught; other errors (e.g.
  "No DB Connected") still propagate.

* `find_team_with_model_access`: when none of the `team_ids_jwt_field`
  groups resolve to a real LiteLLM team, return `(None, None)` instead
  of raising 403 so the same fallback path runs. If at least one group
  DID resolve to a team but none granted the requested model, the
  original 403 is preserved (legitimate access denial — not a claim
  mismatch). Tracked via the new `any_claim_team_resolved` flag.

The strict `is_required_team_id` raise and `enforce_team_based_model_access`
raise remain unchanged. Unit tests cover both new soft-fail paths and
guard each preserved path (strict required, enforce_team_based, the
preserved 403, and the non-HTTPException propagation).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(jwt-auth): narrow HTTPException catch to 404 (greptile review)

Address Greptile review comments on BerriAI#28913:

* `find_and_validate_specific_team_id`: re-raise HTTPException when
  `status_code != 404`, pinning the catch to the "team doesn't exist
  in db" path documented for `get_team_object`. A future change that
  introduces a different status code (e.g. 403 for a blocked team)
  will now propagate instead of silently falling through to the
  single-team DB fallback.

* Add `test_find_and_validate_specific_team_id_non_404_http_exception_propagates`
  parametrised over 400 / 403 / 500 to lock in the contract.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(jwt-auth): gate claim-mismatch fallback behind opt-in flag

The unresolved-team-claim fallback added in the previous commit
weakened the strict claim-based authorization contract by default —
an authenticated user whose JWT carries a stale or invalid team
claim could still consume their single DB team's models/quota via
the fallback.

Gate both soft-fail paths in `find_and_validate_specific_team_id`
and `find_team_with_model_access` behind a new opt-in flag
`team_claim_fallback` on `LiteLLM_JWTAuth` (default False).

Default-off preserves the pre-existing strict behavior. Operators
who intentionally treat IdP team claims as advisory (e.g. machine
tokens whose group claims live in a separate namespace from
LiteLLM team_ids) opt in via config.

Adds two regression tests guarding the default-off behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…26418)

* fix(proxy): infer team from DB when JWT has no team and user has one team

- When team_id is unset after JWT auth but the user row has exactly one
  team, set team_id, team_object, and team_membership from DB.
- Skip when zero or multiple teams (ambiguous).
- Add parametrized unit tests in test_handle_jwt.py.

Made-with: Cursor

* fix(proxy): JWT single-team DB fallback: catch errors, tests match get_team_object

- Wrap get_team_object + get_team_membership in one try/except; log and skip on failure (stale/missing team id no longer fails auth).
- Parametrize tests: HTTP 404/500, membership error; use side_effect not return_value=None for missing team row.

Made-with: Cursor

* refactor(jwt): extract single-team fallback into _resolve_single_team_fallback helper

Made-with: Cursor
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…erriAI#28913)

* fix(jwt-auth): defer to single-team DB fallback on claim mismatch

Extends the single-team DB fallback introduced in BerriAI#26418 to two more
cases where it previously could not run:

* `find_and_validate_specific_team_id`: when `team_id_jwt_field` is
  configured and a claim value is present in the token but the team
  does not exist in the LiteLLM DB (HTTPException 404 from
  `get_team_object`), return `(None, None)` instead of raising — the
  auth_builder fallback then attributes the request to the user's
  single DB team. Only HTTPException is caught; other errors (e.g.
  "No DB Connected") still propagate.

* `find_team_with_model_access`: when none of the `team_ids_jwt_field`
  groups resolve to a real LiteLLM team, return `(None, None)` instead
  of raising 403 so the same fallback path runs. If at least one group
  DID resolve to a team but none granted the requested model, the
  original 403 is preserved (legitimate access denial — not a claim
  mismatch). Tracked via the new `any_claim_team_resolved` flag.

The strict `is_required_team_id` raise and `enforce_team_based_model_access`
raise remain unchanged. Unit tests cover both new soft-fail paths and
guard each preserved path (strict required, enforce_team_based, the
preserved 403, and the non-HTTPException propagation).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(jwt-auth): narrow HTTPException catch to 404 (greptile review)

Address Greptile review comments on BerriAI#28913:

* `find_and_validate_specific_team_id`: re-raise HTTPException when
  `status_code != 404`, pinning the catch to the "team doesn't exist
  in db" path documented for `get_team_object`. A future change that
  introduces a different status code (e.g. 403 for a blocked team)
  will now propagate instead of silently falling through to the
  single-team DB fallback.

* Add `test_find_and_validate_specific_team_id_non_404_http_exception_propagates`
  parametrised over 400 / 403 / 500 to lock in the contract.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(jwt-auth): gate claim-mismatch fallback behind opt-in flag

The unresolved-team-claim fallback added in the previous commit
weakened the strict claim-based authorization contract by default —
an authenticated user whose JWT carries a stale or invalid team
claim could still consume their single DB team's models/quota via
the fallback.

Gate both soft-fail paths in `find_and_validate_specific_team_id`
and `find_team_with_model_access` behind a new opt-in flag
`team_claim_fallback` on `LiteLLM_JWTAuth` (default False).

Default-off preserves the pre-existing strict behavior. Operators
who intentionally treat IdP team claims as advisory (e.g. machine
tokens whose group claims live in a separate namespace from
LiteLLM team_ids) opt in via config.

Adds two regression tests guarding the default-off behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants