Skip to content

fix(auth): support JWT issuer verification + warn when unscoped - #27008

Merged
yuneng-berri merged 3 commits into
BerriAI:litellm_internal_stagingfrom
stuxf:fix/jwt-audience-and-issuer-verification
May 2, 2026
Merged

fix(auth): support JWT issuer verification + warn when unscoped#27008
yuneng-berri merged 3 commits into
BerriAI:litellm_internal_stagingfrom
stuxf:fix/jwt-audience-and-issuer-verification

Conversation

@stuxf

@stuxf stuxf commented May 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

auth_jwt disables audience verification entirely when JWT_AUDIENCE is unset. Tokens minted by any other application that shares the same IdP signing keys (Azure AD, Okta, etc.) were accepted as long as the signature checked out — even when their aud and iss claims pointed at unrelated apps. Because cross-application tokens typically lack the team/user mappings the proxy expects, the resolution falls into the "no team_object / no user_object" branch where the access checks default-allow.

This change keeps the default behavior intact (so existing operators are not broken) but adds two scoping levers:

  • JWT_ISSUER — when set, PyJWT verifies the iss claim. Pairs with JWT_AUDIENCE to fully scope tokens to this proxy/IdP combination.
  • Once-per-process warning — if JWT auth is enabled but neither JWT_AUDIENCE nor JWT_ISSUER is configured, log a single warning so operators running the insecure default see a flag in their logs.

The duplicated jwt.decode calls (RSA/EC/OKP path and x509 path) are factored through a new _build_decode_kwargs helper that resolves audience, issuer, and the corresponding verify_* opt-outs in one place.

Behavior changes

  • No env vars set: same behavior as today. aud/iss not verified, but a warning fires once.
  • JWT_AUDIENCE set: PyJWT verifies aud. Tokens whose audience does not match are rejected. (This is the existing pathway, just exercised more reliably.)
  • JWT_ISSUER set: PyJWT verifies iss. New capability.

Test plan

  • uv run pytest tests/test_litellm/proxy/auth/test_handle_jwt.py -q — 56 pass (50 existing + 6 new for _build_decode_kwargs)
  • End-to-end smoke test: cross-tenant token rejected with InvalidAudienceError, matching-audience token accepted
  • uv run black --check on touched files

Type

🐛 Bug Fix
✅ Test

yuneng-berri and others added 3 commits April 28, 2026 18:31
[Infra] Promote Internal Staging to main
[Infra] Promote Internal Staging to main
When JWT auth is enabled but `JWT_AUDIENCE` is unset, `auth_jwt`
disabled audience verification entirely. Tokens minted by any other
application that shared the same IdP signing keys (Azure AD, Okta,
etc.) were accepted as long as their signature checked out, even
though their `aud` and `iss` claims pointed at unrelated apps. The
proxy then fell into the no-team / no-user branch where access checks
default-allow.

This change:

1. Adds support for the `JWT_ISSUER` env var. When set, PyJWT verifies
   the token's `iss` claim — turning on the same defense for tokens
   that share an audience but come from a different IdP tenant.
2. Refactors the duplicated `jwt.decode` calls (RSA/EC/OKP path and
   x509 path) into a single `_build_decode_kwargs` helper that
   computes audience, issuer, and the corresponding `verify_*` opt-outs
   once per call.
3. Logs a single startup-time warning when JWT auth is enabled but
   neither `JWT_AUDIENCE` nor `JWT_ISSUER` is configured, so operators
   running the insecure default see a flag in their logs without
   getting spammed per-request.

Default behavior (no env vars) is preserved for backward compatibility.
Setting `JWT_AUDIENCE` and/or `JWT_ISSUER` opts into the verification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented May 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.33333% with 1 line in your changes missing coverage. Please review.

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

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a real security gap in JWT auth where tokens from any app sharing the same IdP signing keys were accepted when JWT_AUDIENCE was not set (no aud/iss verification). It adds a _build_decode_kwargs helper to centralise audience/issuer/options resolution, introduces a new JWT_ISSUER env-var for issuer verification, and emits a once-per-process warning when neither scoping variable is configured. The default behavior is fully preserved for backward compatibility.

Confidence Score: 4/5

Safe to merge; no regressions or security issues introduced — only P2 style/concurrency nits.

The implementation is logically correct, backward-compatible, and well-tested. Two P2 findings (non-atomic warning flag under concurrent access, and the slightly opaque options or None idiom) do not affect correctness or security. Score capped at 4 per P2-only ceiling.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/auth/handle_jwt.py Adds _build_decode_kwargs classmethod that centralises audience/issuer resolution and options building for both JWT decode paths; introduces JWT_ISSUER env-var verification and a once-per-process warning when neither scoping variable is set. Logic is correct and backward-compatible; two minor P2s (non-atomic warning flag, options or None idiom).
tests/test_litellm/proxy/auth/test_handle_jwt.py Adds 6 new unit tests covering all four combinations of JWT_AUDIENCE/JWT_ISSUER env vars plus the once-per-process warning behaviour; uses a proper autouse=False fixture to reset the class-level flag between test cases.

Reviews (1): Last reviewed commit: "fix(auth): support JWT issuer verificati..." | Re-trigger Greptile

Comment on lines +723 to +734
if (
audience is None
and issuer is None
and not cls._unscoped_jwt_warning_emitted
):
verbose_proxy_logger.warning(
"JWT auth is enabled but neither JWT_AUDIENCE nor JWT_ISSUER "
"is configured. Tokens minted by any application that shares "
"the same IdP signing keys will be accepted. Set JWT_AUDIENCE "
"(and ideally JWT_ISSUER) to scope this proxy."
)
cls._unscoped_jwt_warning_emitted = True

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.

P2 Non-atomic check-then-set on _unscoped_jwt_warning_emitted

The read of cls._unscoped_jwt_warning_emitted and the subsequent assignment are not atomic. Under asyncio or threaded concurrency two coroutines can both observe False, both emit the warning, and then both set the flag — resulting in duplicate log entries. Because this is only a warning (no correctness impact) the practical risk is very low, but a simple threading.Event or checking the flag after setting it would make the intent cleaner.

Comment on lines +742 to +746
return {
"audience": audience,
"issuer": issuer,
"options": options or None,
}

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.

P2 options or None silently conflates empty dict with None

When both JWT_AUDIENCE and JWT_ISSUER are set, options is {} (falsy), so options or None evaluates to None. PyJWT treats options=None and options={} identically (both use default option values), so the behavior is correct. However, the idiom is non-obvious: a reader might wonder whether passing options=None vs options={} has any difference. A more explicit form such as options if options else None would make the intention clearer without changing behavior.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@krrish-berri-2

Copy link
Copy Markdown
Contributor

please fix the docs issues by filing a PR to litellm-docs @stuxf

shin-berri pushed a commit to BerriAI/litellm-docs that referenced this pull request May 2, 2026
JWT_ISSUER was added in BerriAI/litellm#27008 to scope JWT auth to a specific
issuer. The litellm repo's documentation test expects every env var read in
the source to be listed in the environment-variables reference table; without
this entry that test fails on the source PR.
@yuneng-berri
yuneng-berri enabled auto-merge May 2, 2026 02:57
@yuneng-berri
yuneng-berri merged commit c3f7158 into BerriAI:litellm_internal_staging May 2, 2026
43 of 45 checks passed
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…er-verification

fix(auth): support JWT issuer verification + warn when unscoped
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.

3 participants