mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
fix(auth): scope the JWT enterprise gate to actual JWTs (#33296)
With enable_jwt_auth enabled but no enterprise license (premium_user False), the JWT premium check fired on every request before the token was inspected, so the master key, sk- virtual keys, and the encrypted CLI/UI SSO session token that `lite login` issues all 401'd with "JWT Auth is an enterprise only feature" and were never decoded. That broke `lite login`, `lite claude`, and the proxy master key on any deployment that turned JWT auth on without a license. Move the premium check inside the is_jwt branch so it gates only real JWTs. Non-JWT credentials fall through to their own auth paths regardless of license; actual JWTs still require premium, so the enterprise gate is unchanged for the feature it protects.
This commit is contained in:
parent
8c776605d8
commit
f7849f9e91
2 changed files with 80 additions and 4 deletions
|
|
@ -1191,13 +1191,15 @@ async def _user_api_key_auth_builder(
|
|||
return await handle_oauth2_proxy_request(request=request)
|
||||
|
||||
if general_settings.get("enable_jwt_auth", False) is True:
|
||||
from litellm.proxy.proxy_server import premium_user
|
||||
|
||||
if premium_user is not True:
|
||||
raise ValueError(f"JWT Auth is an enterprise only feature. {CommonProxyErrors.not_premium_user.value}")
|
||||
is_jwt = jwt_handler.is_jwt(token=api_key)
|
||||
verbose_proxy_logger.debug("is_jwt: %s", is_jwt)
|
||||
if is_jwt:
|
||||
from litellm.proxy.proxy_server import premium_user
|
||||
|
||||
if premium_user is not True:
|
||||
raise ValueError(
|
||||
f"JWT Auth is an enterprise only feature. {CommonProxyErrors.not_premium_user.value}"
|
||||
)
|
||||
# Try JWT-to-Virtual-Key mapping first to avoid
|
||||
# unnecessary DB queries in auth_builder
|
||||
do_standard_jwt_auth = True
|
||||
|
|
|
|||
|
|
@ -3992,6 +3992,80 @@ async def test_non_admin_cli_session_token_reaches_production_auth_path(monkeypa
|
|||
assert result.is_session_token is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cli_session_token_authenticates_when_jwt_auth_enabled_without_license(monkeypatch):
|
||||
"""A lite login token is an encrypted (non-JWT) session blob. With
|
||||
enable_jwt_auth on and no enterprise license (premium_user False), the JWT
|
||||
premium gate used to fire for every request before the token was decoded, so
|
||||
the CLI token 401'd with 'JWT Auth is an enterprise only feature' and was
|
||||
never decrypted. The gate must apply only to actual JWTs; a non-JWT session
|
||||
token has to keep authenticating on its own path regardless of license."""
|
||||
monkeypatch.delenv("EXPERIMENTAL_UI_LOGIN", raising=False)
|
||||
cli_token = _mint_cli_session_token(monkeypatch)
|
||||
|
||||
jwt_handler = MagicMock()
|
||||
jwt_handler.is_jwt = JWTHandler.is_jwt
|
||||
jwt_handler.litellm_jwtauth = LiteLLM_JWTAuth()
|
||||
|
||||
mock_request = MagicMock()
|
||||
mock_request.url.path = "/v1/messages"
|
||||
mock_request.method = "POST"
|
||||
mock_request.headers = {"authorization": f"Bearer {cli_token}"}
|
||||
mock_request.query_params = {}
|
||||
|
||||
with (
|
||||
patch("litellm.proxy.proxy_server.general_settings", {"enable_jwt_auth": True}),
|
||||
patch("litellm.proxy.proxy_server.premium_user", False),
|
||||
patch("litellm.proxy.proxy_server.jwt_handler", jwt_handler),
|
||||
patch("litellm.proxy.proxy_server.master_key", "sk-master"),
|
||||
patch("litellm.proxy.proxy_server.prisma_client", None),
|
||||
):
|
||||
result = await user_api_key_auth(
|
||||
request=mock_request,
|
||||
api_key=f"Bearer {cli_token}",
|
||||
)
|
||||
|
||||
assert result.user_id == "cli-admin"
|
||||
assert result.team_id == "cli-team"
|
||||
assert result.token is not None and result.token.startswith("cli-session-")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_real_jwt_still_requires_license_when_jwt_auth_enabled(monkeypatch):
|
||||
"""Guard for the reorder above: the enterprise gate must still reject an
|
||||
actual JWT when there is no license. Moving the premium check inside the
|
||||
is_jwt branch must not open JWT auth to non-premium deployments."""
|
||||
monkeypatch.delenv("EXPERIMENTAL_UI_LOGIN", raising=False)
|
||||
monkeypatch.setenv("LITELLM_SALT_KEY", "sk-salt-cli-test")
|
||||
|
||||
jwt_token = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyMSJ9.sig"
|
||||
jwt_handler = MagicMock()
|
||||
jwt_handler.is_jwt = JWTHandler.is_jwt
|
||||
jwt_handler.litellm_jwtauth = LiteLLM_JWTAuth()
|
||||
|
||||
mock_request = MagicMock()
|
||||
mock_request.url.path = "/v1/messages"
|
||||
mock_request.method = "POST"
|
||||
mock_request.headers = {"authorization": f"Bearer {jwt_token}"}
|
||||
mock_request.query_params = {}
|
||||
|
||||
with (
|
||||
patch("litellm.proxy.proxy_server.general_settings", {"enable_jwt_auth": True}),
|
||||
patch("litellm.proxy.proxy_server.premium_user", False),
|
||||
patch("litellm.proxy.proxy_server.jwt_handler", jwt_handler),
|
||||
patch("litellm.proxy.proxy_server.master_key", "sk-master"),
|
||||
patch("litellm.proxy.proxy_server.prisma_client", None),
|
||||
):
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
await user_api_key_auth(
|
||||
request=mock_request,
|
||||
api_key=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
message = str(getattr(exc_info.value, "message", exc_info.value))
|
||||
assert "enterprise only feature" in message
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auth_path_caches_team_object_under_canonical_team_id_key():
|
||||
"""Regression for LIT-4000: the auth builder must cache the team object under
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue