From d3878ee9a931fb67f70598d52ea8bcba3f973b5a Mon Sep 17 00:00:00 2001 From: Yassin Kortam Date: Wed, 10 Jun 2026 18:03:08 -0700 Subject: [PATCH] fix(auth_v2): require scim:write auth on all SCIM routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SCIM router mounted /scim/v2/* with no security dependency, so any caller could create or delete users and groups unauthenticated. Guard the whole router with Security(get_current_principal, scopes=["scim:write"]) per design 03 ยง11, so provisioning callers authenticate with the same bearer token or API key as every other route and the scope gates them: unauthenticated requests now 401, an authenticated principal without scim:write gets 403 insufficient_scope. Also document two deployment facts uncovered alongside this: uvicorn's --proxy-headers rewrites request.client from X-Forwarded-For before this module's trusted_proxy_cidrs check runs and silently bypasses it (install_auth docstring), and the scheme_order precedence where HTTP precedes openIdConnect so a bearer JWT is labeled bearer_jwt rather than oidc (both verify identically). --- litellm/auth_v2/config.py | 4 ++++ litellm/auth_v2/scim.py | 9 +++++++-- litellm/auth_v2/security.py | 9 +++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/litellm/auth_v2/config.py b/litellm/auth_v2/config.py index 16169e758ad..3e2acc7a78b 100644 --- a/litellm/auth_v2/config.py +++ b/litellm/auth_v2/config.py @@ -84,6 +84,10 @@ class SamlConfig(BaseModel): class AuthConfig(BaseModel): + # First-match-wins precedence. HTTP precedes OPENID_CONNECT, so a bearer JWT + # is claimed by HttpAuthenticator (auth_method=bearer_jwt) and OidcAuthenticator + # never runs; both share the same JwtVerifiers and verify identically, so this + # only changes the auth_method label. Reorder if openIdConnect labeling matters. scheme_order: List[SecuritySchemeType] = Field( default_factory=lambda: [ SecuritySchemeType.API_KEY, diff --git a/litellm/auth_v2/scim.py b/litellm/auth_v2/scim.py index d9a2739bbfa..0f9b13f1e33 100644 --- a/litellm/auth_v2/scim.py +++ b/litellm/auth_v2/scim.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any, Dict, Optional, Type, TypeVar -from fastapi import APIRouter, Request, Response, status +from fastapi import APIRouter, Request, Response, Security, status from fastapi.responses import JSONResponse from pydantic import ValidationError from scim2_models import ( @@ -23,6 +23,7 @@ from scim2_models import ( ) from .resolver import ProvisioningStore +from .security import get_current_principal R = TypeVar("R", bound=Resource) @@ -63,7 +64,11 @@ def _dump(resource: Resource, ctx: Context) -> Dict[str, Any]: def build_scim_router() -> APIRouter: - router = APIRouter(prefix="/scim/v2", tags=["scim"]) + router = APIRouter( + prefix="/scim/v2", + tags=["scim"], + dependencies=[Security(get_current_principal, scopes=["scim:write"])], + ) @router.post("/Users", status_code=status.HTTP_201_CREATED) async def create_user(request: Request) -> Response: diff --git a/litellm/auth_v2/security.py b/litellm/auth_v2/security.py index 5330509f617..7dd2721a3fb 100644 --- a/litellm/auth_v2/security.py +++ b/litellm/auth_v2/security.py @@ -31,6 +31,15 @@ def install_auth( mount_oidc: bool = True, mount_saml: bool = True, ) -> AuthContext: + """Wire the authenticators, resolver and optional routers onto the app. + + Deployment requirement for trusted-proxy IP resolution: uvicorn's + ``--proxy-headers`` (enabled by default) overwrites ``request.client`` from + ``X-Forwarded-For`` before this module's ``trusted_proxy_cidrs`` check runs, + which silently bypasses it. Run uvicorn with ``--no-proxy-headers`` and let + this module resolve the client IP, or leave ``trusted_proxy_cidrs`` empty and + rely on uvicorn's own ``--forwarded-allow-ips``. Do not enable both. + """ ctx = AuthContext(config, build_authenticators(config), resolver) app.state.auth_v2 = ctx if mount_scim: