Files
ai-ticket-platform/backend/apps/accounts/oidc.py

73 lines
2.6 KiB
Python

"""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)}"