Implement NetKingdom identity shell for coulomb.social (CSOC-WP-0002)
Django scaffold aligned with the business delivery lane: tenant-keyed Member model without passwords, identity app as sole OIDC/session boundary, dev-claims login, authenticated /app/ shell, ADR-0001, and tests. T01/T02/T05/T06 done; OIDC registration, real user-engine HTTP, flex-auth, and packaging remain open.
This commit is contained in:
parent
2ec7761504
commit
01da195c13
51 changed files with 2215 additions and 41 deletions
103
coulomb_social/apps/identity/oidc.py
Normal file
103
coulomb_social/apps/identity/oidc.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
"""OIDC/PKCE helpers for NetKingdom IAM Profile issuers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
from typing import Any
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import httpx
|
||||
from authlib.integrations.httpx_client import OAuth2Client
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class OIDCConfigurationError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def oidc_configured() -> bool:
|
||||
return bool(
|
||||
settings.OIDC_ENABLED
|
||||
and settings.OIDC_ISSUER
|
||||
and settings.OIDC_CLIENT_ID
|
||||
and settings.OIDC_REDIRECT_URI
|
||||
)
|
||||
|
||||
|
||||
def discovery_document() -> dict[str, Any]:
|
||||
if not settings.OIDC_ISSUER and not settings.OIDC_DISCOVERY_URL:
|
||||
raise OIDCConfigurationError("OIDC_ISSUER or OIDC_DISCOVERY_URL required")
|
||||
url = settings.OIDC_DISCOVERY_URL or (
|
||||
settings.OIDC_ISSUER.rstrip("/") + "/.well-known/openid-configuration"
|
||||
)
|
||||
resp = httpx.get(url, timeout=15.0)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
def build_authorization_url(*, state: str, code_verifier: str) -> str:
|
||||
if not oidc_configured():
|
||||
raise OIDCConfigurationError("OIDC is not enabled/configured")
|
||||
doc = discovery_document()
|
||||
auth_endpoint = doc["authorization_endpoint"]
|
||||
client = OAuth2Client(
|
||||
client_id=settings.OIDC_CLIENT_ID,
|
||||
client_secret=settings.OIDC_CLIENT_SECRET or None,
|
||||
redirect_uri=settings.OIDC_REDIRECT_URI,
|
||||
scope=settings.OIDC_SCOPES,
|
||||
code_challenge_method="S256",
|
||||
)
|
||||
uri, _ = client.create_authorization_url(
|
||||
auth_endpoint,
|
||||
state=state,
|
||||
code_verifier=code_verifier,
|
||||
)
|
||||
return uri
|
||||
|
||||
|
||||
def exchange_code(code: str, *, code_verifier: str) -> dict[str, Any]:
|
||||
doc = discovery_document()
|
||||
token_endpoint = doc["token_endpoint"]
|
||||
client = OAuth2Client(
|
||||
client_id=settings.OIDC_CLIENT_ID,
|
||||
client_secret=settings.OIDC_CLIENT_SECRET or None,
|
||||
redirect_uri=settings.OIDC_REDIRECT_URI,
|
||||
)
|
||||
token = client.fetch_token(
|
||||
token_endpoint,
|
||||
code=code,
|
||||
code_verifier=code_verifier,
|
||||
grant_type="authorization_code",
|
||||
)
|
||||
return token
|
||||
|
||||
|
||||
def fetch_userinfo(access_token: str) -> dict[str, Any]:
|
||||
doc = discovery_document()
|
||||
userinfo_endpoint = doc.get("userinfo_endpoint")
|
||||
if not userinfo_endpoint:
|
||||
return {}
|
||||
resp = httpx.get(
|
||||
userinfo_endpoint,
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
timeout=15.0,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
def new_pkce_pair() -> tuple[str, str]:
|
||||
"""Return (state, code_verifier). Challenge is computed by Authlib client."""
|
||||
state = secrets.token_urlsafe(24)
|
||||
code_verifier = secrets.token_urlsafe(48)
|
||||
return state, code_verifier
|
||||
|
||||
|
||||
def claims_from_token_response(token: dict[str, Any], userinfo: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Merge id_token claims (if present as dict) with userinfo."""
|
||||
claims: dict[str, Any] = {}
|
||||
# authlib may leave id_token as JWT string; userinfo is preferred when available
|
||||
claims.update(userinfo or {})
|
||||
if not claims.get("sub") and isinstance(token.get("userinfo"), dict):
|
||||
claims.update(token["userinfo"])
|
||||
return claims
|
||||
Loading…
Add table
Add a link
Reference in a new issue