This commit is contained in:
Jens
2026-07-24 22:46:37 +02:00
parent 0101660bd7
commit bdf44eec04
26 changed files with 1770 additions and 24 deletions
+107
View File
@@ -0,0 +1,107 @@
"""Microsoft Entra ID (Azure AD) SSO via de OAuth2 authorization-code flow.
De ``msal``-bibliotheek wordt bewust *lazy* geïmporteerd: zolang SSO uit staat
(``ENTRA_ID_ENABLED=0``) hoeft het pakket niet geïnstalleerd te zijn en blijven de
bestaande tests en de kernapplicatie volledig werken.
"""
from __future__ import annotations
from dataclasses import dataclass
from django.conf import settings
class EntraConfigError(RuntimeError):
"""Entra ID is ingeschakeld maar niet (volledig) geconfigureerd."""
@dataclass(frozen=True)
class EntraConfig:
client_id: str
client_secret: str
tenant_id: str
scopes: list[str]
allowed_domains: tuple[str, ...]
redirect_uri: str | None
@property
def authority(self) -> str:
return f"https://login.microsoftonline.com/{self.tenant_id}"
def entra_enabled() -> bool:
return bool(getattr(settings, "ENTRA_ID_ENABLED", False))
def load_config() -> EntraConfig:
client_id = getattr(settings, "ENTRA_CLIENT_ID", "") or ""
client_secret = getattr(settings, "ENTRA_CLIENT_SECRET", "") or ""
tenant_id = getattr(settings, "ENTRA_TENANT_ID", "") or ""
if not (client_id and client_secret and tenant_id):
raise EntraConfigError(
"Entra ID staat aan maar ENTRA_CLIENT_ID, ENTRA_CLIENT_SECRET en "
"ENTRA_TENANT_ID zijn niet allemaal ingesteld."
)
scopes = list(getattr(settings, "ENTRA_SCOPES", ["User.Read"]))
allowed = tuple(
d.lower().lstrip("@")
for d in getattr(settings, "ENTRA_ALLOWED_DOMAINS", [])
if d
)
return EntraConfig(
client_id=client_id,
client_secret=client_secret,
tenant_id=tenant_id,
scopes=scopes,
allowed_domains=allowed,
redirect_uri=getattr(settings, "ENTRA_REDIRECT_URI", "") or None,
)
def _build_app(config: EntraConfig):
try:
import msal # lazy import: alleen nodig als SSO aan staat
except ModuleNotFoundError as exc: # pragma: no cover - hangt van omgeving af
raise EntraConfigError(
"Het pakket 'msal' is niet geïnstalleerd. Voeg het toe (zie pyproject.toml) "
"om Entra ID SSO te gebruiken."
) from exc
return msal.ConfidentialClientApplication(
client_id=config.client_id,
client_credential=config.client_secret,
authority=config.authority,
)
def build_auth_flow(config: EntraConfig, redirect_uri: str) -> dict:
"""Start de authorization-code flow en geef het flow-dict terug (bewaar in sessie)."""
app = _build_app(config)
return app.initiate_auth_code_flow(config.scopes, redirect_uri=redirect_uri)
def redeem_auth_code(config: EntraConfig, flow: dict, auth_response: dict) -> dict:
"""Wissel de teruggekeerde code in voor tokens en claims."""
app = _build_app(config)
return app.acquire_token_by_auth_code_flow(flow, auth_response)
def extract_identity(token_result: dict) -> tuple[str, str]:
"""Haal (e-mailadres, weergavenaam) uit de id-token-claims."""
claims = token_result.get("id_token_claims") or {}
email = (
claims.get("preferred_username")
or claims.get("email")
or claims.get("upn")
or ""
).strip()
name = (claims.get("name") or "").strip()
return email, name
def domain_allowed(config: EntraConfig, email: str) -> bool:
if not config.allowed_domains:
return True
domain = email.rsplit("@", 1)[-1].lower()
return domain in config.allowed_domains