You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Credential selection when multiple credential from wallet match #4067
A new optional credential_selection parameter on the request-service-access-token endpoint. The EHR provides simple key-value pairs mapping PD field IDs to expected values:
The PD author controls what is selectable by assigning id values to constraint fields (already done for introspection claims). The EHR only needs to know the field name and value — no JSON paths, no query language.
See the full PRD for design details, trade-offs, and architecture.
I was under the assumption DCQL could be used, reading section 6.3, you can provide a claims query which consists of a path of the claim and possible values.
From appendix D, this example shows selecting a person with last name "Doe" living in either 90210 or 90211.
When a wallet contains multiple credentials of the same type (e.g., multiple PatientEnrollmentCredentials for different patients, or multiple HealthcareProviderTypeCredentials for different organization types), the Nuts node has no mechanism for the EHR to specify which credential to select. The current Presentation Definition (PD) matching picks the first match, which is non-deterministic when multiple credentials of the same type exist.
Solution
Add a credential_selection parameter to the request-service-access-token endpoint (auth v2) that allows the EHR to specify which credential to use via simple key-value pairs. Each key maps to a field id already declared in the Presentation Definition's input descriptor constraints. The value narrows the match to credentials where that field equals the given value.
A configurable CredentialSelector is injected into the PD matcher's matchConstraints function. When credential_selection is provided, a parameter-based selector filters the candidates by matching field values. When not provided, the default selector (first match) preserves current behavior. The Match public API and return types remain unchanged — one credential per input descriptor.
The selection must narrow to exactly one credential per input descriptor. Zero matches → error (soft failure if submission requirements allow it, e.g. min: 0). Multiple matches → error. The EHR must provide values specific enough to select one credential.
How It Works
PDs already declare constraint fields with optional id properties. These IDs are used for extracting values into the access token via ResolveConstraintsFields, which builds a flat map[string]any (field ID → value). This flat map is stored in AccessToken.InputDescriptorConstraintIdMap and returned as top-level claims in the introspection response. Field IDs are already validated to be globally unique across all PDs in the WalletOwnerMapping — duplicates cause a ServerError (see resolveInputDescriptorValues in s2s_vptoken.go).
The credential_selection parameter reuses these field IDs. When the EHR provides { "patient_id": "123456789" }, the selector finds all candidates matching the input descriptor's constraints, then further narrows to those where the patient_id field equals "123456789".
The PD author controls what is selectable by assigning id values to constraint fields. The EHR only needs to know the field name and the value — no JSON paths, no query language syntax.
User Stories
As an EHR developer, I want to specify which patient's PatientEnrollmentCredential to present, so that the correct patient context is established in the access token request.
As an EHR developer, I want to specify which HealthcareProviderTypeCredential to present, so that the correct organization type is used when my organization has multiple classifications.
As an EHR developer, I want to only provide credential_selection for ambiguous credential types, so that I don't need to specify selection criteria for credentials that are already unambiguous.
As an EHR developer, I want a clear error when my selection criteria match zero credentials in the wallet, so that I can diagnose misconfiguration or missing credentials.
As an EHR developer, I want a clear error when my selection criteria still match multiple credentials, so that I know my values are not specific enough.
As an EHR developer, I want a simple key-value interface for credential selection, so that I don't need to learn a query language or know internal credential JSON paths.
As an EHR developer, I want to continue providing self-attested credentials via the existing credentials array alongside credential_selection, so that both mechanisms work together.
As an EHR developer, I want the existing behavior (PD picks first match) to be preserved for input descriptors where I don't provide selection criteria, so that this change is backward compatible.
As a Nuts node operator, I want credential selection errors to be descriptive, so that I can help EHR vendors troubleshoot integration issues.
Implementation Decisions
API Design
The ServiceAccessTokenRequest gains one new optional field:
credential_selection: An object mapping PD field ids to expected values. Each key MUST match a field id declared in a PD input descriptor's constraints. Field IDs are already guaranteed unique across PDs (enforced by resolveInputDescriptorValues duplicate detection).
A CredentialSelector strategy is injected into the PD matcher's matchConstraints function. This is the same injection point used by the DCQL selector (#4089). See approach comment.
matchConstraints collects all matching VCs per input descriptor (removes the break on first match), then calls the CredentialSelector to pick one. The Match return type and Candidate struct remain unchanged.
Default selector: picks the first match (preserves current behavior)
Selection selector: for each candidate, resolves the PD fields referenced by credential_selection keys, keeps only candidates where all field values match the given values, errors on zero or multiple remaining candidates
The selection selector follows the same factory pattern as NewDCQLSelector in vcr/pe/selector.go (branch feature/4089-dcql-selector): validate selection keys against PD field IDs at construction time, return a CredentialSelector closure. The wiring through buildSubmission in presenter.go (branch feature/4090-api-credential-query) is also directly reusable — replace credentialQueries []dcql.CredentialQuery with credentialSelection map[string]string.
Error handling in matchConstraints:
ErrNoCredentials from the selector is treated as a soft failure (nil selection), allowing submission requirements (e.g., pick rules with min: 0) to evaluate whether zero fulfilled descriptors is acceptable.
ErrMultipleCredentials and other errors are hard failures.
Trade-offs
This approach is simpler but more constrained than a full query language:
credential_selection (chosen)
credential_query (DCQL, superseded)
Caller complexity
Key-value pairs, no paths needed
Must know JSON paths and DCQL syntax
Query expressiveness
Equality only, pre-declared fields
Arbitrary paths, OR semantics
PD author effort
Add id to selectable fields (often already done for introspection)
None (queries are self-contained)
Error surface
Wrong key name
Wrong path, wrong ID, wrong structure
Why the simpler approach is sufficient:
Real-world use cases are equality matches on known fields (which patient, which organization type).
The PD already declares relevant fields with ids for access token value extraction — reusing these avoids duplication.
Field IDs are already guaranteed unique across PDs and surfaced as flat claims in introspection — the namespace is consistent.
If a richer query language is needed in the future, it can be added as a separate parameter without breaking credential_selection.
Selection CredentialSelector (new issue, reuses code from feature/4089-dcql-selector): Implementation that resolves PD field ids against candidates and filters by the credential_selection values. Same factory pattern as NewDCQLSelector — validate keys at construction, return selector closure. Errors on zero or multiple matches.
API layer + OpenAPI spec (Add credential_selection to access token request API #4090, modify auth/api/iam, reuses wiring from feature/4090-api-credential-query): Add credential_selection to ServiceAccessTokenRequest, propagate through RequestRFC021AccessToken → BuildSubmission → presenter.buildSubmission → builder.SetCredentialSelector. Same call chain as the DCQL approach, with map[string]string instead of []dcql.CredentialQuery.
Selection CredentialSelector (new): Adapt test structure from feature/4089-dcql-selector branch. Unit tests for: single match success, zero matches error (ErrNoCredentials), multiple matches error (ErrMultipleCredentials), selection key not matching any PD field ID errors, multiple selection keys (AND semantics), fallback to default selector for descriptors without matching selection keys, interaction with submission requirements.
Replacing PD with DCQL entirely: Longer-term goal. DCQL currently lacks some PD features we depend on, notably regex-based value extraction for token introspection.
Verifier-side changes: The verifier's PD and token validation are unchanged. credential_selection is purely a requestor-side wallet selection mechanism.
Complex query support: Range queries, OR semantics, pattern matching on selection values. Can be added later if needed.
Migration of existing PD configurations: Existing PDs continue to work unchanged.
Does the PEX specification allow for multiple results? If not, does this make our PEX broken? Is this a problem if we always perform the post filtering with the default filter (latest)? Perhaps provide a param to the PEX selector with either a set of post filters or a boolean to return all results or only one.
Updated approach: inject selection strategy into PD matcher instead of returning multiple candidates
After reviewing the PEX specification, we confirmed that the descriptor_map in a Presentation Submission maps one credential per Input Descriptor. While the spec doesn't explicitly prohibit multiple matches, the entire submission model is designed around a 1:1 relationship.
Returning multiple candidates from the matcher would change its return type and potentially break callers. Instead, we inject a credential selection strategy into the existing matchConstraints function:
matchConstraints collects all matching VCs per input descriptor (removes the break on first match)
A CredentialSelector function picks one credential from the candidates
The Candidate struct and Match return type remain unchanged — one VC per input descriptor
The default CredentialSelector picks the first match (preserving current behavior). When a credential_query is provided, a DCQL-based selector filters candidates and applies the match_policy.
This keeps the Match public API stable, avoids a separate filter chain layer, and puts the selection logic where it belongs — inside the matcher that already knows about input descriptors and candidates.
Decision: drop match_policy, DCQL filter must narrow to exactly one credential
After reviewing the DCQL multiple field and our PEX submission constraints, we've decided to drop the match_policy parameter from the API.
The PEX specification requires one credential per Input Descriptor in the descriptor_map. This means regardless of how many credentials the DCQL filter matches, we can only submit one. The match_policy options we considered ("error", "latest", "all") are either the only safe choice or incompatible with PEX:
"error" — the only deterministic option. The EHR must write a query specific enough to match exactly one credential.
"latest" — non-deterministic, depends on wallet state rather than explicit selection criteria. If the EHR cares which credential to use, they should be specific in their query.
"all" — incompatible with PEX 1:1 submission model.
New behavior: the DCQL credential_query must narrow to exactly one credential per input descriptor. Zero matches → error. Multiple matches → error. No configuration needed — the EHR writes a precise query.
This also means we don't need the DCQL multiple field, since our PEX submission is always 1:1.
Activity
stevenvegt commentedon Mar 20, 2026
I was under the assumption DCQL could be used, reading section 6.3, you can provide a claims query which consists of a path of the claim and possible values.
From appendix D, this example shows selecting a person with last name "Doe" living in either 90210 or 90211.
reinkrul commentedon Mar 20, 2026
Nice! I think I didn't read it properly.
stevenvegt commentedon Mar 20, 2026
PRD: Credential Selection via Named Parameters
Problem Statement
When a wallet contains multiple credentials of the same type (e.g., multiple PatientEnrollmentCredentials for different patients, or multiple HealthcareProviderTypeCredentials for different organization types), the Nuts node has no mechanism for the EHR to specify which credential to select. The current Presentation Definition (PD) matching picks the first match, which is non-deterministic when multiple credentials of the same type exist.
Solution
Add a
credential_selectionparameter to therequest-service-access-tokenendpoint (auth v2) that allows the EHR to specify which credential to use via simple key-value pairs. Each key maps to a fieldidalready declared in the Presentation Definition's input descriptor constraints. The value narrows the match to credentials where that field equals the given value.A configurable
CredentialSelectoris injected into the PD matcher'smatchConstraintsfunction. Whencredential_selectionis provided, a parameter-based selector filters the candidates by matching field values. When not provided, the default selector (first match) preserves current behavior. TheMatchpublic API and return types remain unchanged — one credential per input descriptor.The selection must narrow to exactly one credential per input descriptor. Zero matches → error (soft failure if submission requirements allow it, e.g.
min: 0). Multiple matches → error. The EHR must provide values specific enough to select one credential.How It Works
PDs already declare constraint fields with optional
idproperties. These IDs are used for extracting values into the access token viaResolveConstraintsFields, which builds a flatmap[string]any(field ID → value). This flat map is stored inAccessToken.InputDescriptorConstraintIdMapand returned as top-level claims in the introspection response. Field IDs are already validated to be globally unique across all PDs in the WalletOwnerMapping — duplicates cause aServerError(seeresolveInputDescriptorValuesins2s_vptoken.go).Example PD field:
{ "id": "patient_id", "path": ["$.credentialSubject.hasEnrollment.patient.identifier.value"], "filter": { "type": "string" } }The
credential_selectionparameter reuses these field IDs. When the EHR provides{ "patient_id": "123456789" }, the selector finds all candidates matching the input descriptor's constraints, then further narrows to those where thepatient_idfield equals"123456789".The PD author controls what is selectable by assigning
idvalues to constraint fields. The EHR only needs to know the field name and the value — no JSON paths, no query language syntax.User Stories
credential_selectionfor ambiguous credential types, so that I don't need to specify selection criteria for credentials that are already unambiguous.credentialsarray alongsidecredential_selection, so that both mechanisms work together.Implementation Decisions
API Design
The
ServiceAccessTokenRequestgains one new optional field:credential_selection: An object mapping PD fieldids to expected values. Each key MUST match a fieldiddeclared in a PD input descriptor's constraints. Field IDs are already guaranteed unique across PDs (enforced byresolveInputDescriptorValuesduplicate detection).Example request:
{ "scope": "medicatieoverdracht-gf", "authorization_server": "https://verifier.example.com", "credential_selection": { "patient_id": "123456789" } }Compare to the previous DCQL approach (now superseded):
{ "credential_query": [ { "id": "id_patient_enrollment", "claims": [ { "path": ["credentialSubject", "hasEnrollment", "patient", "identifier", "value"], "values": ["123456789"] } ] } ] }CredentialSelector Architecture
A
CredentialSelectorstrategy is injected into the PD matcher'smatchConstraintsfunction. This is the same injection point used by the DCQL selector (#4089). See approach comment.matchConstraintscollects all matching VCs per input descriptor (removes thebreakon first match), then calls theCredentialSelectorto pick one. TheMatchreturn type andCandidatestruct remain unchanged.credential_selectionkeys, keeps only candidates where all field values match the given values, errors on zero or multiple remaining candidatesThe selection selector follows the same factory pattern as
NewDCQLSelectorinvcr/pe/selector.go(branchfeature/4089-dcql-selector): validate selection keys against PD field IDs at construction time, return aCredentialSelectorclosure. The wiring throughbuildSubmissioninpresenter.go(branchfeature/4090-api-credential-query) is also directly reusable — replacecredentialQueries []dcql.CredentialQuerywithcredentialSelection map[string]string.Error handling in
matchConstraints:ErrNoCredentialsfrom the selector is treated as a soft failure (nil selection), allowing submission requirements (e.g.,pickrules withmin: 0) to evaluate whether zero fulfilled descriptors is acceptable.ErrMultipleCredentialsand other errors are hard failures.Trade-offs
This approach is simpler but more constrained than a full query language:
idto selectable fields (often already done for introspection)Why the simpler approach is sufficient:
ids for access token value extraction — reusing these avoids duplication.credential_selection.Modules
vcr/pe): InjectCredentialSelectorintomatchConstraints. Collect all candidates per input descriptor, then call selector. Default selector picks first match. Soft-fail forErrNoCredentials. Status: PR #4088: CredentialSelector extension point in PD matcher #4098 open.feature/4089-dcql-selector): Implementation that resolves PD fieldids against candidates and filters by thecredential_selectionvalues. Same factory pattern asNewDCQLSelector— validate keys at construction, return selector closure. Errors on zero or multiple matches.auth/api/iam, reuses wiring fromfeature/4090-api-credential-query): Addcredential_selectiontoServiceAccessTokenRequest, propagate throughRequestRFC021AccessToken→BuildSubmission→presenter.buildSubmission→builder.SetCredentialSelector. Same call chain as the DCQL approach, withmap[string]stringinstead of[]dcql.CredentialQuery.Superseded modules:
Credential filter chain with DCQL and default selector #4089 — DCQL CredentialSelector(replaced by selection selector; code infeature/4089-dcql-selectorbranch reusable for structure/tests)Modules kept open for future consideration:
Reusable Code
Branches
feature/4089-dcql-selectorandfeature/4090-api-credential-querycontain implementation that maps closely to the new approach:4089vcr/pe/selector.goCredentialSelectorclosure. Replacedcql.Matchwith field-value matching.4089vcr/pe/selector_test.go4090vcr/holder/presenter.gobuildSubmissionwiring: create selector if selection provided, callbuilder.SetCredentialSelector. Replace[]dcql.CredentialQueryparam withmap[string]string.4090vcr/holder/interface.goBuildSubmissionsignature change. ReplacecredentialQueriesparam withcredentialSelection.4090auth/api/iam/api.go4090docs/_static/auth/v2.yamlcredential_queryschema withcredential_selectionobject.Testing Decisions
ErrNoCredentialswithmin: 0submission requirements. Existing PD matching tests pass unchanged. Done in PR #4088: CredentialSelector extension point in PD matcher #4098.feature/4089-dcql-selectorbranch. Unit tests for: single match success, zero matches error (ErrNoCredentials), multiple matches error (ErrMultipleCredentials), selection key not matching any PD field ID errors, multiple selection keys (AND semantics), fallback to default selector for descriptors without matching selection keys, interaction with submission requirements.credential_selection. Adapt fromfeature/4090-api-credential-querye2e test.Out of Scope
credential_selectionis purely a requestor-side wallet selection mechanism.Sub-issues
credential_selectionto access token request API (scope updated fromcredential_query)Dependency graph
Changelog
credential_query(DCQL) approach withcredential_selection(named parameters). Motivation: user feedback indicated DCQL queries are complex to implement for EHR developers and the actual use cases are simple equality matches on known fields. The named parameter approach reuses existing PD fieldids (already guaranteed unique and used for introspection claims), is dramatically simpler for callers, and sufficient for current requirements. DCQL parser (DCQL credential query parser with claims/values matching #4087) kept open for potential future use. DCQL selector (Credential filter chain with DCQL and default selector #4089) superseded; code in local branch reusable for structure. Added soft-fail semantics forErrNoCredentialsinmatchConstraints(discovered during PR #4088: CredentialSelector extension point in PD matcher #4098 review). AddedErrNoCredentialssoft-fail handling and test in PR #4088: CredentialSelector extension point in PD matcher #4098. See original DCQL-based PRD in edit history for the previous approach.stevenvegt commentedon Mar 23, 2026
Does the PEX specification allow for multiple results? If not, does this make our PEX broken? Is this a problem if we always perform the post filtering with the default filter (latest)? Perhaps provide a param to the PEX selector with either a set of post filters or a boolean to return all results or only one.
stevenvegt commentedon Mar 24, 2026
Updated approach: inject selection strategy into PD matcher instead of returning multiple candidates
After reviewing the PEX specification, we confirmed that the
descriptor_mapin a Presentation Submission maps one credential per Input Descriptor. While the spec doesn't explicitly prohibit multiple matches, the entire submission model is designed around a 1:1 relationship.Returning multiple candidates from the matcher would change its return type and potentially break callers. Instead, we inject a credential selection strategy into the existing
matchConstraintsfunction:matchConstraintscollects all matching VCs per input descriptor (removes thebreakon first match)CredentialSelectorfunction picks one credential from the candidatesCandidatestruct andMatchreturn type remain unchanged — one VC per input descriptorThe default
CredentialSelectorpicks the first match (preserving current behavior). When acredential_queryis provided, a DCQL-based selector filters candidates and applies thematch_policy.This keeps the
Matchpublic API stable, avoids a separate filter chain layer, and puts the selection logic where it belongs — inside the matcher that already knows about input descriptors and candidates.stevenvegt commentedon Mar 24, 2026
Decision: drop
match_policy, DCQL filter must narrow to exactly one credentialAfter reviewing the DCQL
multiplefield and our PEX submission constraints, we've decided to drop thematch_policyparameter from the API.The PEX specification requires one credential per Input Descriptor in the
descriptor_map. This means regardless of how many credentials the DCQL filter matches, we can only submit one. Thematch_policyoptions we considered ("error","latest","all") are either the only safe choice or incompatible with PEX:"error"— the only deterministic option. The EHR must write a query specific enough to match exactly one credential."latest"— non-deterministic, depends on wallet state rather than explicit selection criteria. If the EHR cares which credential to use, they should be specific in their query."all"— incompatible with PEX 1:1 submission model.New behavior: the DCQL
credential_querymust narrow to exactly one credential per input descriptor. Zero matches → error. Multiple matches → error. No configuration needed — the EHR writes a precise query.This also means we don't need the DCQL
multiplefield, since our PEX submission is always 1:1.#4067: Credential selection when multiple credential from wallet match (