TEN-WP-0005 landed the authoritative metadata update and reversible retirement contract, so USER-WP-0021-T01's deferred tenant operations are now implementable without user-engine inventing lifecycle semantics. TenantManagementPort gains read, update, retire, and reactivate. The HTTP adapter echoes the record version as an If-Match ETag (never `*`), sends an Idempotency-Key plus actor/reason/correlation_id, and surfaces Idempotent-Replay. Authority failures map to redacted domain errors carrying only the contract's stable error_code; its detail text never crosses the boundary. Platform operators get the matching API routes and a CSRF-protected browser screen that reads the record before mutating it and hides the metadata form for a retired tenant. Portal OpenAPI moves to 0.3.0 with TenantRecord, UpdateTenant, and TenantLifecycleChange. Full suite: 145 tests, 3 external-provider skips. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
252 lines
12 KiB
Markdown
252 lines
12 KiB
Markdown
---
|
|
id: USER-WP-0022
|
|
type: workplan
|
|
title: "Public registration and JIT application profiles"
|
|
domain: communication
|
|
repo: user-engine
|
|
status: blocked
|
|
owner: codex
|
|
topic_slug: netkingdom
|
|
created: "2026-08-09"
|
|
updated: "2026-08-16"
|
|
depends_on:
|
|
- USER-WP-0021
|
|
- NK-WP-0025
|
|
state_hub_workstream_id: "97145e5b-5f8f-4cb1-9135-2593f5baac8f"
|
|
---
|
|
|
|
# USER-WP-0022 - public registration and application JIT
|
|
|
|
Turn the existing provider-neutral registration domain into a safe public
|
|
product flow and provide idempotent first-login application-profile creation.
|
|
user-engine remains the user/profile source of truth but delegates login
|
|
identity creation to NetKingdom.
|
|
|
|
## T01 - Publish the public registration journey
|
|
|
|
```task
|
|
id: USER-WP-0022-T01
|
|
status: wait
|
|
priority: high
|
|
state_hub_task_id: "43dd49b7-6dbc-4117-a401-6d9f6e59aa26"
|
|
```
|
|
|
|
Add accessible browser/API entry points for start, email verification,
|
|
provider password-setup handoff, resume, expiry, cancel, and completion.
|
|
Enable public registration only through explicit runtime configuration and
|
|
signed allow-listed return context. Apply rate limits, anti-enumeration
|
|
responses, CSRF, idempotency, and safe diagnostics.
|
|
|
|
Done when the flow can be enabled per deployment without exposing whether an
|
|
email or username already exists.
|
|
|
|
Implementation contract established in NetKingdom at
|
|
`docs/public-registration-orchestration-contract.md`. The current
|
|
authenticated registration API will not be moved ahead of authentication;
|
|
the public adapter will use opaque intent handles and construct a narrowly
|
|
scoped applicant actor only from consumed verification evidence. First code
|
|
slice added `RegistrationVerificationPort`, bound request/receipt/evidence
|
|
types, and a fail-closed HTTP adapter with purpose checking and normalized
|
|
mailbox evidence. Adapter tests and the full 123-test suite pass.
|
|
|
|
Second code slice adds explicit allow-listed anonymous endpoints:
|
|
`POST /api/v1/public/registrations` returns only a generic 202 status, while
|
|
`POST /api/v1/public/registrations/verify` consumes purpose-bound evidence and
|
|
attaches the verified email factor. Persisted registration sessions bind the
|
|
random applicant subject, canonical username, client, and tenant. Evidence
|
|
mismatch fails closed, browser-supplied return URLs are ignored, and this
|
|
transition creates neither a user nor an LLDAP identity. Full suite now passes
|
|
125 tests with 3 environment-dependent skips.
|
|
|
|
Third code slice completes the verified happy path: create the local user,
|
|
call identity-provisioner with deterministic registration idempotency and only
|
|
the baseline `user` role, link the returned subject under the configured
|
|
KeyCape issuer, and redirect only to an allow-listed HTTPS provider password
|
|
setup origin. Full suite now passes 126 tests with 3 skips. Automated recovery
|
|
after local completion/provider failure remains open before production enablement.
|
|
|
|
2026-08-10 browser increment: `/register` now provides the accessible public
|
|
account form and the home page links to it only when registration is configured.
|
|
Anonymous mutations use a short-lived Secure/HttpOnly/SameSite double-submit
|
|
CSRF cookie. Verification links land on an explicit one-time confirmation page,
|
|
successful provisioning presents the allow-listed provider password handoff,
|
|
and provider outages render a retry form backed by the separate resume handle.
|
|
Responses remain non-enumerating. The full suite passes 128 tests with three
|
|
environment-dependent skips. Cancellation, ingress rate limiting, and the
|
|
credential-gated production enablement remain.
|
|
|
|
The tested UI image is published at
|
|
`forgejo.coulomb.social/coulomb/user-engine@sha256:2fdb60eec093a8c8a0bff1736ab515916cefac49348c0098d308728b72ab3594`.
|
|
A guarded rollout correctly failed closed because the production manifest does
|
|
not yet supply the required flex-auth/event delivery runtime configuration; the
|
|
last known-good revision was restored and remained available. Do not activate
|
|
this image or public registration until the OpenBao delivery and verification
|
|
tokens plus transactional SMTP lane are installed.
|
|
|
|
2026-08-10 security increment: every anonymous registration mutation now has
|
|
a bounded per-peer sliding-window limit, configurable through
|
|
`USER_ENGINE_REGISTRATION_RATE_LIMIT` and
|
|
`USER_ENGINE_REGISTRATION_RATE_WINDOW_SECONDS`. The application deliberately
|
|
ignores spoofable forwarding headers and relies on the ingress-normalized peer
|
|
address. Conformance proves isolation between peers and a generic 429 response.
|
|
Expired and replayed verification handles are also proven to produce the same
|
|
redacted rejection without provider or account detail. The full suite passes
|
|
130 tests with three environment-dependent skips. Cluster ingress throttling
|
|
remains defense in depth before public enablement.
|
|
|
|
2026-08-10 cancellation increment: each verification email now carries a
|
|
fixed cancellation link using the same digest-only opaque handle. A
|
|
CSRF-protected confirmation consumes purpose-bound cancellation evidence and
|
|
terminally abandons the matching registration. Cancellation and verification
|
|
atomically invalidate one another, replay fails closed, and no identity is
|
|
created from cancellation. The full user-engine suite passes 133 tests with
|
|
three environment-dependent skips; email-connect passes all 24 tests.
|
|
|
|
2026-08-10 idempotency completion: public API starts now require an
|
|
`Idempotency-Key`, while the browser form receives a cryptographically random
|
|
hidden key. user-engine persists only a keyed HMAC of that value plus a hash of
|
|
the normalized request. Exact retries return the same generic response without
|
|
creating another session or requesting another email; reuse with different
|
|
inputs returns 409. Raw keys and applicant fields are absent from idempotency
|
|
evidence. The full suite passes 134 tests with three external skips.
|
|
|
|
2026-08-16 gate review: every implementation item listed for this task is
|
|
built and covered — accessible start/verify/resume/cancel entry points,
|
|
allow-listed anonymous routes, opaque intent handles, CSRF, per-peer rate
|
|
limiting, required idempotency keys, and non-enumerating responses. The
|
|
application-level registration limit is configured in the runtime manifest on
|
|
top of the Traefik-wide limit, closing the ingress defense-in-depth item.
|
|
|
|
What remains is not user-engine code. Public registration stays disabled until
|
|
the OpenBao verification and delivery tokens plus the transactional SMTP lane
|
|
are delivered through governed channels. The service already fails closed when
|
|
they are absent, and no placeholder or reused credential will be added to
|
|
unblock it. This task stays in `wait` because the remaining gate is a
|
|
credential-custody handoff, not an outstanding change here.
|
|
|
|
## T02 - Orchestrate provider identity creation
|
|
|
|
```task
|
|
id: USER-WP-0022-T02
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "74239ce4-5c1e-46cf-8abf-3cbec3f3f989"
|
|
```
|
|
|
|
Use the existing provisioning port to create or resume an ordinary NetKingdom
|
|
identity only after required verification. Persist lifecycle intent and outbox
|
|
state atomically; compensate or surface recoverable partial state on provider
|
|
failure. Never accept or retain an LLDAP password, admin credential, MFA seed,
|
|
or verification secret in the user-engine domain.
|
|
|
|
Done when a registration can resume safely across timeout/retry and produces
|
|
one external identity link.
|
|
|
|
The existing provisioning port remains the privileged boundary. Public
|
|
registration adds canonical username and uses the registration ID as its
|
|
stable idempotency key; it accepts only the baseline ordinary-user group and
|
|
the provider-owned password-setup handoff. `ProvisioningRequest` now carries
|
|
an optional canonical `preferred_username` through the HTTP adapter without
|
|
changing existing callers.
|
|
|
|
Retry-safe recovery is now implemented for the local-completion/provider-
|
|
failure window. A single-use mailbox handle is never replayed; instead,
|
|
user-engine returns a separate random resume handle, stores only its SHA-256
|
|
digest, retries with the same registration idempotency key and local user,
|
|
and invalidates the handle after provider linking. Replay is denied. The full
|
|
suite passes 127 tests with 3 environment-dependent skips.
|
|
|
|
2026-08-10 completion: identity-provisioner now validates and honors the
|
|
canonical preferred username instead of silently deriving it from the mailbox.
|
|
The corrected image is live at immutable digest
|
|
`sha256:4a6ec93d74eed6e17065a7e5d4c3d8d0ed14078bdfda0112738438528c6b2680`.
|
|
Together with the deterministic retry path, this completes the provider
|
|
orchestration boundary without exposing provider credentials or passwords.
|
|
|
|
## T03 - Create application profiles on first login
|
|
|
|
```task
|
|
id: USER-WP-0022-T03
|
|
status: cancel
|
|
priority: high
|
|
state_hub_task_id: "01fa1a22-0f4a-4a36-a9b3-0f94d3c70be1"
|
|
```
|
|
|
|
Implement an idempotent ensure-application-profile operation keyed by
|
|
application plus verified issuer and subject. Seed only approved profile
|
|
defaults and preserve application ownership of application-specific data.
|
|
Verified email may suggest an explicit link to an existing local profile but
|
|
must never authorize an automatic takeover.
|
|
|
|
Done when repeated OIDC callbacks return the same profile and collisions
|
|
require explicit authenticated resolution.
|
|
|
|
Cancelled here because application profiles are application-owned. The
|
|
coulomb-social consumer implements this under CSOC-WP-0003 using its unique
|
|
`(issuer, subject)` Member binding; user-engine continues to supply `/me`.
|
|
|
|
## T04 - Carry assurance and step-up requirements
|
|
|
|
```task
|
|
id: USER-WP-0022-T04
|
|
status: cancel
|
|
priority: high
|
|
state_hub_task_id: "d3bec8c6-e8d8-46d5-8fa2-b4675ff1ade8"
|
|
```
|
|
|
|
Model the application's minimum assurance and per-user/profile step-up policy
|
|
as data consumed by KeyCape/application authorization. Do not let profile data
|
|
issue tokens or weaken client minimums. Add a reauthentication/step-up handoff
|
|
that returns to the intended action after fresh OIDC authentication.
|
|
|
|
Done when a regular profile can be password-only while a profile or protected
|
|
action can require MFA without changing global identity facts.
|
|
|
|
Cancelled here in favor of KEY-WP-0008 client policy and CSOC-WP-0003 action
|
|
step-up. user-engine must not become the token assurance authority.
|
|
|
|
## T05 - Registration and JIT security matrix
|
|
|
|
```task
|
|
id: USER-WP-0022-T05
|
|
status: progress
|
|
priority: high
|
|
state_hub_task_id: "0a5a5f3a-0d47-4d5d-bda5-e2c7c737fee6"
|
|
```
|
|
|
|
Cover enumeration resistance, duplicate username/email, verification
|
|
replay/expiry, provider outage and compensation, return-state tampering, JIT
|
|
replay, account-link collision, cross-tenant profile access, step-up downgrade,
|
|
unlink, and deletion. Verify audit/outbox redaction and correlation.
|
|
|
|
Done when the full suite and deployed consumer conformance both pass.
|
|
|
|
The user-engine portion now covers return-context rejection, provider outage
|
|
and idempotent recovery, resume replay denial, purpose/binding mismatch,
|
|
verification expiry/replay redaction, peer rate limiting, identity-link
|
|
collision, cross-tenant denial, lifecycle deletion, audit/outbox redaction,
|
|
and request correlation. Deployed consumer conformance remains gated on public
|
|
runtime credentials and activation.
|
|
|
|
Duplicate-input conformance now also proves that a verified mailbox reused
|
|
with a different username creates a distinct local user instead of implicitly
|
|
linking to an existing account. Reusing an existing provider username fails at
|
|
the identity-link uniqueness boundary, leaves the original link unchanged, and
|
|
never transfers that identity to the later registration. The full suite passes
|
|
131 tests with three environment-dependent skips.
|
|
|
|
2026-08-16 authorization evidence: flex-auth reports FLEX-WP-0009 finished and
|
|
handed back deployed-policy evidence for the user-engine surface, including the
|
|
three registration-applicant fixtures (applicant allow, membership deny, wrong
|
|
issuer deny) replayed against the live `POST /v1/check`. The fail-closed
|
|
`FlexAuthHTTPAdapter` maps a missing or refused service to deny. This removes
|
|
authorization from this task's gate; only the credential handoff remains.
|
|
|
|
The latest tested image is published at
|
|
`forgejo.coulomb.social/coulomb/user-engine@sha256:a49a0a105d392e374f6da958f8ad1bff36f2cfa8779ac5cafa479b7f34b6efc1`;
|
|
the matching email-connect image is
|
|
`forgejo.coulomb.social/coulomb/email-connect@sha256:bc7a01b4ab9acd2f12f3150c84e4b6590744c035bc15689c93584d8da06bd730`.
|
|
The cluster already applies a Traefik-wide rate limit, and the runtime manifest
|
|
now explicitly configures the stricter application registration limit. Public
|
|
registration remains disabled pending its OpenBao verification/delivery tokens
|
|
and transactional SMTP configuration.
|