Skip to content
Deze documentatie wordt actief uitgebreid — kom regelmatig terug.

ADR-00013: Applicant Keycloak account provisioned at intake

Status flow: Proposed → Accepted → Reviewed → Approved (or Rejected), or Not Required. A superseded ADR keeps its file; only its status changes to Superseded by NNNNN.

Governance bodyStatusLast update
CTN Project TeamProposed2026-06-15
CTN Technical Advisory BoardPending
CTN Steering CommitteePending
BDI Conformance TeamPending
BDI Framework TeamPending

RdN Question: I have two cases in my mind, for which I am not certain that the proposed option will work correctly.

  1. Creating accounts with fake email accounts. In other projects I stumble every time across this issue. Bots which create accounts with fake email addresses. Email addresses which never are verified. In those projects I have chosen the route only to create IdP account after the email has been verified. In order not to pollute your IdP with fake accounts, you have to cleanup afterwards. Not sure if this is feasible with Keycloak and/or proposed option below.
  2. How to deal with duplicates. We certainly come across the situation that for instance a DHL user will try to register himself with ASR (again). He is customer with multiple Inland Terminals. That there is now suddenly an Inlandbase Association which supersedes the Inland Terminals will be hard to digest, and certainly be forgotten by some in the long run.

The onboarding request workflow needs to be demonstrable end-to-end: an applicant submits an anonymous Intake, an admin reviews and decides, and — when the admin sends the request back — the applicant logs in to an authenticated edit page to enrich/correct the request and resubmit it.

That authenticated edit page is the problem. ADR-00012 (the lifecycle seam) assumes the requestor already holds an authenticated Keycloak identity (Actor = sub) by the time they resubmit, and explicitly deferred the question of how that identity comes to exist: “Provisioning the requestor’s KC account at intake — a shift from today’s anonymous intake — … a separate identity decision to be made when the edit page is built. Not decided here.” The edit page is now being built, so the decision is due.

Two settled constraints frame it:

  • Intake stays anonymous and low-effort (glossary Intake): no login, no registry calls, syntax + local duplicate check only. Adding a credentialing step to the intake form would erode this.
  • ADR-00003 separates users (Keycloak) from participants (ASR) and resolves the chicken-and-egg with User-Identity-First: a person registers in Keycloak, then onboards. Anonymous intake already departs from that ordering; this ADR must say how identity is reconciled for the onboarding flow without reintroducing the prototype’s anti-patterns (business data in Keycloak, read-only-profile dead-ends).

OnboardingRequest.applicantUserId (a nullable Keycloak sub) already exists on the entity; what is undecided is when and how it is populated.

Option 1: Silent mint at intake + set-password email (chosen)

Section titled “Option 1: Silent mint at intake + set-password email (chosen)”
  • Description: On successful intake, asr-service ensures a Keycloak account for the applicant via the KC Admin API — reuse-or-create keyed by email — stores its sub in applicantUserId, and (only when it created the account) triggers Keycloak’s set-password / reset-credentials email. The intake form gains no password or username field. The submission response tells the applicant to expect the email and includes a link to the edit page for this request.
  • Pros: Intake stays minimal and anonymous-feeling (no credentialing step on the form); the request↔account link (sub) exists from submission, so every later requestor verb has a real Actor exactly as ADR-00012 assumes; account creation is a single-writer server-side side-effect (asr-service → KC Admin API), consistent with ADR-00007’s mutation-proxy direction.
  • Cons: Inverts ADR-00003’s User-Identity-First ordering for this flow (identity becomes a side-effect of intake); intake now has an external dependency (KC Admin API) and an email side-effect, so a KC outage degrades intake.

Option 2: Self-service registration at intake

Section titled “Option 2: Self-service registration at intake”
  • Description: The intake form itself collects a password (or runs Keycloak’s registration), so the applicant leaves intake already credentialed.
  • Pros: Simplest credential flow; no later “ensure account” reconciliation.
  • Cons: Makes intake heavier and no longer anonymous/low-effort — a direct conflict with the settled Intake design; pushes a security step into the public, unauthenticated entry point.

Option 3: Deferred — mint on first edit access

Section titled “Option 3: Deferred — mint on first edit access”
  • Description: Intake stays fully anonymous, applicantUserId null. The account is created later, when the admin sends the request back and the applicant follows a magic link that triggers Keycloak registration.
  • Pros: Keeps intake perfectly pure; no KC dependency at submission.
  • Cons: The request has no Actor until first edit, complicating the ownership guard and audit story ADR-00012 relies on; the magic-link-to-KC binding is the genuinely hard part and is merely postponed, not avoided.

