Refresh
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user