Parent PRD
#4067 (see PRD comment)
What to build
Add credential_query as an optional field on the ServiceAccessTokenRequest and wire it through the full access token request flow.
Call stack
The credential_query flows through the following layers:
API handler: RequestServiceAccessToken (auth/api/iam/api.go)
extracts credential_query from request body
→ RequestRFC021AccessToken (auth/client/iam/openid4vp.go)
accepts credentialQueries as new parameter
→ wallet.BuildSubmission (vcr/holder/interface.go)
accepts credentialQueries as new parameter alongside presentationDefinition
→ presenter.buildSubmission (vcr/holder/presenter.go)
if credentialQueries provided:
selector, _ = pe.NewDCQLSelector(credentialQueries, pd, pe.FirstMatchSelector)
builder.SetCredentialSelector(selector)
→ builder.Build(format)
Changes per layer
OpenAPI spec (docs/_static/auth/v2.yaml): add credential_query (optional array of DCQL credential query objects) to ServiceAccessTokenRequest.
Generated types (auth/api/iam/generated.go): regenerate to include new field.
API handler (auth/api/iam/api.go): extract credential_query from request.Body, pass to RequestRFC021AccessToken.
Client interface (auth/client/iam/interface.go): add credentialQueries []dcql.CredentialQuery parameter to RequestRFC021AccessToken.
RFC021 client (auth/client/iam/openid4vp.go): accept credentialQueries, pass to wallet.BuildSubmission.
Wallet interface (vcr/holder/interface.go): add credentialQueries []dcql.CredentialQuery parameter to BuildSubmission, placed alongside presentationDefinition since they work together — the PD defines what's required, the queries narrow which specific credentials to use.
BuildSubmission(ctx context.Context, walletDIDs []did.DID,
additionalCredentials map[did.DID][]vc.VerifiableCredential,
presentationDefinition pe.PresentationDefinition,
credentialQueries []dcql.CredentialQuery,
params BuildParams,
) (*vc.VerifiablePresentation, *pe.PresentationSubmission, error)
SQL wallet (vcr/holder/sql_wallet.go): pass credentialQueries through to presenter.
Presenter (vcr/holder/presenter.go): if credentialQueries is non-empty, create a DCQL selector via pe.NewDCQLSelector(credentialQueries, pd, pe.FirstMatchSelector) and call builder.SetCredentialSelector(selector).
Design decisions
credential_query is a function parameter on BuildSubmission (not in BuildParams) because it's about which credentials to select, not about how to build the VP. BuildParams contains VP construction parameters (audience, format, nonce, expiry).
- The DCQL selector is constructed in the presenter layer, keeping PE/DCQL concerns out of the API and client layers.
- Coexistence: existing
credentials array (self-attested VCs) continues to work alongside credential_query.
- No
match_policy parameter — the DCQL filter must narrow to exactly one credential per input descriptor. See decision comment.
Example request
{
"scope": "medicatieoverdracht-gf",
"authorization_server": "https://verifier.example.com",
"credential_query": [
{
"id": "id_patient_enrollment",
"claims": [
{
"path": ["credentialSubject", "hasEnrollment", "patient", "identifier", "value"],
"values": ["123456789"]
}
]
}
]
}
Acceptance criteria
Blocked by
User stories addressed
- User story 1: specify which patient's credential via API
- User story 2: specify which organization type credential via API
- User story 3: only provide credential_query for ambiguous types
- User story 4: clear error on zero matches via API
- User story 5: clear error on ambiguous matches via API
- User story 6: standard DCQL syntax in API
- User story 7: self-attested credentials alongside credential_query
- User story 8: credential_query ID maps to PD input descriptor ID
- User story 12: descriptive error messages
Parent PRD
#4067 (see PRD comment)
What to build
Add
credential_queryas an optional field on theServiceAccessTokenRequestand wire it through the full access token request flow.Call stack
The
credential_queryflows through the following layers:Changes per layer
OpenAPI spec (
docs/_static/auth/v2.yaml): addcredential_query(optional array of DCQL credential query objects) toServiceAccessTokenRequest.Generated types (
auth/api/iam/generated.go): regenerate to include new field.API handler (
auth/api/iam/api.go): extractcredential_queryfromrequest.Body, pass toRequestRFC021AccessToken.Client interface (
auth/client/iam/interface.go): addcredentialQueries []dcql.CredentialQueryparameter toRequestRFC021AccessToken.RFC021 client (
auth/client/iam/openid4vp.go): acceptcredentialQueries, pass towallet.BuildSubmission.Wallet interface (
vcr/holder/interface.go): addcredentialQueries []dcql.CredentialQueryparameter toBuildSubmission, placed alongsidepresentationDefinitionsince they work together — the PD defines what's required, the queries narrow which specific credentials to use.SQL wallet (
vcr/holder/sql_wallet.go): passcredentialQueriesthrough to presenter.Presenter (
vcr/holder/presenter.go): ifcredentialQueriesis non-empty, create a DCQL selector viape.NewDCQLSelector(credentialQueries, pd, pe.FirstMatchSelector)and callbuilder.SetCredentialSelector(selector).Design decisions
credential_queryis a function parameter onBuildSubmission(not inBuildParams) because it's about which credentials to select, not about how to build the VP.BuildParamscontains VP construction parameters (audience, format, nonce, expiry).credentialsarray (self-attested VCs) continues to work alongsidecredential_query.match_policyparameter — the DCQL filter must narrow to exactly one credential per input descriptor. See decision comment.Example request
{ "scope": "medicatieoverdracht-gf", "authorization_server": "https://verifier.example.com", "credential_query": [ { "id": "id_patient_enrollment", "claims": [ { "path": ["credentialSubject", "hasEnrollment", "patient", "identifier", "value"], "values": ["123456789"] } ] } ] }Acceptance criteria
credential_queryfield added toServiceAccessTokenRequestin OpenAPI spec (optional array)RequestServiceAccessTokenhandler extracts and passescredential_querythrough the flowRequestRFC021AccessTokenacceptscredentialQueriesparameterWallet.BuildSubmissionacceptscredentialQueriesparameter alongsidepresentationDefinitioncredentialQueriesis non-emptycredentials(self-attested) array works alongsidecredential_querycredential_querybehaves identically to current behaviorcredential_queryselects correct credential from walletcredential_querymatching zero credentials returns descriptive errorcredential_querymatching multiple credentials returns descriptive errorcredential_queryvalues produce different cache keys (existing hash-based cache handles this)Blocked by
User stories addressed