73 lines
2.6 KiB
Python
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)}"
|