ADR-00014: Claim Verification Provider Seam and eHerkenning Control-Proof
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-17 |
| CTN Technical Advisory Board | Pending | — |
| CTN Steering Committee | Pending | — |
| BDI Conformance Team | Pending | — |
| BDI Framework Team | Pending | — |
Context and Problem Statement
Section titled “Context and Problem Statement”An applicant must be able to prove the claims on an Onboarding Request — first that the company they are registering is real, and that they are authorised to act for it. The first authorised-representation mechanism is eHerkenning (the Dutch eID for businesses), surfaced as an authenticated step-up during onboarding. In the (hopefully not too distant future also European Business Wallets, eIDAS or known in NL as EDI-wallet).
This is the first instance of a seam that will recur: future modules will poll KvK, VIES, GLEIF, Handelsregister, the Belgian KBO, and comparable institutions in BE/DE/elsewhere. The output of each is the same in kind — validated claims coupled with an assurance level — even though the mechanisms differ wildly (an interactive browser redirect vs a server-to-server register lookup). The seam should be reusable across consortia: multiple NL dataspaces may use the same eHerkenning module with their own onboarding flows, which points at the marketplace layer (ADR-00009, ADR-00010).
Three prior decisions constrain this one:
- ADR-00003 — identity lives in Keycloak; participant business data, verification records live in ASR. No business data mirrored as Keycloak attributes.
- ADR-00006 (still open) — how participant context (including verified identifiers) is composed into tokens. Its prototype findings M16 (trust level missing from tokens) and M17 (identifier used as a mutable attribute key) are the cautionary tales here.
- ADR-00008 — Keycloak is the only IdP ASR talks to; eHerkenning federates behind Keycloak. ADR-00013 already provisions an applicant Keycloak account at intake.
Please note that for Connected Trade Network we provide eHerkenning as an optional proof. In many cases the user is not aware how to use eHerkenning. Usually it is with his accountant.
What eHerkenning actually returns (research, 2026-06-14)
Section titled “What eHerkenning actually returns (research, 2026-06-14)”A premise correction that shapes the model. eHerkenning (technically the Afsprakenstelsel Elektronische Toegangsdiensten) is SAML 2.0 only; a contracted Herkenningsmakelaar (broker) is mandatory — one cannot join the federation directly. A successful login asserts:
- LegalSubjectID / EntityConcernedID — the company, by KvK number (
urn:etoegang:1.9:EntityConcernedID:KvKnr), RSIN, or vestigingsnummer (a ServiceRestriction). Encrypted per recipient. - ActingSubjectID — the person, as a per-service-provider pseudonym (
urn:etoegang:core:ActingSubjectID), not a name. The real name/BSN is not delivered under normal representation, at any assurance level. - Representation + a mandate (machtiging): the Machtigingenregister asserts the acting person is authorised to act for that company for this service.
- Assurance level —
urn:etoegang:core:assurance-class:loa3(EH3 = eIDAS Substantial, the de-facto minimum) orloa4(EH4 = High). Assurance does not change whether a name is returned. - No company name in core eHerkenning (resolved out-of-band from KvK). A broker OIDC facade (a commercial eID broker) exposes a convenience
organisationclaim, but it is broker-derived, not an authoritative etoegang attribute.
So eHerkenning proves control/authority over an identifier, plus a baseline that the identifier is a live, registered entity at login time. It does not prove the entity’s attributes (name, address, VAT, EUID) — those still require register enrichment (already built as nl.ctn.asr.enrichment.*). For cross-border (BE/DE), foreign companies arrive through the same broker via the eIDAS koppelpunt and yield a foreign legal identifier + legal name but no KvK — an origin-dependent claim shape.
The central question
Section titled “The central question”How are these verified claims captured into ASR such that nobody can easily tamper with them, the mechanism is reusable across verification sources and consortia, and it respects the Keycloak/ASR ownership boundary?
Considered Options
Section titled “Considered Options”The contested, load-bearing choice is where the verified claims live and how they get there. (The seam shape and lifecycle choices follow from it and are recorded under Decision Outcome.)
Option 1: Store eHerkenning claims as Keycloak user attributes, read via userinfo
Section titled “Option 1: Store eHerkenning claims as Keycloak user attributes, read via userinfo”- Description: Persist KvK / assurance / mandate as attributes on the applicant’s Keycloak user; ASR reads them from the OIDC userinfo endpoint.
- Pros: No ASR persistence to build; claims “follow” the account.
- Cons: Violates ADR-00003 (business/verification data in Keycloak). Keycloak user attributes are admin-mutable plaintext — the exact tamper hole to avoid; Keycloak neither signs nor enforces them. Re-creates M16/M17. A company is not a property of a person’s account (one person may hold mandates for several companies). Userinfo returns current attribute state, not what was true at verification time. Rejected.
Option 2: Transient signed token → ASR verifies → ASR persists an immutable attestation (chosen)
Section titled “Option 2: Transient signed token → ASR verifies → ASR persists an immutable attestation (chosen)”- Description: Keycloak performs the (brokered) eHerkenning login and mints a short-lived token carrying the claims as transient transport (session-scoped, not durable attributes). ASR verifies signature/audience/freshness, extracts the claims, and writes a write-once attestation into its own store, with an evidence reference to the underlying assertion.
- Pros: Claims live in ASR (ADR-00003 single source of truth). Tamper-resistance: append-only attestation + audit, no admin-editable verification state. Transport token is one-time and signature-verified. Identity facts that are account-bound (pseudonym,
idp) legitimately stay in Keycloak. Achievable config-only in Keycloak on a broker OIDC facade (no plugin). - Cons: Requires ASR persistence + a Keycloak mapper chain. The attestation is point-in-time and can go stale (accepted — aging deferred).
Option 3: Keep/pass the original eHerkenning assertion as the primary store
Section titled “Option 3: Keep/pass the original eHerkenning assertion as the primary store”- Description: Persist and re-parse the raw signed SAML assertion as the record of truth.
- Pros: Cryptographically self-evident.
- Cons: Forces every consumer to parse SAML; brittle; couples ASR to one source’s wire format. Rejected as primary — but a reference/hash of the assertion is kept as evidence on the Option-2 attestation.
Option 4: Keep token minimal, ASR exposes a context endpoint fetched on demand
Section titled “Option 4: Keep token minimal, ASR exposes a context endpoint fetched on demand”- Description: ADR-00006 Option 4 style — no verification claims in the token; consumers call ASR when needed.
- Pros: Clean boundary; small tokens.
- Cons: Addresses serving claims to downstream consumers, not capturing a one-time interactive verification event. Orthogonal to this decision; left to ADR-00006.
Decision Outcome
Section titled “Decision Outcome”Chosen option: Option 2 — a transient signed token carrying eHerkenning claims into ASR, which verifies and persists an immutable attestation; Option 3 retained only as evidence. Built behind a reusable seam:
The seam
Section titled “The seam”- Claim Verification Provider — a marketplace SPI in core
org.bdinetwork.asr.spithat, given a subject (an Onboarding Request’s identifier + applicant), returns a Verified Claim Set: individually-sourced claims, each carrying its Validation Source and an assurance level, in a broker-neutral, eIDAS-aligned vocabulary (LOW/SUBSTANTIAL/HIGH). Not a fixed{kvk, name}record — the shape varies by source and by subject origin. - Two sub-interfaces over the shared output:
InteractiveClaimVerificationProvider(start()→ redirect,complete()→ Verified Claim Set; user present, multi-round capable) andBackendClaimVerificationProvider(verify()→ Verified Claim Set; no user). Forcing one signature onto both would leak one mode’s accident into the other. - Two attestation kinds, kept distinct: control (eHerkenning: “a mandate-holder controls KvK X”) vs existence/attributes (register enrichment: “KvK X is real and looks like this”). Control proof implies existence-at-login-time, but the two have independent revocation triggers, so they are separate attestations.
eHerkenning specifics
Section titled “eHerkenning specifics”- Keycloak-as-broker (ADR-00008). Broker independence is a requirement: ASR always consumes a broker-neutral claim set; the broker-specific mapping is confined to the Keycloak IdP-mapper layer. The transport vs implementation choice — Path A (broker OIDC facade, e.g. a commercial eID broker; config-only Keycloak) vs Path B (the implementation partner
Extended-SAML-IDPplugin; broker-agnostic) — is deferred to research ticket KEYC-1192. - The step-up is an account-linked, same-person login on the intake-provisioned account (ADR-00013): the applicant is already authenticated; eHerkenning is linked onto that current session via Keycloak’s Application-Initiated Action
idp_link(kc_action=idp_link:eherkenning-sim), which requires the requestor to hold theaccountrolemanage-account-links. The link is session-scoped and matched by the live session, not by email — faithful to eHerkenning, which returns no name and no email. The acting-subject pseudonym, representation flag and assurance are stored on the attestation (the authoritative “who proved the mandate”); same-person is a flow assumption, not a schema invariant, so delegation is later additive. - Persistence: extend the existing
verificationtable (type=EHerkenning,category=Automatic) with a nullableonboarding_request_id(no Participant exists until approval), a structuredclaimspayload, andassurance_level. The claim payload is write-once (re-verification = a new row); the verification status (Pending / Approved / Rejected / Revoked) is the mutable lifecycle layered on top; status history lives inaudit_record. - Behaviour: lives under Enrichment (the authenticated edit page) and does not auto-transition the request. On
complete(): the eHerkenning KvK is compared to the request’s identifier — match → positive control attestation; mismatch → failure recorded and flagged for admin attention (fraud/mistake guard). Offered NL-only for MVP. The admin assigns the trust level at approval with the attestation as evidence.
Simulated broker realm for the end-to-end demo (issue #45)
Section titled “Simulated broker realm for the end-to-end demo (issue #45)”- A second Keycloak realm,
eherkenning-sim(asr-service/src/dev-services/keycloak/eherkenning-sim-realm.json), is imported alongsidequarkus-realm.json(multi-valuerealm-path). It is a faithful OIDC stand-in for a commercial eID broker’s eHerkenning OIDC facade (Path A): a per-acting-person user (e.g. the seededardaacting-person login, no email) whose token emits eHerkenning-shaped claims (a KvK number, an acting-subject pseudonym, an assurance-class LoA, and a representation flag) via per-user attribute protocol mappers (oidc-usermodel-attribute-mapper, #62) — each fact is read from the logged-in acting person’s own user attributes rather than a realm-global hardcoded value, so different acting persons can prove different KvK numbers. - The application realm federates it declaratively as the
eherkenning-simOIDC broker IdP (identityProviders), withstoreToken=false. The interactive link is driven by the Application-Initiated Actionidp_link(kc_action=idp_link:eherkenning-sim), which links the brokered identity onto the requestor’s current session — not matched by email (eHerkenning returns none) and never a duplicate (ADR-00013). The transient-claim chain is IdP-mapper → user-session-note → protocol-mapper entirely in the realm JSON:oidc-user-session-note-idp-mappers carry each brokered claim into a session note (nooidc-user-attribute-idp-mapper, so nothing lands on a Keycloak user attribute on the requestor account — ADR-00003/00006), andoidc-usersessionmodel-note-mappers on the interactive-login client emit each note into the issued token. - Only the literal broker claim names are unverified guesses at a commercial eID broker’s contract (documented inline in both realm JSONs). All broker-claim-name mapping lives in the IdP-mapper layer (ADR-00014/00008): a real-broker swap is a configuration change confined to that layer, not a code change —
EHerkenningTokenVerifierreads the neutral token claim nameskvk/loa/acting_subject/represents, which default to these literals but are themselves devops-configurable (see Configurable neutral token claim contract below, #72).
Note (2026-06-15, spike — see PRD #61): the interactive link mechanism settled as Keycloak Application-Initiated Action
idp_link(kc_action=idp_link:eherkenning-sim), as the body above now describes. Two earlier candidates were rejected and are recorded here for history: a verified-emailfirst broker loginmatch (superseded — eHerkenning returns no email, so there is nothing to match on) and Client-Initiated Account Linking (/broker/{idp}/link, deprecated in KC26 and rejects the public client). The AIA links the brokered identity onto the current requestor session and mints a token carrying the claims; it requires the requestor to hold theaccountrolemanage-account-links. Brokered claims are session-scoped (they ride every token for the session lifetime, surviving refresh and repeat login undersyncMode=IMPORT); the protection is the audience-restricted verification + write-once attestation, not token scarcity. The sim emits no name. Path A/B and the production key source remain KEYC-1192.
Configurable neutral token claim contract (2026-06-17, #72)
Section titled “Configurable neutral token claim contract (2026-06-17, #72)”The four token claim names the ASR verifier reads — and the LoA value vocabulary it maps to assurance levels — are devops-configurable via application.properties, each defaulting to today’s literal so an unconfigured deployment is byte-for-byte unchanged.
- Claim names — under the existing
eherkenning.token.*family (sibling toexpected-audience,allowed-skew-seconds,public-key-location), exposed as a@ConfigMapping:eherkenning.token.claim.kvk(defaultkvk)eherkenning.token.claim.loa(defaultloa)eherkenning.token.claim.acting-subject(defaultacting_subject)eherkenning.token.claim.represents(defaultrepresents)EHerkenningTokenVerifierreads each field by its configured name, andTOKEN_CLAIM_NAMESis derived from the configured set, not a hardcoded literal set. [#73]
- LoA value vocabulary — the broker LoA value strings accepted for each eIDAS level, as lists:
eherkenning.token.loa.substantial(defaultloa3,EH3,urn:etoegang:core:assurance-class:loa3)eherkenning.token.loa.high(defaultloa4,EH4,urn:etoegang:core:assurance-class:loa4)- An unrecognised value still fails with the same rejection semantics as today. [#74]
- Transport:
application.propertiesonly — thequarkus-config-yamlextension is out of scope (maintainer call, 2026-06-17).
Boundary stance (relation to broker-neutrality). This preserves the broker-neutral principle, it does not relax it. Raw broker attribute names — etoegang SAML URNs (urn:etoegang:1.9:EntityConcernedID:KvKnr, …) or a a commercial eID broker OIDC-facade’s claim names — continue to be mapped to neutral claims only in the Keycloak IdP-mapper layer; no raw broker name enters ASR. What becomes configurable is the neutral token-claim contract: the key names (and LoA value vocabulary) the verifier expects in the already-verified, already-neutralised token. Previously both ends — the Keycloak protocol-mapper output and the ASR verifier input — independently hardcoded the same four literals, an implicit coupling. Externalising the ASR end lets that contract be re-aligned with a re-configured mapper output (or a different broker facade) without an ASR code change or redeploy — the same config-only swap goal the IdP-mapper layer already serves. The relaxation is narrow and deliberate: claim values are still sourced only from the verified token (M16/M17 and ADR-00003 unaffected); only the key names and LoA vocabulary the verifier reads under are externalised.
Rationale
Section titled “Rationale”- Business alignment: proves authorised representation — the strongest thing eHerkenning offers — while keeping the door open to reuse across consortia and across future sources.
- Technical: honours ADR-00003 (claims in ASR, not Keycloak), avoids M16/M17, and is achievable config-only on a broker OIDC facade. The control/existence split matches the genuinely different things eHerkenning and register lookups attest.
- Risk: the chief tamper risk (admin-editable verification state) is removed by keeping claims in an append-only ASR attestation; the transport token is one-time and verified.
Consequences
Section titled “Consequences”- Positive: single source of truth for verification (ASR); tamper-resistant append-only attestations; one reusable seam for all present and future verification sources; Keycloak stays stock on the broker-OIDC path; broker-swappable by config.
- Negative: attestations are point-in-time and can go stale — aging / revocation / re-verification is explicitly out of scope for now and must be designed before relying on attestation freshness. A Keycloak mapper chain (IdP-mapper → session note → protocol mapper) must be built and tested to keep claims transient (not persisted to user attributes).
- Neutral / deferred (tracked):
- Delegation / ketenmachtiging (
eherkenning_intermediate_*), BE/DE-via-eIDAS, and a numeric trust-level mapping are deferred. - Convergence debt: the existing
nl.ctn.asr.enrichment.*cluster is the non-interactive provider in embryo but writeslegal_entity_identifierdirectly rather than attestations. For MVP it is left as-is; folding it behindBackendClaimVerificationProvider(unifyingEnrichmentResultwithVerifiedClaimSet) is deferred. Two models coexist until then.
- Delegation / ketenmachtiging (
Implementation Plan
Section titled “Implementation Plan”- Phase 1 (MVP / demo): Define the SPI +
VerifiedClaimSetinorg.bdinetwork.asr.spi; implement the interactive eHerkenning provider inorg.bdinetwork.marketplace.asr.eherkenning; wire Keycloak → broker (a commercial eID broker OIDC for the demo) with the transient-claim mapper chain; extend theverificationtable; surface match/mismatch on the admin review screen. NL-only. - Phase 2: Resolve KEYC-1192 (Path A vs Path B; broker-neutral claim contract across HMs). Design attestation aging / re-verification / revocation.
- Phase 3: Fold
nl.ctn.asr.enrichment.*behindBackendClaimVerificationProvider(convergence). Add delegation / ketenmachtiging and BE/DE-via-eIDAS. Align with ADR-00006.
Compliance and Validation
Section titled “Compliance and Validation”- Security review completed (focus: transient-token verification, no claims in Keycloak attributes, mismatch/fraud handling, append-only attestation integrity)
- Performance impact assessed
- Integration impact evaluated (Keycloak mapper chain, broker dependency)
- Documentation updated (CONTEXT.md glossary entries added)
- Stakeholder approval obtained
Related Documents
Section titled “Related Documents”- 00003-separate-users-and-participants.md — ownership boundary
- 00006-token-claim-composition.md — token claims, M16/M17 (advances)
- 00008-external-idp-federation-strategy.md — Keycloak-as-broker (refines)
- 00009-layered-source-code-structure.md, 00010-evolving-to-quarkus-extensions.md — SPI / marketplace layering
- 00011-uri-based-legal-entity-identifier-scheme.md — Identifier URI matched against
- 00012-onboarding-request-lifecycle-transition-seam.md — Enrichment vs lifecycle verbs
- 00013-applicant-keycloak-account-provisioned-at-intake.md — the account linked at step-up
- ASR Data Model —
verification,audit_record - Identifier Enrichment Architecture — the non-interactive providers
- CONTEXT.md — Claim Verification Provider, Verified Claim Set, Attestation
- the implementation partner Jira KEYC-1192 — broker-independent eHerkenning integration (Path A vs B)
Status History
Section titled “Status History”| Date | Status | Notes |
|---|---|---|
| 2026-06-14 | Proposed | Initial proposal from grilling session |
| 2026-06-15 | Proposed | Interactive link mechanism settled as AIA idp_link (CIAL deprecated in KC26; email-match superseded), spike-verified; see PRD #61 |
| 2026-06-17 | Proposed | Neutral token-claim contract (the four claim names + LoA value vocabulary) made devops-configurable via application.properties, defaults preserve behaviour; broker-neutrality preserved (raw names stay in the IdP-mapper layer). See #72 / #73 / #74 |
In samenwerking met




