refactor(auth_v2): split oidc/saml/scim into sub-packages

Per the revised design §2, each protocol that carries its own config/routes
becomes a sub-package while the shared core stays flat. oidc.py -> oidc/router.py
with oidc/config.py (OIDCProviderConfig); saml.py -> saml/router.py with
saml/config.py (SAMLConfig + the attribute-map default); scim.py -> scim/router.py.
Each sub-package __init__ re-exports its public names so call sites read
from litellm.proxy.auth_v2.saml import SAMLConfig, build_saml_router. SessionConfig
moves next to the SessionStore it configures in session.py. git mv preserves
history; AuthConfig now composes the protocol configs from their sub-packages.
This commit is contained in:
Yassin Kortam 2026-06-10 19:25:44 -07:00
parent 0ee4397a59
commit 9b9cc60994
12 changed files with 93 additions and 76 deletions

View file

@ -4,18 +4,16 @@ from .config import (
HttpBasicConfig,
MutualTLSConfig,
OAuth2IntrospectionConfig,
OIDCProviderConfig,
SAMLConfig,
SessionConfig,
TrustedProxyConfig,
)
from .models import Principal
from .oidc import build_oidc_router
from .oidc import OIDCProviderConfig, build_oidc_router
from .rbac import Role
from .resolver import IdentityResolver, InMemoryIdentityStore, ProvisioningStore
from .saml import build_saml_router
from .saml import SAMLConfig, build_saml_router
from .scim import build_scim_router
from .security import AuthSecurity
from .session import SessionConfig
__all__ = [
"AuthSecurity",

View file

@ -21,9 +21,9 @@ from .config import (
HttpBasicConfig,
MutualTLSConfig,
OAuth2IntrospectionConfig,
OIDCProviderConfig,
TrustedProxyConfig,
)
from .oidc.config import OIDCProviderConfig
from .models import (
AuthMethod,
ClientCertificate,

View file

@ -1,23 +1,11 @@
from __future__ import annotations
from typing import List, Optional
from typing import Dict, List, Optional
from pydantic import AnyHttpUrl, BaseModel, Field, SecretStr, model_validator
from pydantic import AnyHttpUrl, BaseModel, Field, SecretStr
from .models import SecuritySchemeType
DEFAULT_SAML_ATTRIBUTE_MAP = {
"email": "email",
"mail": "email",
"givenName": "given_name",
"surname": "family_name",
"sn": "family_name",
"displayName": "display_name",
"userName": "user_name",
"uid": "user_name",
"groups": "groups",
"roles": "roles",
}
from .oidc.config import OIDCProviderConfig
from .saml.config import SAMLConfig
from .session import SessionConfig
class ApiKeySchemeConfig(BaseModel):
@ -29,19 +17,6 @@ class HttpBasicConfig(BaseModel):
realm: str = "litellm"
class OIDCProviderConfig(BaseModel):
issuer: str
audience: List[str]
jwks_uri: Optional[AnyHttpUrl] = None
algorithms: List[str] = Field(default_factory=lambda: ["RS256"])
require_at_jwt: bool = False
client_id: Optional[str] = None
client_secret: Optional[SecretStr] = None
login_scopes: List[str] = Field(
default_factory=lambda: ["openid", "email", "profile"]
)
class OAuth2IntrospectionConfig(BaseModel):
introspection_endpoint: AnyHttpUrl
client_id: str
@ -60,38 +35,6 @@ class TrustedProxyConfig(BaseModel):
trusted_proxy_cidrs: List[str] = Field(default_factory=list)
class SessionConfig(BaseModel):
cookie: str = "litellm_session"
secure: bool = True
ttl_seconds: int = 3600
max_size: int = 10000
default_redirect_path: str = "/"
login_cookie: str = "litellm_oidc_txn"
login_state_ttl: int = 300
class SAMLConfig(BaseModel):
enabled: bool = False
entity_id: str
acs_url: str
idp_metadata: str = ""
sp_key_file: Optional[str] = None
sp_cert_file: Optional[str] = None
allow_unsolicited: bool = False
xmlsec_binary: Optional[str] = None
attribute_map: Dict[str, str] = Field(
default_factory=lambda: dict(DEFAULT_SAML_ATTRIBUTE_MAP)
)
@model_validator(mode="after")
def _require_idp_metadata(self) -> "SAMLConfig":
if self.enabled and not self.idp_metadata.strip():
raise ValueError(
"SAML enabled but idp_metadata is empty (inline XML, local path, or URL)"
)
return self
class AuthConfig(BaseModel):
scheme_order: List[SecuritySchemeType] = Field(
default_factory=lambda: [

View file

@ -0,0 +1,4 @@
from .config import OIDCProviderConfig
from .router import build_oidc_router
__all__ = ["OIDCProviderConfig", "build_oidc_router"]

View file

@ -0,0 +1,16 @@
from typing import List, Optional
from pydantic import AnyHttpUrl, BaseModel, Field, SecretStr
class OIDCProviderConfig(BaseModel):
issuer: str
audience: List[str]
jwks_uri: Optional[AnyHttpUrl] = None
algorithms: List[str] = Field(default_factory=lambda: ["RS256"])
require_at_jwt: bool = False
client_id: Optional[str] = None
client_secret: Optional[SecretStr] = None
login_scopes: List[str] = Field(
default_factory=lambda: ["openid", "email", "profile"]
)

View file

@ -9,11 +9,11 @@ from fastapi.responses import RedirectResponse
from scim2_models import User as ScimUser
from .config import OIDCProviderConfig
from .resolver import ProvisioningStore
from .session import safe_relay_state
from ..resolver import ProvisioningStore
from ..session import safe_relay_state
if TYPE_CHECKING:
from .security import AuthSecurity
from ..security import AuthSecurity
_CLAIM_KEYS = ("email", "preferred_username", "name", "groups", "roles")

View file

@ -0,0 +1,4 @@
from .config import SAMLConfig
from .router import build_saml_router
__all__ = ["SAMLConfig", "build_saml_router"]

View file

@ -0,0 +1,38 @@
from typing import Dict, Optional
from pydantic import BaseModel, Field, model_validator
DEFAULT_SAML_ATTRIBUTE_MAP = {
"email": "email",
"mail": "email",
"givenName": "given_name",
"surname": "family_name",
"sn": "family_name",
"displayName": "display_name",
"userName": "user_name",
"uid": "user_name",
"groups": "groups",
"roles": "roles",
}
class SAMLConfig(BaseModel):
enabled: bool = False
entity_id: str
acs_url: str
idp_metadata: str = ""
sp_key_file: Optional[str] = None
sp_cert_file: Optional[str] = None
allow_unsolicited: bool = False
xmlsec_binary: Optional[str] = None
attribute_map: Dict[str, str] = Field(
default_factory=lambda: dict(DEFAULT_SAML_ATTRIBUTE_MAP)
)
@model_validator(mode="after")
def _require_idp_metadata(self) -> "SAMLConfig":
if self.enabled and not self.idp_metadata.strip():
raise ValueError(
"SAML enabled but idp_metadata is empty (inline XML, local path, or URL)"
)
return self

View file

@ -13,11 +13,11 @@ from scim2_models import Email, Name
from scim2_models import User as ScimUser
from .config import SAMLConfig
from .resolver import ProvisioningStore
from .session import safe_relay_state
from ..resolver import ProvisioningStore
from ..session import safe_relay_state
if TYPE_CHECKING:
from .security import AuthSecurity
from ..security import AuthSecurity
_SINGLE_VALUE_TARGETS = {
"email",

View file

@ -0,0 +1,3 @@
from .router import build_scim_router
__all__ = ["build_scim_router"]

View file

@ -23,10 +23,10 @@ from scim2_models import (
User,
)
from .resolver import ProvisioningStore
from ..resolver import ProvisioningStore
if TYPE_CHECKING:
from .security import AuthSecurity
from ..security import AuthSecurity
R = TypeVar("R", bound=Resource)

View file

@ -5,10 +5,21 @@ import time
from typing import Any, Dict, Optional, Tuple
from fastapi import Request
from pydantic import BaseModel
from .models import AuthMethod, Credential, CredentialRef, SecuritySchemeType
class SessionConfig(BaseModel):
cookie: str = "litellm_session"
secure: bool = True
ttl_seconds: int = 3600
max_size: int = 10000
default_redirect_path: str = "/"
login_cookie: str = "litellm_oidc_txn"
login_state_ttl: int = 300
def safe_relay_state(target: Optional[str], default: str) -> str:
if (
target