Implement verified public registration flow
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

This commit is contained in:
tegwick 2026-08-10 11:26:18 +02:00
parent b7a57a50a4
commit c36a09bded
12 changed files with 610 additions and 7 deletions

View file

@ -10,6 +10,9 @@ from user_engine.adapters.provisioning import HTTPIdentityProvisioningAdapter
from user_engine.adapters.tenant_management import HTTPTenantManagementAdapter
from user_engine.adapters.flex_auth import FlexAuthHTTPAdapter
from user_engine.adapters.delivery import HTTPOutboxDeliveryAdapter
from user_engine.adapters.registration_verification import (
HTTPRegistrationVerificationAdapter,
)
__all__ = [
"InMemoryUserEngineStore",
@ -20,4 +23,5 @@ __all__ = [
"HTTPTenantManagementAdapter",
"FlexAuthHTTPAdapter",
"HTTPOutboxDeliveryAdapter",
"HTTPRegistrationVerificationAdapter",
]

View file

@ -36,6 +36,7 @@ class HTTPIdentityProvisioningAdapter:
"idempotency_key": request.idempotency_key,
"correlation_id": request.correlation_id,
"roles": request.roles,
"preferred_username": request.preferred_username,
})
def suspend(self, *, external_subject: str, idempotency_key: str, correlation_id: str) -> ProvisioningResult:
@ -88,6 +89,7 @@ class HTTPIdentityProvisioningAdapter:
"idempotency_key": request.idempotency_key,
"correlation_id": request.correlation_id,
"roles": request.roles,
"preferred_username": request.preferred_username,
"desired_status": desired_status,
})
return IdentityDriftResult(

View file

@ -0,0 +1,86 @@
"""HTTP adapter for the NetKingdom mailbox-verification issuer."""
from __future__ import annotations
import json
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
from user_engine.ports import (
RegistrationVerificationReceipt,
RegistrationVerificationRequest,
VerifiedRegistrationApplicant,
)
class HTTPRegistrationVerificationAdapter:
def __init__(
self, *, base_url: str, bearer_token: str, timeout: float = 5.0
) -> None:
self.base_url = base_url.rstrip("/")
self.bearer_token = bearer_token.strip()
if not self.bearer_token:
raise ValueError("bearer token must not be empty")
self.timeout = timeout
def request(
self, request: RegistrationVerificationRequest
) -> RegistrationVerificationReceipt:
result = self._post(
"/v1/registration-verifications",
{
"registration_id": request.registration_id,
"email": request.normalized_email,
"preferred_username": request.preferred_username,
"client_id": request.client_id,
"tenant": request.tenant,
"correlation_id": request.correlation_id,
"display_name": request.display_name,
},
)
return RegistrationVerificationReceipt(
request_id=str(result["request_id"]),
accepted=bool(result.get("accepted", True)),
)
def consume(self, opaque_handle: str) -> VerifiedRegistrationApplicant:
if len(opaque_handle) < 32:
raise ValueError("verification handle is invalid")
result = self._post(
"/v1/registration-verifications/consume", {"handle": opaque_handle}
)
if result.get("purpose") != "public-registration":
raise RuntimeError("verification evidence has the wrong purpose")
return VerifiedRegistrationApplicant(
verification_id=str(result["verification_id"]),
registration_id=str(result["registration_id"]),
normalized_email=str(result["email"]).strip().lower(),
preferred_username=str(result["preferred_username"]),
client_id=str(result["client_id"]),
tenant=str(result["tenant"]),
source_system=str(result["source_system"]),
assurance=dict(result.get("assurance", {})),
display_name=(
str(result["display_name"]) if result.get("display_name") else None
),
)
def _post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
request = Request(
self.base_url + path,
data=json.dumps(payload).encode(),
headers={
"Authorization": f"Bearer {self.bearer_token}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with urlopen(request, timeout=self.timeout) as response:
return dict(json.loads(response.read()))
except HTTPError as exc:
# Do not reflect provider details that could reveal account state.
raise RuntimeError("registration verification was rejected") from exc
except (URLError, json.JSONDecodeError, KeyError) as exc:
raise RuntimeError("registration verification is unavailable") from exc