From 6f9d906abe725009e38506b2cef7accea4e627ba Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Fri, 5 Jun 2026 11:17:39 -0700 Subject: [PATCH] fix(proxy): make auth_v2 import-safe and lock its deps so CI/startup don't break Importing proxy_server registers the auth_v2 router, which pulled casbin in at module load via enforcer.py - so the proxy failed to even start in any env without casbin (CI, minimal installs), regardless of whether auth_v2 was enabled. An opt-in feature must not be a hard import. - enforcer.py imports casbin lazily inside CasbinEnforcer.__init__; authlib in jwt_verifier.py is imported lazily inside the verify/JWKS functions. The package (and the startup router) now import with both deps absent; they are required only when auth_v2 actually builds an enforcer or verifies a JWT. - casbin and authlib were declared in pyproject but missing from uv.lock, so `uv sync --frozen` never installed them. Regenerated the lock (adds casbin, authlib, joserfc, simpleeval) so the deps install where the feature runs. Verified: the v2 package imports with casbin+authlib blocked, and building an enforcer raises a clear ImportError only when casbin is genuinely absent. --- litellm/proxy/auth/v2/enforcer.py | 7 ++-- litellm/proxy/auth/v2/jwt_verifier.py | 7 ++-- uv.lock | 52 ++++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/litellm/proxy/auth/v2/enforcer.py b/litellm/proxy/auth/v2/enforcer.py index 4edb31757c4..4b9e7f91e84 100644 --- a/litellm/proxy/auth/v2/enforcer.py +++ b/litellm/proxy/auth/v2/enforcer.py @@ -1,8 +1,6 @@ import os from typing import List, Optional, Sequence -import casbin - _MODEL_PATH = os.path.join(os.path.dirname(__file__), "model.conf") Rule = Sequence[str] @@ -24,6 +22,11 @@ class CasbinEnforcer: resource_groupings: Optional[List[Rule]] = None, domain_groupings: Optional[List[Rule]] = None, ): + # Imported lazily so merely importing the proxy (or this package, e.g. to + # register the policy-admin router) never requires casbin. The dependency + # is only needed once auth_v2 is actually enabled and builds an enforcer. + import casbin + self._enforcer = casbin.Enforcer(_MODEL_PATH) self._enforcer.enable_auto_save(False) for rule in policies: diff --git a/litellm/proxy/auth/v2/jwt_verifier.py b/litellm/proxy/auth/v2/jwt_verifier.py index 9b1e7e12f0a..f74b3cd04b7 100644 --- a/litellm/proxy/auth/v2/jwt_verifier.py +++ b/litellm/proxy/auth/v2/jwt_verifier.py @@ -1,9 +1,6 @@ import time from typing import Any, Dict, List, Optional -from authlib.jose import JsonWebKey, JsonWebToken -from authlib.jose.errors import JoseError - class JWTVerificationError(Exception): """Raised when a token fails signature or standard-claim validation.""" @@ -55,6 +52,9 @@ def verify( network). Raises :class:`JWTVerificationError` on any failure so callers never branch on authlib's internal exception types. """ + from authlib.jose import JsonWebToken + from authlib.jose.errors import JoseError + decoder = JsonWebToken(algorithms or _DEFAULT_ALGORITHMS) try: claims = decoder.decode( @@ -83,6 +83,7 @@ class JWKSProvider: return self._key_set import httpx + from authlib.jose import JsonWebKey async with httpx.AsyncClient() as client: response = await client.get(self.jwks_uri) diff --git a/uv.lock b/uv.lock index db2c5cbb72a..58350fe540a 100644 --- a/uv.lock +++ b/uv.lock @@ -9,7 +9,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-05-28T03:32:27.927695Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P3D" [manifest] @@ -423,6 +423,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/1f/aa74b23b6eea4cf9b79ace914df59123c4c8e7e4bd32dd22d09c126422d9/aurelio_sdk-0.0.19-py3-none-any.whl", hash = "sha256:390c0212b59ce99116df8722d3badced88c5ef0bb742a6222d479ceed0ed3948", size = 17322, upload-time = "2025-03-24T14:37:31.305Z" }, ] +[[package]] +name = "authlib" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "joserfc" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/98/7d93f30d029643c0275dbc0bd6d5a6f670661ee6c9a94d93af7ab4887600/authlib-1.7.2.tar.gz", hash = "sha256:2cea25fefcd4e7173bdf1372c0afc265c8034b23a8cd5dcb6a9164b826c64231", size = 176511, upload-time = "2026-05-06T08:10:23.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/95/adcb68e20c34162e9135f370d6e31737719c2b6f94bc953fe7ed1f10fe21/authlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:3e1faedc9d87e7d56a164eca3ccb6ace0d61b94abe83e92242f8dc8bba9b4a9f", size = 259548, upload-time = "2026-05-06T08:10:21.436Z" }, +] + [[package]] name = "azure-ai-contentsafety" version = "1.0.0" @@ -655,6 +668,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bf/0f/f897abe4ea0a8c408ae65c8c83bffab4936ad65d6032d4fb4cd35bbdc3ee/cachetools-7.1.1-py3-none-any.whl", hash = "sha256:0335cd7a0952d2b22327441fb0628139e234c565559eeb91a8a4ac7551c5353d", size = 16775, upload-time = "2026-05-03T20:00:27.857Z" }, ] +[[package]] +name = "casbin" +version = "1.43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "simpleeval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/df/ff2aa55cf0d7c14622ce4f9252cdc34c828c81d4213965d73207ac5434ae/casbin-1.43.0.tar.gz", hash = "sha256:d2e90ce8e72f912877851e94d37999f32c558c6ba7aba0437d483275262e86e0", size = 425727, upload-time = "2025-05-10T06:57:18.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/07/facef6abd81378e6153b757dc1848621675d971fbc88ebb5d182ebc1c37f/casbin-1.43.0-py3-none-any.whl", hash = "sha256:63a3d1228870250e859ccd94133fe478821093f71dd37f05e0baa0c6fea26623", size = 475059, upload-time = "2025-05-10T06:57:16.89Z" }, +] + [[package]] name = "certifi" version = "2026.4.22" @@ -2735,6 +2760,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, ] +[[package]] +name = "joserfc" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/c3/2f590052b55cbdd0ace470ee7ee1f685f6882051be93a9374891005623e2/joserfc-1.7.0.tar.gz", hash = "sha256:4aced6ab0c47846f0a531402aec2419a874b91e918df9c4c9da8a82fb559d6c4", size = 232967, upload-time = "2026-06-02T09:59:34.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/83/b6b62a66a06ce872d9429a5eb5ee20b2002fd9c331b953c94381c1f7c9f9/joserfc-1.7.0-py3-none-any.whl", hash = "sha256:17e5d7a5a35e65442b05efc435a3d5d46696ffa2c8a2ed0eea6f63fc268e3224", size = 70387, upload-time = "2026-06-02T09:59:33.264Z" }, +] + [[package]] name = "jsonlines" version = "4.0.0" @@ -3318,10 +3355,12 @@ mlflow = [ ] proxy = [ { name = "apscheduler" }, + { name = "authlib" }, { name = "azure-identity" }, { name = "azure-storage-blob" }, { name = "backoff" }, { name = "boto3" }, + { name = "casbin" }, { name = "cryptography" }, { name = "fastapi" }, { name = "fastapi-sso" }, @@ -3472,6 +3511,7 @@ requires-dist = [ { name = "apscheduler", marker = "extra == 'proxy'", specifier = ">=3.11.2,<4.0" }, { name = "audioread", marker = "extra == 'stt-nvidia-riva'", specifier = ">=3.0.1" }, { name = "aurelio-sdk", marker = "python_full_version < '3.14' and extra == 'semantic-router'", specifier = ">=0.0.19,<1.0" }, + { name = "authlib", marker = "extra == 'proxy'", specifier = ">=1.6.5,<2.0" }, { name = "azure-ai-contentsafety", marker = "extra == 'proxy-runtime'", specifier = ">=1.0.0,<2.0" }, { name = "azure-identity", marker = "extra == 'extra-proxy'", specifier = ">=1.25.2,<2.0" }, { name = "azure-identity", marker = "extra == 'proxy'", specifier = ">=1.25.2,<2.0" }, @@ -3480,6 +3520,7 @@ requires-dist = [ { name = "azure-storage-file-datalake", marker = "extra == 'proxy-runtime'", specifier = ">=12.20.0,<13.0" }, { name = "backoff", marker = "extra == 'proxy'", specifier = ">=2.2.1,<3.0" }, { name = "boto3", marker = "extra == 'proxy'", specifier = ">=1.43.1,<2.0" }, + { name = "casbin", marker = "extra == 'proxy'", specifier = ">=1.43.0,<2.0" }, { name = "click", specifier = ">=8.0.0,<9.0" }, { name = "cryptography", marker = "extra == 'proxy'", specifier = ">=46.0.7,<47.0" }, { name = "ddtrace", marker = "extra == 'proxy-runtime'", specifier = ">=2.19.0,<3.0" }, @@ -7055,6 +7096,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] +[[package]] +name = "simpleeval" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/9d/e7c9309940794dd3073cba2e5101df5874d84243595ce63b1e1c8f9b9c76/simpleeval-1.0.7.tar.gz", hash = "sha256:1e10e5f9fec597814444e20c0892ed15162fa214c8a88f434b5b077cf2fef85b", size = 30250, upload-time = "2026-03-16T10:53:03.464Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/2f/f32aa85591882378bb43caa09363f3ed97df399369a5144c7f19f2275bc0/simpleeval-1.0.7-py3-none-any.whl", hash = "sha256:97ac271bfd8f2af9e7b9a36ceea67617f26fa873f9d5ae1922f64d4c1442534b", size = 18792, upload-time = "2026-03-16T10:53:02.103Z" }, +] + [[package]] name = "six" version = "1.17.0"