Implement verified public registration flow
This commit is contained in:
parent
b7a57a50a4
commit
c36a09bded
12 changed files with 610 additions and 7 deletions
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
86
src/user_engine/adapters/registration_verification.py
Normal file
86
src/user_engine/adapters/registration_verification.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue