diff --git a/litellm/proxy/auth_v2/__init__.py b/litellm/proxy/auth_v2/__init__.py index 2bd9527afc7..9c31dbce9ed 100644 --- a/litellm/proxy/auth_v2/__init__.py +++ b/litellm/proxy/auth_v2/__init__.py @@ -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", diff --git a/litellm/proxy/auth_v2/authenticators.py b/litellm/proxy/auth_v2/authenticators.py index 7f4ba45b5f4..4ea6f86163c 100644 --- a/litellm/proxy/auth_v2/authenticators.py +++ b/litellm/proxy/auth_v2/authenticators.py @@ -21,9 +21,9 @@ from .config import ( HttpBasicConfig, MutualTLSConfig, OAuth2IntrospectionConfig, - OIDCProviderConfig, TrustedProxyConfig, ) +from .oidc.config import OIDCProviderConfig from .models import ( AuthMethod, ClientCertificate, diff --git a/litellm/proxy/auth_v2/config.py b/litellm/proxy/auth_v2/config.py index 41558a45de2..e82cf10dc15 100644 --- a/litellm/proxy/auth_v2/config.py +++ b/litellm/proxy/auth_v2/config.py @@ -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: [ diff --git a/litellm/proxy/auth_v2/oidc/__init__.py b/litellm/proxy/auth_v2/oidc/__init__.py new file mode 100644 index 00000000000..0e9c3e12a15 --- /dev/null +++ b/litellm/proxy/auth_v2/oidc/__init__.py @@ -0,0 +1,4 @@ +from .config import OIDCProviderConfig +from .router import build_oidc_router + +__all__ = ["OIDCProviderConfig", "build_oidc_router"] diff --git a/litellm/proxy/auth_v2/oidc/config.py b/litellm/proxy/auth_v2/oidc/config.py new file mode 100644 index 00000000000..5859ed07d9a --- /dev/null +++ b/litellm/proxy/auth_v2/oidc/config.py @@ -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"] + ) diff --git a/litellm/proxy/auth_v2/oidc.py b/litellm/proxy/auth_v2/oidc/router.py similarity index 97% rename from litellm/proxy/auth_v2/oidc.py rename to litellm/proxy/auth_v2/oidc/router.py index 28712597846..61d72bcc8e5 100644 --- a/litellm/proxy/auth_v2/oidc.py +++ b/litellm/proxy/auth_v2/oidc/router.py @@ -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") diff --git a/litellm/proxy/auth_v2/saml/__init__.py b/litellm/proxy/auth_v2/saml/__init__.py new file mode 100644 index 00000000000..c6eb35a995e --- /dev/null +++ b/litellm/proxy/auth_v2/saml/__init__.py @@ -0,0 +1,4 @@ +from .config import SAMLConfig +from .router import build_saml_router + +__all__ = ["SAMLConfig", "build_saml_router"] diff --git a/litellm/proxy/auth_v2/saml/config.py b/litellm/proxy/auth_v2/saml/config.py new file mode 100644 index 00000000000..4c83fd87225 --- /dev/null +++ b/litellm/proxy/auth_v2/saml/config.py @@ -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 diff --git a/litellm/proxy/auth_v2/saml.py b/litellm/proxy/auth_v2/saml/router.py similarity index 98% rename from litellm/proxy/auth_v2/saml.py rename to litellm/proxy/auth_v2/saml/router.py index 5f0a2e8bfb9..c5018ac4169 100644 --- a/litellm/proxy/auth_v2/saml.py +++ b/litellm/proxy/auth_v2/saml/router.py @@ -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", diff --git a/litellm/proxy/auth_v2/scim/__init__.py b/litellm/proxy/auth_v2/scim/__init__.py new file mode 100644 index 00000000000..bb6917c367b --- /dev/null +++ b/litellm/proxy/auth_v2/scim/__init__.py @@ -0,0 +1,3 @@ +from .router import build_scim_router + +__all__ = ["build_scim_router"] diff --git a/litellm/proxy/auth_v2/scim.py b/litellm/proxy/auth_v2/scim/router.py similarity index 99% rename from litellm/proxy/auth_v2/scim.py rename to litellm/proxy/auth_v2/scim/router.py index 64255db266a..dc64380b164 100644 --- a/litellm/proxy/auth_v2/scim.py +++ b/litellm/proxy/auth_v2/scim/router.py @@ -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) diff --git a/litellm/proxy/auth_v2/session.py b/litellm/proxy/auth_v2/session.py index fe671ce40f6..2636e9a5c8e 100644 --- a/litellm/proxy/auth_v2/session.py +++ b/litellm/proxy/auth_v2/session.py @@ -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