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
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.goAuthorizationDetail). 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_identifier when 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_supported containing openid_credential: the node sends the resolved credential_configuration_id inside authorization_details. The Credential Request uses credential_identifier when the Token Response
returns credential_identifiers, otherwise credential_configuration_id (§8.2). Used by the Vektis mock.
Credential Configuration ID flow — otherwise: the node identifies the credential with credential_configuration_id in the Credential Request (§8.2). The authorization scope the AS requires is
supplied by the request profile. Used by the AET SDK (the aet profile 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.
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.
auth/oauth/types.go: add AuthorizationDetailsTypesSupported []string to AuthorizationServerMetadata.
auth/api/iam/openid4vci.go: type-match resolution + flow selection in RequestOpenid4VCICredentialIssuance; post-issuance type check in handleOpenID4VCICallback.
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 request profile for the authorization scope.
Security: no new key material. The post-issuance type check guarantees the stored credential matches the
request. Existing redaction on credential_request_params (PII) retained.
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.
credential_definition / vct are already in the metadata documents the node fetches today; step added baseline funcs from old repo #1 parses
fields the node currently discards, no new requests.
Related: #4248 (supersedes), #4338, #4316, #4310, #4365
Parent PRD: #3972
Problem Statement
POST /internal/auth/v2/{subjectID}/request-credentialrequires the EHR to hand-build anauthorization_detailsarray containing OpenID4VCI/RFC 9396 internals —type: "openid_credential",credential_configuration_id,format(auth/api/iam/openid4vci.go:88-91,auth/api/iam/generated.goAuthorizationDetail). The EHR should be able to ask for a credential with theonly 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 viacredential_configuration_idat the credential endpoint. Both selectionpoints 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:
credential_configuration_id, by matching the type against the issuer'scredential_configurations_supported.credential_configuration_id→ authorization-stage flow, chosen from AS metadata.credential_configuration_id(the node also honorscredential_identifierwhen an AS returns one in the Token Response — §8.2; already handled byextractCredentialIdentifier,auth/api/iam/openid4vci.go:197).The node keeps the resolved
credential_configuration_idin the session(
OAuthSession.IssuerCredentialConfigurationID,auth/api/iam/session.go), so both flows terminate at thecredential endpoint with no extra plumbing.
Considered and rejected: making
credential_configuration_idthe primary input (#4248). The type is thecaller's natural handle; the config id is an issuer-specific string the node can resolve.
User Stories
User Stories
VC concepts.
one API serves both the Vektis demo issuer and the AET SDK.
Implementation Decisions
API extension
credential_typeis the only credential-specific field the caller provides. The node derivescredential_configuration_id, the credential format, and the authorization-stage flow from the issuer'smetadata.
Type → configuration resolution (new)
Match
credential_typeagainst each entry incredential_configurations_supported, by the type-locatingfield per format:
jwt_vc_json,ldp_vccredential_definition.type(array; match ignoring baseVerifiableCredential)vc+sd-jwt,dc+sd-jwtvctcredential_definition/vctare 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 letscredential_configuration_idbe an arbitrary issuer-chosen string.Resolution outcome:
400—issuer does not offer a credential of type "<type>"400—issuer offers "<type>" only in format(s): <list>Authorization-stage flow (chosen from AS metadata, no probing)
authorization_details_types_supportedcontainingopenid_credential: the node sends the resolvedcredential_configuration_idinsideauthorization_details. The Credential Request usescredential_identifierwhen the Token Responsereturns
credential_identifiers, otherwisecredential_configuration_id(§8.2). Used by the Vektis mock.credential_configuration_idin the Credential Request (§8.2). The authorization scope the AS requires issupplied by the request
profile. Used by the AET SDK (theaetprofile suppliesscope=openid profile api).Both flows reuse the same resolved
credential_configuration_id;extractCredentialIdentifierpicks thecorrect 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_jsonwithcredential_definition.type.nuts-knooppunt/mock-components/vc-issuer)jwt_vc_jsonjwt_vc_jsoncredential_definition.type=[…,"HealthcareProviderRoleTypeCredential"]credential_definition.type=[…,"HealthcareProfessionalDelegationCredential"]/PatientEnrollmentCredentialauthorization_details_types_supported: [openid_credential])scope=openid profile apivia theaetprofile)credential_configuration_idinauthorization_details→ credential endpointcredential_configuration_idat the Credential RequestDependency for e2e: AET's
credential_configurations_supportedis populated with the credential types (weinfluence this upstream).
Post-issuance validation
After the Credential Endpoint returns, verify the issued credential's type matches the requested
credential_typebefore storing (vcr.Wallet().Put,auth/api/iam/openid4vci.go:258); return a502-class error on mismatch. Implement on the auth side.Modules to build/modify
auth/openid4vci/types.go: extendOpenIDCredentialIssuerMetadatawithCredentialConfigurationsSupported map[string]CredentialConfiguration; addCredentialConfiguration(
format,credential_definition/vct).auth/oauth/types.go: addAuthorizationDetailsTypesSupported []stringtoAuthorizationServerMetadata.auth/api/iam/openid4vci.go: type-match resolution + flow selection inRequestOpenid4VCICredentialIssuance; 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) theCredential Request body, (d) the failure response for each unhappy path.
Modules to test:
0 matches → 400; matches only in unsupported formats → 400; per-format type field
(
credential_definition.type,vct).authorization_details_types_supported); Credential Configuration ID flow(AS does not) with profile-supplied scope.
auth/api/iam/openid4vci_test.go.Impact Assessment
credential_type; breaking change to theexperimental API. Acceptable — flagged experimental.
authorization_details_types_supported; AET-style issuers use a requestprofilefor the authorization scope.request. Existing redaction on
credential_request_params(PII) retained.Out of Scope
credential_configurations_supported[*].scope)— future; the current issuers are covered by the Authorization Details flow and Credential Configuration ID flow.
credential_typearray.vcr/openid4vci/vcr/holder/openid.go— separate draft-11 flow, not this code path.Further Notes
profile(OpenID4VCI: configurable request profiles (auth.experimental.profile) with built-in AET profile #4338) carries any issuer-specific authorization-request parameters (e.g. AET'sopenid profile apiscope), keeping the caller surface in VC concepts.credential_definition/vctare already in the metadata documents the node fetches today; step added baseline funcs from old repo #1 parsesfields the node currently discards, no new requests.
Implementation Plan
Logical steps, ordered by dependency.
credential_configurations_supported(issuer) +authorization_details_types_supported(AS).credential_configuration_idresolution: match + deterministic format-preference pick + errors.(needs 1)
credential-endpoint wiring. (needs 1)
credential_typerequest body; OpenAPI + regen. (needs 2, 3)