#4090: Wire credential_selection through API to presenter#4122
#4090: Wire credential_selection through API to presenter#4122stevenvegt wants to merge 4 commits intofeature/4120-selection-selectorfrom
Conversation
Adds a credential_query test to the RFC021 e2e flow: - Issues two NutsOrganizationCredentials with different org names - Uses credential_query to select "Second Org B.V." by name - Verifies extended introspection contains the selected organization Test correctly fails: credential_query is currently ignored, so the default PD matcher picks "Caresoft B.V." instead of "Second Org B.V." Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- OpenAPI spec: add credential_query to ServiceAccessTokenRequest - Regenerate types and mocks - API handler: extract credential_query, convert to dcql.CredentialQuery - Client.RequestRFC021AccessToken: accept credentialQueries parameter - Wallet.BuildSubmission: accept credentialQueries parameter - Presenter: create DCQL selector when credentialQueries is non-empty - All existing tests pass with nil for new parameter Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace credential_query (DCQL) with credential_selection (named parameters) throughout the callstack. The API accepts a simple map[string]string mapping PD field IDs to expected values, which is propagated through the IAM client, wallet, and presenter to NewSelectionSelector. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
0 new issues
|
|
Coverage Impact ⬆️ Merging this pull request will increase total coverage on Modified Files with Diff Coverage (6)
🤖 Increase coverage with AI coding...🚦 See full report on Qlty Cloud » 🛟 Help
|
- Double-quote shell variables in e2e test to prevent globbing/splitting - Use single-quoted heredoc for JSON literal - Fix empty-response check to use proper test syntax - Add unit test for buildSubmission with credential_selection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces a credential_selection parameter on the service access token request API and wires it through the RFC021/OpenID4VP flow into the holder’s presentation building so callers can deterministically disambiguate which wallet credential to present when multiple match.
Changes:
- Added
credential_selectionto the OpenAPI spec and regenerated API/client types. - Threaded
credential_selectionthrough API handler → IAM client → wallet → presenter and applied it viape.NewSelectionSelector. - Extended e2e test PD + script to validate selecting the intended organization credential end-to-end.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
docs/_static/auth/v2.yaml |
Adds credential_selection schema + documentation to the API spec. |
auth/api/iam/generated.go |
Regenerated request type to include CredentialSelection. |
auth/api/iam/api.go |
Extracts credential_selection from request body and forwards it to IAM client. |
auth/api/iam/api_test.go |
Updates mocks/calls for new IAM client method signature. |
auth/client/iam/interface.go |
Extends RequestRFC021AccessToken signature with credentialSelection. |
auth/client/iam/openid4vp.go |
Forwards credentialSelection into wallet.BuildSubmission. |
auth/client/iam/openid4vp_test.go |
Updates expectations for the updated wallet call signature. |
auth/client/iam/mock.go |
Regenerated mock for updated client interface. |
vcr/holder/interface.go |
Extends wallet BuildSubmission signature with credentialSelection. |
vcr/holder/sql_wallet.go |
Passes credentialSelection through to presenter. |
vcr/holder/memory_wallet.go |
Passes credentialSelection through to presenter (in-memory wallet). |
vcr/holder/presenter.go |
Constructs and installs a selection-based credential selector when provided. |
vcr/holder/presenter_test.go |
Adds a unit test asserting selection narrows to the intended credential. |
vcr/holder/mock.go |
Regenerated mock for updated wallet interface. |
e2e-tests/oauth-flow/rfc021/node-A/presentationexchangemapping.json |
Adds id: organization_name to PD field so it can be targeted by selection. |
e2e-tests/oauth-flow/rfc021/do-test.sh |
Adds e2e flow that issues two org creds and selects one via credential_selection. |
e2e-tests/browser/client/iam/generated.go |
Updates generated browser test client with CredentialSelection. |
auth/api/iam/openid4vp.go |
Updates wallet BuildSubmission call with new signature (selection unused here). |
auth/api/iam/openid4vp_test.go |
Updates wallet mock expectations for new signature. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Extract credential_selection from request | ||
| var credentialSelection map[string]string | ||
| if request.Body.CredentialSelection != nil { | ||
| credentialSelection = *request.Body.CredentialSelection | ||
| } | ||
|
|
||
| clientID := r.subjectToBaseURL(request.SubjectID) | ||
| tokenResult, err := r.auth.IAMClient().RequestRFC021AccessToken(ctx, clientID.String(), request.SubjectID, request.Body.AuthorizationServer, request.Body.Scope, useDPoP, credentials) | ||
| tokenResult, err := r.auth.IAMClient().RequestRFC021AccessToken(ctx, clientID.String(), request.SubjectID, request.Body.AuthorizationServer, request.Body.Scope, useDPoP, credentials, credentialSelection) |
There was a problem hiding this comment.
The new credential_selection request field is wired through to RequestRFC021AccessToken, but there’s no unit test in api_test.go asserting that a non-nil CredentialSelection is actually forwarded (and therefore influences behavior like cache key hashing). Add a test case that sets body.CredentialSelection and expects RequestRFC021AccessToken to be called with the same map (or a semantically equivalent value).
| @@ -296,7 +296,7 @@ func (c *OpenID4VPClient) RequestRFC021AccessToken(ctx context.Context, clientID | |||
| additionalWalletCredentials[subjectDID] = append(additionalWalletCredentials[subjectDID], credential.AutoCorrectSelfAttestedCredential(curr, subjectDID)) | |||
| } | |||
| } | |||
| vp, submission, err := c.wallet.BuildSubmission(ctx, subjectDIDs, additionalWalletCredentials, *presentationDefinition, params) | |||
| vp, submission, err := c.wallet.BuildSubmission(ctx, subjectDIDs, additionalWalletCredentials, *presentationDefinition, credentialSelection, params) | |||
There was a problem hiding this comment.
RequestRFC021AccessToken now accepts credentialSelection and forwards it to wallet.BuildSubmission, but the tests only cover the nil case. Add a unit test that passes a non-empty credentialSelection map and asserts the wallet mock receives the same value, to prevent regressions where the selection gets dropped.
| type: object | ||
| description: | | ||
| Optional key-value mapping for credential selection when the wallet contains multiple | ||
| credentials matching a single input descriptor. Each key must match a field id declared |
There was a problem hiding this comment.
Use consistent capitalization for the abbreviation “ID” in this user-facing description (“field id” → “field ID”).
| credentials matching a single input descriptor. Each key must match a field id declared | |
| credentials matching a single input descriptor. Each key must match a field ID declared |
| useDPoP = false | ||
| } | ||
| // Extract credential_selection from request | ||
| var credentialSelection map[string]string |
There was a problem hiding this comment.
might be smart to initialize as empty map if not provided, because otherwise you'll be dealing with nil further downstream, which is error-prone
| @@ -45,7 +45,7 @@ type Client interface { | |||
| PresentationDefinition(ctx context.Context, endpoint string) (*pe.PresentationDefinition, error) | |||
| // RequestRFC021AccessToken is called by the local EHR node to request an access token from a remote OAuth2 Authorization Server using Nuts RFC021. | |||
There was a problem hiding this comment.
godoc to explaincredentials and credentialSelection is appropriate
| credentials matching a single input descriptor. Each key must match a field id declared | ||
| in the Presentation Definition's input descriptor constraints. The value narrows the | ||
| match to credentials where that field equals the given value. | ||
|
|
There was a problem hiding this comment.
maybe add what the behavior is when there are multiple credentials in the wallet that match, but no credential selector is provided
| // If credential selection is provided, create a selector that narrows | ||
| // credential selection per input descriptor by field ID values. | ||
| if len(credentialSelection) > 0 { | ||
| selector, err := pe.NewSelectionSelector(credentialSelection, presentationDefinition, pe.FirstMatchSelector) |
There was a problem hiding this comment.
I don't understand the fallback FirstMatchSelector. We want to apply the credentialSelection criteria, which should lead to exactly 1 match per Input Descriptor, so why the FirstMatchSelector?

Summary
credential_selectionparameter toServiceAccessTokenRequestOpenAPI spec — a simplemap[string]stringmapping PD field IDs to expected valuesNewSelectionSelectorcredential_query(DCQL) approach with named parametersid: "organization_name"to e2e test PD so credential selection can target itcredential_selection, verifies introspection returns correct orgCloses #4090
Part of #4067
Callstack
Test plan
vcr/pe/,vcr/holder/,auth/api/iam/,auth/client/iam/🤖 Generated with Claude Code