"""OIDC 客户端构建和安全重定向辅助函数。""" from urllib.parse import quote from django.conf import settings from apps.accounts.models import EnterpriseIdentityProvider from apps.modelhub.crypto import decrypt_secret class OIDCConfigurationError(ValueError): pass def provider_metadata_url(provider: EnterpriseIdentityProvider) -> str: if provider.metadata_url: return provider.metadata_url if provider.issuer: return f"{provider.issuer.rstrip('/')}/.well-known/openid-configuration" raise OIDCConfigurationError("OIDC 提供方缺少发现文档地址或 issuer") def build_oidc_client(provider: EnterpriseIdentityProvider): """构建依赖发现文档和加密凭据的 OIDC 客户端。""" try: from authlib.integrations.django_client import OAuth except ImportError as error: raise OIDCConfigurationError("服务端未安装 Authlib") from error if not provider.client_id: raise OIDCConfigurationError("OIDC 提供方缺少 client_id") if not provider.credential_encrypted: raise OIDCConfigurationError("OIDC 提供方缺少 client secret") scopes = provider.scopes or ["openid", "profile", "email"] if "openid" not in scopes: raise OIDCConfigurationError("OIDC scopes 必须包含 openid") oauth = OAuth() return oauth.register( name=f"enterprise_oidc_{provider.pk}", client_id=provider.client_id, client_secret=decrypt_secret(provider.credential_encrypted), server_metadata_url=provider_metadata_url(provider), client_kwargs={ "scope": " ".join(scopes), "token_endpoint_auth_method": provider.configuration.get( "token_endpoint_auth_method", "client_secret_basic", ), }, ) def callback_uri(request, provider: EnterpriseIdentityProvider) -> str: path = f"/api/identity/auth/oidc/{provider.slug}/callback" if settings.OIDC_CALLBACK_BASE_URL: return f"{settings.OIDC_CALLBACK_BASE_URL}{path}" return request.build_absolute_uri(path) def normalize_return_to(value: str | None) -> str: """只允许站内绝对路径,阻断协议相对地址和外部开放重定向。""" if not value or not value.startswith("/") or value.startswith("//") or len(value) > 500: return "/" return value def frontend_redirect(return_to: str = "/", error: str = "") -> str: if error: return f"{settings.FRONTEND_APP_URL}/#/login?auth_error={quote(error[:240])}" return f"{settings.FRONTEND_APP_URL}/#{normalize_return_to(return_to)}"