Skip to content

#4090: Wire credential_selection through API to presenter#4122

Open
stevenvegt wants to merge 4 commits intofeature/4120-selection-selectorfrom
feature/4090-api-credential-query
Open

#4090: Wire credential_selection through API to presenter#4122
stevenvegt wants to merge 4 commits intofeature/4120-selection-selectorfrom
feature/4090-api-credential-query

Conversation

@stevenvegt
Copy link
Member

Summary

  • Adds credential_selection parameter to ServiceAccessTokenRequest OpenAPI spec — a simple map[string]string mapping PD field IDs to expected values
  • Wires it through the full callstack: API handler → IAM client → wallet → presenter → NewSelectionSelector
  • Replaces the previous credential_query (DCQL) approach with named parameters
  • Adds id: "organization_name" to e2e test PD so credential selection can target it
  • E2E test verifies end-to-end: issues two org credentials, selects one via credential_selection, verifies introspection returns correct org

Closes #4090
Part of #4067

Callstack

API: RequestServiceAccessToken (auth/api/iam/api.go)
  → extracts credential_selection map[string]string from request
  → RequestRFC021AccessToken (auth/client/iam/openid4vp.go)
    → wallet.BuildSubmission (vcr/holder/sql_wallet.go)
      → presenter.buildSubmission (vcr/holder/presenter.go)
        → pe.NewSelectionSelector(credentialSelection, pd, pe.FirstMatchSelector)
        → builder.SetCredentialSelector(selector)
        → builder.Build()

Test plan

  • E2E test: credential_selection selects correct org credential (sqlite, mysql, postgres, sqlserver)
  • E2E test: introspection returns correct organization name
  • All existing rfc021 e2e tests pass
  • Unit tests pass: vcr/pe/, vcr/holder/, auth/api/iam/, auth/client/iam/

🤖 Generated with Claude Code

stevenvegt and others added 3 commits March 25, 2026 17:42
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>
@qltysh
Copy link

qltysh bot commented Mar 25, 2026

0 new issues

Tool Category Rule Count

@qltysh
Copy link

qltysh bot commented Mar 25, 2026

Qlty

Coverage Impact

⬆️ Merging this pull request will increase total coverage on feature/4120-selection-selector by 0.02%.

Modified Files with Diff Coverage (6)

RatingFile% DiffUncovered Line #s
Coverage rating: B Coverage rating: B
vcr/holder/sql_wallet.go100.0%
Coverage rating: B Coverage rating: B
vcr/holder/presenter.go71.4%65-66
Coverage rating: B Coverage rating: B
auth/api/iam/api.go60.0%779-780
Coverage rating: B Coverage rating: B
auth/api/iam/openid4vp.go100.0%
Coverage rating: B Coverage rating: B
auth/client/iam/openid4vp.go100.0%
Coverage rating: F Coverage rating: F
vcr/holder/memory_wallet.go0.0%63-75
Total68.4%
🤖 Increase coverage with AI coding...

In the `feature/4090-api-credential-query` branch, add test coverage for this new code:

- `auth/api/iam/api.go` -- Line 779-780
- `vcr/holder/memory_wallet.go` -- Line 63-75
- `vcr/holder/presenter.go` -- Line 65-66

🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

- 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>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_selection to the OpenAPI spec and regenerated API/client types.
  • Threaded credential_selection through API handler → IAM client → wallet → presenter and applied it via pe.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.

Comment on lines +776 to +783
// 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)
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines 237 to +299
@@ -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)
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use consistent capitalization for the abbreviation “ID” in this user-facing description (“field id” → “field ID”).

Suggested change
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

Copilot uses AI. Check for mistakes.
useDPoP = false
}
// Extract credential_selection from request
var credentialSelection map[string]string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add credential_selection to access token request API

3 participants