# Public registration orchestration contract Status: implementation contract for NK-WP-0025-T02 and USER-WP-0022-T01/T02. ## Boundary `user-engine` owns the public registration state machine. It never accepts an LLDAP password or an LLDAP administrator credential. A verification service proves mailbox control; the existing identity-provisioner creates the ordinary LLDAP identity and returns a provider-owned, single-use password-setup URL. The public browser is not an authenticated user-engine actor. It receives only opaque registration and verification handles. A verified applicant actor is constructed from consumed verification evidence and is authorized only for the registration instance named by that evidence. ## Public flow 1. `POST /api/v1/public/registrations` accepts `username`, `email`, `display_name`, `client_id`, and optional `tenant_hint`. Client and tenant values must be registered allow-list entries; browser-provided return URLs are rejected. 2. The response is always the same `202 Accepted` envelope for syntactically valid requests, whether the username/email is new, duplicated, throttled, or already pending. It contains no existence result. 3. user-engine records an expiring registration intent and emits `registration.verification_requested` through its transactional outbox. The mail receiver obtains the recipient and an opaque verification handle; the general event receiver gets only `recipient_present=true`. 4. `POST /api/v1/public/registrations/verify` consumes the opaque handle. Verification is single-use, expiry checked, purpose bound, and returns normalized mailbox evidence bound to the registration ID and submitted username. 5. user-engine attaches the verified email factor, completes registration, and calls identity-provisioner using the registration ID as the stable idempotency key. Only the baseline ordinary-user group may be requested. 6. The provisioner creates or resumes exactly one LLDAP identity, links the provider subject to the user-engine user, and returns its single-use password-setup URL. Provider collision is a reconciliation result, never a second privileged identity. 7. After provider password setup, the browser starts a fresh KeyCape authorization-code/PKCE flow for the allow-listed client. Registration state is never accepted as an application session. ## Required state Registration intent persistence must include: - opaque registration ID and lifecycle status; - normalized username and email hash, with display values separately protected from general event/audit output; - registered client ID and tenant; - verification handle digest, issued/expiry/consumed timestamps, attempt counters, and purpose; - provisioning idempotency key and external provider result; - allow-listed post-setup authorization target identifier, not a raw URL; - correlation ID and redacted failure/compensation state. No password, verification token plaintext, LLDAP admin credential, MFA seed, or provider bearer token may enter registration state, logs, audit, State Hub, or the general event lane. ## Validation and abuse controls - Usernames use the LLDAP-compatible canonical grammar, length limit, and reserved-name list; case folding happens before uniqueness checks. - Email is normalized for comparison without provider-specific alias rewriting. Mailbox verification proves control but does not authorize linking to an existing user. - Rate limits apply per source network bucket, normalized email digest, and username digest. Responses and timing remain non-enumerating. - CSRF applies to browser form submission; JSON callers additionally require an allow-listed client registration and idempotency key. - Verification handles are random, high entropy, stored only as a digest, purpose-bound, expiring, and atomically consumed. - Duplicate email/username, expired/replayed verification, and provider conflict enter explicit reconciliation or terminal states. They never fall back to auto-linking by email. ## Provider contract The identity-provisioner request is the existing `ProvisioningRequest` plus a validated canonical username. The response remains `ProvisioningResult` with `external_subject`, `status`, and optional `password_setup_url`. For public registration the adapter must guarantee: - the same idempotency key returns the same external subject; - baseline membership only; no tenant-admin or platform groups; - password setup is provider-owned, single-use, and expiring; - partial creation is resumable/reconcilable and observable without exposing credentials; - the external subject is linked before registration reports completion. ## Activation gates Public registration stays disabled until all of the following pass together: 1. verification issuer and mail receiver conformance, including replay and expiry; 2. user-engine persistence migration and anonymous-route abuse controls; 3. flex-auth policy limiting the verified applicant actor to its registration; 4. identity-provisioner canonical-username/idempotency conformance; 5. KeyCape fresh-login return flow and client allow-list checks; 6. disposable-user live tests and cleanup on railiance01. ## Runtime configuration The source implementation is fail closed and remains disabled unless all of these are configured together: - `USER_ENGINE_PUBLIC_REGISTRATION=true`; - `USER_ENGINE_REGISTRATION_VERIFICATION_URL` and its OpenBao-backed `USER_ENGINE_REGISTRATION_VERIFICATION_TOKEN`; - comma-separated `USER_ENGINE_REGISTRATION_CLIENTS` and `USER_ENGINE_REGISTRATION_TENANTS` allow-lists; - `USER_ENGINE_REGISTRATION_PASSWORD_SETUP_ORIGINS`, containing only the trusted HTTPS provider origins allowed to receive the browser. After verification, user-engine completes the local user, provisions with the deterministic `public-registration-` idempotency key and only the `user` role, then links the returned subject under the configured KeyCape issuer. A setup URL outside the explicit HTTPS origin allow-list is rejected.