nuts-foundation / nuts-foundation/nuts-node
OpenID4VCI: select credential by type in request-credential; node resolves config + authorization flow from metadata
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 28
- Forks
- 23
- Avg merge
- 1d 10h
- Merged PRs (30d)
- 76
Description
Related: #4248 (supersedes), #4338, #4316, #4310, #4365
Parent PRD: #3972
Problem Statement
POST /internal/auth/v2/{subjectID}/request-credential requires the EHR to hand-build an
authorization_details array containing OpenID4VCI/RFC 9396 internals — type: "openid_credential",
credential_configuration_id, format (auth/api/iam/openid4vci.go:88-91,
auth/api/iam/generated.go AuthorizationDetail). The EHR should be able to ask for a credential with the
only facts it actually owns: the credential type it needs (a VC concept) and which issuer it trusts.
The node also needs to drive two issuers in the network that select the credential differently — one via
authorization_details, one via credential_configuration_id at the credential endpoint. Both selection
points are derivable from metadata the node already fetches, so the node can negotiate the flow instead of
the caller encoding it.
Trigger: a generic OpenID4VCI 1.0 client for the Nuts network — (a) the Authorization Details flow used by the Vektis demo
issuer and (b) the AET SDK (authorization scope + credential-endpoint selection).
Solution
Expose credential type as the request input. The node resolves the rest from metadata:
- type →
credential_configuration_id, by matching the type against the issuer's
credential_configurations_supported. credential_configuration_id→ authorization-stage flow, chosen from AS metadata.- credential endpoint → the resolved
credential_configuration_id(the node also honors
credential_identifierwhen an AS returns one in the Token Response — §8.2; already handled by
extractCredentialIdentifier,auth/api/iam/openid4vci.go:197).
The node keeps the resolved credential_configuration_id in the session
(OAuthSession.IssuerCredentialConfigurationID, auth/api/iam/session.go), so both flows terminate at the
credential endpoint with no extra plumbing.
Considered and rejected: making credential_configuration_id the primary input (#4248). The type is the
caller's natural handle; the config id is an issuer-specific string the node can resolve.
User Stories
User Stories
- As an EHR integrator, I want to request a credential by its type and issuer, so that I work entirely in
VC concepts. - As a Nuts node operator, I want the node to choose the authorization flow from issuer/AS metadata, so that
one API serves both the Vektis demo issuer and the AET SDK.
Implementation Decisions
API extension
POST /internal/auth/v2/{subjectID}/request-credential
{
"wallet_did": "did:web:example.com:iam:123", // (#4365 may make this optional)
"issuer": "https://issuer.example.com", // trust decision
"credential_type": "HealthcareProviderRoleTypeCredential", // which credential to request
"redirect_uri": "https://ehr.example.com/cb",
"profile": "aet" // optional: trusted param bundle (#4338)
}
credential_type is the only credential-specific field the caller provides. The node derives
credential_configuration_id, the credential format, and the authorization-stage flow from the issuer's
metadata.
Type → configuration resolution (new)
Match credential_type against each entry in credential_configurations_supported, by the type-locating
field per format:
| Format | Type field |
|---|---|
jwt_vc_json, ldp_vc |
credential_definition.type (array; match ignoring base VerifiableCredential) |
vc+sd-jwt, dc+sd-jwt |
vct |
credential_definition / vct are read from the issuer's Credential Issuer Metadata
(/.well-known/openid-credential-issuer, §12.2). Match on the type field, not the map key: the spec lets
credential_configuration_id be an arbitrary issuer-chosen string.
Resolution outcome:
| Result | Behavior |
|---|---|
| 1 match | use it |
| Multiple matches | take the first whose format the node supports, iterating a fixed node format-preference order (deterministic — never Go map order) |
| 0 matches | 400 — issuer does not offer a credential of type "<type>" |
| matches only in formats the node does not support | 400 — issuer offers "<type>" only in format(s): <list> |
Authorization-stage flow (chosen from AS metadata, no probing)
- Authorization Details flow — AS advertises
authorization_details_types_supportedcontaining
openid_credential: the node sends the resolvedcredential_configuration_idinside
authorization_details. The Credential Request usescredential_identifierwhen the Token Response
returnscredential_identifiers, otherwisecredential_configuration_id(§8.2). Used by the Vektis mock. - Credential Configuration ID flow — otherwise: the node identifies the credential with
credential_configuration_idin the Credential Request (§8.2). The authorization scope the AS requires is
supplied by the requestprofile. Used by the AET SDK (theaetprofile supplies
scope=openid profile api).
Both flows reuse the same resolved credential_configuration_id; extractCredentialIdentifier picks the
correct credential-endpoint field independent of the flow.
Target-issuer compliance
Validated against the two issuers in the network. The Vektis mock is our demo app (it implements OpenID4VCI
itself; the Nuts node has no issuer side); the AET SDK is a third-party issuer whose development we influence.
Both use jwt_vc_json with credential_definition.type.
Vektis mock (nuts-knooppunt/mock-components/vc-issuer) |
AET SDK | |
|---|---|---|
| Format | jwt_vc_json |
jwt_vc_json |
| Type-locating field | credential_definition.type = […,"HealthcareProviderRoleTypeCredential"] |
credential_definition.type = […,"HealthcareProfessionalDelegationCredential"] / PatientEnrollmentCredential |
| Authorization-stage flow | Authorization Details flow (authorization_details_types_supported: [openid_credential]) |
Credential Configuration ID flow (scope=openid profile api via the aet profile) |
| Credential selected by | credential_configuration_id in authorization_details → credential endpoint |
credential_configuration_id at the Credential Request |
Dependency for e2e: AET's credential_configurations_supported is populated with the credential types (we
influence this upstream).
Post-issuance validation
After the Credential Endpoint returns, verify the issued credential's type matches the requested
credential_type before storing (vcr.Wallet().Put, auth/api/iam/openid4vci.go:258); return a
502-class error on mismatch. Implement on the auth side.
Modules to build/modify
auth/openid4vci/types.go: extendOpenIDCredentialIssuerMetadatawith
CredentialConfigurationsSupported map[string]CredentialConfiguration; addCredentialConfiguration
(format,credential_definition/vct).auth/oauth/types.go: addAuthorizationDetailsTypesSupported []stringto
AuthorizationServerMetadata.auth/api/iam/openid4vci.go: type-match resolution + flow selection in
RequestOpenid4VCICredentialIssuance; post-issuance type check inhandleOpenID4VCICallback.docs/_static/auth/v2.yaml+ regenerate (make gen-api):credential_typerequest body.auth/openid4vci/client.go:RequestCredentialalready emitscredential_identifier/
credential_configuration_idper §8.2 — reused as-is.
Testing Decisions
A good test asserts external wire behavior: given issuer + AS metadata fixtures and a credential_type,
assert (a) the resolved credential_configuration_id, (b) the authorization-stage flow chosen, (c) the
Credential Request body, (d) the failure response for each unhappy path.
Modules to test:
- Type resolution: single match; multiple matches → first supported by preference order (deterministic);
0 matches → 400; matches only in unsupported formats → 400; per-format type field
(credential_definition.type,vct). - Flow selection: Authorization Details flow (AS advertises
authorization_details_types_supported); Credential Configuration ID flow
(AS does not) with profile-supplied scope. - Post-issuance: issuer returns a different type → rejected, not stored.
- Prior art:
auth/api/iam/openid4vci_test.go.
Impact Assessment
- Backwards compatibility: the request body now takes
credential_type; breaking change to the
experimental API. Acceptable — flagged experimental. - Versioning: minor; auth v2, experimental.
- Configuration/deployment: Authorization Details flow issuers advertise
authorization_details_types_supported; AET-style issuers use a requestprofilefor the authorization scope. - Security: no new key material. The post-issuance type check guarantees the stored credential matches the
request. Existing redaction oncredential_request_params(PII) retained. - Spec stability: OpenID4VCI 1.0 (ID-1), §8.2, §12.2; RFC 9396.
Out of Scope
- Metadata-driven scope-flow (credential-selection scope from
credential_configurations_supported[*].scope)
— future; the current issuers are covered by the Authorization Details flow and Credential Configuration ID flow. - Pre-authorized_code flow and Credential Offer consumption (issuer-initiated) — tracked under #3972.
- Multiple credentials per request — future
credential_typearray. vcr/openid4vci/vcr/holder/openid.go— separate draft-11 flow, not this code path.
Further Notes
- The request
profile(#4338) carries any issuer-specific authorization-request parameters (e.g. AET's
openid profile apiscope), keeping the caller surface in VC concepts. credential_definition/vctare already in the metadata documents the node fetches today; step #1 parses
fields the node currently discards, no new requests.
Implementation Plan
Logical steps, ordered by dependency.
- Parse
credential_configurations_supported(issuer) +authorization_details_types_supported(AS). - Type →
credential_configuration_idresolution: match + deterministic format-preference pick + errors.
(needs 1) - Authorization-stage flow selection (Authorization Details flow / Credential Configuration ID flow) +
credential-endpoint wiring. (needs 1) - API surface:
credential_typerequest body; OpenAPI + regen. (needs 2, 3) - Post-issuance credential-type validation before wallet store. (needs 2)
- Docs + e2e (Vektis Authorization Details flow, AET Credential Configuration ID flow). (needs 4, 5)
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with auth/api/iam/openid4vci.go, auth/openid4vci/types.go, auth/oauth/types.go, and auth/api/iam/openid4v4ci_test.go to trace metadata parsing, request handling, and existing callback behavior. Run the relevant OpenID4VCI tests first. Done means type-based configuration resolution, metadata-driven flow selection, API regeneration, unhappy-path coverage, and post-issuance type validation for both issuer flows.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- api, authentication, backend
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 52/100