ADR-00013: Applicant Keycloak account provisioned at intake
Approval Status
Section titled “Approval Status”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 body | Status | Last update |
|---|---|---|
| CTN Project Team | Proposed | 2026-06-15 |
| CTN Technical Advisory Board | Pending | — |
| CTN Steering Committee | Pending | — |
| BDI Conformance Team | Pending | — |
| BDI Framework Team | Pending | — |
RdN Question: I have two cases in my mind, for which I am not certain that the proposed option will work correctly.
- 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.
- 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.
Context and Problem Statement
Section titled “Context and Problem Statement”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.
Considered Options
Section titled “Considered Options”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
subinapplicantUserId, 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,
applicantUserIdnull. 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.
Decision Outcome
Section titled “Decision Outcome”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.
Rationale
Section titled “Rationale”- 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 thesubor 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
subif 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.
Consequences
Section titled “Consequences”- 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
applicantUserIdthat 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.
Implementation Plan
Section titled “Implementation Plan”- Phase 1 — provisioning: asr-service “ensure KC account” step on intake (reuse-or-create by email; generated username;
loginWithEmailAllowed; storesub); trigger set-password email on create only; grant therequestorrealm role to the ensured account on both paths (idempotent top-up — see Requestor role grant below). - 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.
- Phase 3 — applicant edit page: login-protected; editable fields
legalName, applicantName, addressLine1, postalCode, city, countryCode; read-onlyidentifierUri, applicantEmail, intakeCountry, status; editable inSubmittedandChangesRequested; ends a round withresubmit. - 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.
Requestor role grant (issue #47)
Section titled “Requestor role grant (issue #47)”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.
Compliance and Validation
Section titled “Compliance and Validation”- 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
Related Documents
Section titled “Related Documents”- 00003-separate-users-and-participants.md — Keycloak owns identity; the User-Identity-First ordering this ADR inverts for the onboarding flow
- 00007-identity-mutation-proxy.md — the path by which the applicant’s email (login alias) is later mutated without changing the account
- 00012-onboarding-request-lifecycle-transition-seam.md — the lifecycle seam that deferred this decision and assumes the Actor this ADR provisions
- Anchor issue #33 — driving requirement + acceptance criteria (implementation issues link here)
docs/ai/CONTEXT.md— Onboarding / Actors terms
Status History
Section titled “Status History”| Date | Status | Notes |
|---|---|---|
| 2026-06-14 | Proposed | Initial proposal |
| 2026-06-15 | Proposed | Folded 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




