Make public registration provisioning resumable
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

This commit is contained in:
tegwick 2026-08-10 11:32:08 +02:00
parent c36a09bded
commit c12bc604a8
4 changed files with 130 additions and 8 deletions

View file

@ -9,9 +9,11 @@ of user-engine.
from __future__ import annotations
from dataclasses import asdict, is_dataclass
from dataclasses import asdict, is_dataclass, replace
from enum import Enum
from html import escape
import hashlib
import hmac
import json
import re
import secrets
@ -177,6 +179,10 @@ class PortalApplication:
return self._verify_public_registration(
environ, start_response, correlation_id
)
if path == "/api/v1/public/registrations/resume" and method == "POST":
return self._resume_public_registration(
environ, start_response, correlation_id
)
actor = self._actor(environ)
if path == "/api/v1/me" and method == "GET":
@ -907,6 +913,12 @@ class PortalApplication:
),
correlation_id=correlation_id,
)
resume_handle = secrets.token_urlsafe(32)
updated = replace(
updated,
provisioning_resume_hash=hashlib.sha256(resume_handle.encode()).hexdigest(),
)
self.service.store.save_registration_session(updated)
if self.provisioning is None or not self.registration_oidc_issuer:
raise RuntimeError("public registration provisioning is unavailable")
completion = self.service.complete_registration(
@ -916,26 +928,77 @@ class PortalApplication:
primary_email=evidence.normalized_email,
correlation_id=correlation_id,
)
try:
return self._provision_public_registration(
start_response, actor, completion.user, completion.session,
evidence.normalized_email, evidence.display_name,
evidence.preferred_username, correlation_id,
)
except RuntimeError:
return self._json(start_response, "202 Accepted", {
"status": "provisioning_pending",
"registration_id": updated.registration_id,
"resume_handle": resume_handle,
}, correlation_id)
def _resume_public_registration(self, environ, start_response, correlation_id):
if not self.public_registration:
raise NotFoundError("public registration is unavailable")
body = self._body(environ)
session = self.service.store.registration_session(str(body.get("registration_id") or ""))
handle = str(body.get("resume_handle") or "")
digest = hashlib.sha256(handle.encode()).hexdigest()
if (
session is None or session.status.value != "completed"
or not session.provisioning_resume_hash
or not hmac.compare_digest(digest, session.provisioning_resume_hash)
):
raise AuthorizationDenied("registration resume is invalid")
factors = self.service.store.factors_for_registration(session.registration_id)
email_factor = next((item for item in factors if item.factor_type == IdentityFactorType.EMAIL), None)
if email_factor is None or not session.user_id:
raise ValidationError("registration is not recoverable")
actor = Actor(
issuer="urn:netkingdom:public-registration", subject=str(session.started_by_subject),
tenant=session.tenant, principal_type=PrincipalType.HUMAN,
audience=("user-engine",), roles=("registration-applicant",),
authorized_party=session.client_id, preferred_username=session.applicant_username,
)
user = self.service.store.user(session.user_id)
if user is None:
raise ValidationError("registration user is unavailable")
return self._provision_public_registration(
start_response, actor, user, session, email_factor.normalized_value,
user.display_name, str(session.applicant_username), correlation_id,
)
def _provision_public_registration(
self, start_response, actor, user, session, email, display_name,
preferred_username, correlation_id,
):
provisioned = self.provisioning.provision(
ProvisioningRequest(
user_id=completion.user.user_id,
tenant=updated.tenant,
primary_email=evidence.normalized_email,
display_name=evidence.display_name,
preferred_username=evidence.preferred_username,
idempotency_key=f"public-registration-{updated.registration_id}",
user_id=user.user_id,
tenant=session.tenant,
primary_email=email,
display_name=display_name,
preferred_username=preferred_username,
idempotency_key=f"public-registration-{session.registration_id}",
correlation_id=correlation_id,
roles=("user",),
)
)
self.service.link_identity(
actor,
completion.user.user_id,
user.user_id,
issuer=self.registration_oidc_issuer,
subject=provisioned.external_subject,
provider=provisioned.provider,
correlation_id=correlation_id,
)
self.service.store.save_registration_session(
replace(session, provisioning_resume_hash=None)
)
if provisioned.password_setup_url:
self._validate_registration_handoff(provisioned.password_setup_url)
return self._redirect(