Chosen option: Option 1 — silent reuse-or-create at intake, set-password email on create. This is a deliberately simple first version to make the workflow demonstrable.

  • It satisfies the demo’s gating requirement (a login-protected applicant edit page) while preserving the minimal-intake invariant, because credentialing happens out-of-band via Keycloak’s own email rather than on the intake form.
  • The stable request↔account link is the Keycloak sub, never the email. Email is configured as a Keycloak login alias (loginWithEmailAllowed), and the Keycloak username is system-generated (internal, stable). The applicant logs in with their email; if the email later changes it is updated via the identity-mutation-proxy (ADR-00007) without touching the sub or the username — so “email changeable without changing the account” holds by construction.
  • Reuse-or-create handles the returning applicant (same person, second company / different identifier): provisioning looks the user up by email, reuses the existing sub if present (and sends no second reset email), otherwise creates and triggers the email. This avoids a Keycloak duplicate-email collision that “always create” would hit.
  • The ordering inversion vs ADR-00003 is accepted for the onboarding flow only and recorded here so a future reader does not “fix” it back to identity-first.
  • Positive: Edit page is unblocked; every requestor lifecycle verb has a non-null Actor from submission onward; no business data is written to Keycloak (only identity), so the prototype anti-patterns are avoided.
  • Negative: Intake gains a synchronous dependency on the KC Admin API and an email side-effect; a Keycloak failure degrades intake (mitigation/atomicity is an implementation concern — provisioning failure must not silently produce a request with a null applicantUserId that can never be edited).
  • Neutral: The intake action itself remains actor-less — the Actor glossary entry is unchanged; the account is provisioned as a result of an anonymous action, the anonymous action does not acquire an Actor.
  1. Phase 1 — provisioning: asr-service “ensure KC account” step on intake (reuse-or-create by email; generated username; loginWithEmailAllowed; store sub); trigger set-password email on create only; grant the requestor realm role to the ensured account on both paths (idempotent top-up — see Requestor role grant below).
  2. Phase 2 — submission response: change the intake response to instruct the applicant to expect the email and link to the edit page for the request.
  3. Phase 3 — applicant edit page: login-protected; editable fields legalName, applicantName, addressLine1, postalCode, city, countryCode; read-only identifierUri, applicantEmail, intakeCountry, status; editable in Submitted and ChangesRequested; ends a round with resubmit.
  4. Phase 4 — admin review screen: read all fields + drive startReview/approve/reject/sendBack; disabled placeholder controls for the future admin-side verification (trust-level assignment, DE manual-proof acceptance), no logic.

The resubmit enrichment guard keeps the ADR-00012 OnboardingEnrichmentPolicy SPI seam, with a single permissive default registered for the demo (no completeness gate); per-country enrichment gating remains future work.

ADR-00012 gates the owner-scoped requestor endpoints with @RolesAllowed("requestor"). The provisioner is therefore the producer that must satisfy that gate: ensureAccount explicitly grants the requestor realm role (Roles.REQUESTOR) to the account via the admin client, on both the create and reuse paths. The grant is idempotent (Keycloak’s role-mapping add is a no-op when present), so the reuse path tops up unconditionally rather than assuming a prior grant — a returning or externally-created account is never left without the role. A grant failure propagates (intake fails loudly); a dangling KC account from a failed grant self-heals on retry via the reuse-path top-up.

This replaces a dev-only stopgap where requestor sat in the realm’s default-roles-quarkus composite, so any user created without explicit roles (and the service accounts) silently inherited it. That composite membership is removed in the same change, so dev mirrors production: only provisioned applicants carry requestor.

Production contract (no production realm IaC exists in this repo yet — not near prod; recorded here as the deployment requirement): the production Keycloak realm must define a non-composite requestor realm role, and the asr-service admin client’s service account must hold view-realm (to read the role representation) and manage-users (to add the role mapping). The grant code is realm-neutral; only these two facts must hold wherever it runs.

  • Security review completed (KC Admin API scope, reuse-or-create, no business data in KC)
  • Performance impact assessed (intake → KC Admin API synchronous call)
  • Integration impact evaluated (intake degraded on KC outage; provisioning atomicity)
  • Documentation updated (CONTEXT.md Intake / provisioning terms)
  • Stakeholder approval obtained
DateStatusNotes
2026-06-14ProposedInitial proposal
2026-06-15ProposedFolded in #47: provisioner grants requestor explicitly (both paths, idempotent top-up), retiring the default-roles-quarkus stopgap; production realm/scope contract recorded

In samenwerking met

Connected Trade NetworkConclusionData in LogisticsContargoInland Terminals GroupVan Berkel