feat(openid4vci): migrate to v1.0 spec - #4057
Conversation
|
Coverage Impact ⬇️ Merging this pull request will decrease total coverage on Modified Files with Diff Coverage (5)
🤖 Increase coverage with AI coding...🚦 See full report on Qlty Cloud » 🛟 Help
|
There was a problem hiding this comment.
Pull request overview
Migrates the Nuts Node OpenID4VCI implementation from draft-11 to the OpenID4VCI v1.0 (ID1) spec across issuer/holder flows, API surface, and related client integrations.
Changes:
- Introduces the v1.0 Nonce Endpoint and moves nonce handling out of the token response (including retry-on-
invalid_noncelogic). - Updates core OpenID4VCI types to v1.0 shapes (
credential_configuration_id,proofs,credentials[], metadata map format, typed grants). - Refreshes issuer/holder/auth client logic, tests, and OpenAPI docs/codegen to match the new v1.0 contract.
Reviewed changes
Copilot reviewed 37 out of 37 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| vcr/test/openid4vci_integration_test.go | Aligns integration test requests/headers and VC context/type usage. |
| vcr/openid4vci/wallet_client_test.go | Updates offer payload assertions to v1.0 fields and typed grants. |
| vcr/openid4vci/validators_test.go | Adjusts credentialSubject validation test setup. |
| vcr/openid4vci/validators.go | Refines CredentialDefinition.Validate semantics and spec link. |
| vcr/openid4vci/types_test.go | Adds v1.0 conformance tests for requests/offers/metadata/responses. |
| vcr/openid4vci/types.go | Updates OpenID4VCI types for v1.0 (nonce endpoint, config IDs, proofs, credentials array). |
| vcr/openid4vci/test.go | Updates test harness to return v1.0 credential responses and serve nonce endpoint. |
| vcr/openid4vci/issuer_client_test.go | Updates issuer client tests and adds nonce request tests. |
| vcr/openid4vci/issuer_client_mock.go | Extends mock issuer API client with RequestNonce. |
| vcr/openid4vci/issuer_client.go | Adds nonce request support and structured error parsing for credential requests. |
| vcr/openid4vci/error.go | Aligns error codes with v1.0 (e.g., invalid_nonce, invalid_credential_request). |
| vcr/issuer/test/valid/ExampleCredential.json | Updates test credential definition to v1.0 fields and example context. |
| vcr/issuer/openid_test.go | Updates issuer handler tests for config map metadata and nonce endpoint behavior. |
| vcr/issuer/openid_store_test.go | Adds standalone nonce store/consume test coverage. |
| vcr/issuer/openid_store.go | Implements standalone nonce storage/consumption in OpenID store. |
| vcr/issuer/openid_mock.go | Updates mock OpenID handler interface (token no longer returns c_nonce; adds nonce handler). |
| vcr/issuer/openid.go | Migrates issuer logic to v1.0 (config IDs, nonce endpoint, proof validation changes, config loading). |
| vcr/issuer/assets/definitions/NutsOrganizationCredential.json | Adds proof type support and updates context URL. |
| vcr/issuer/assets/definitions/NutsAuthorizationCredential.json | Adds proof type support and updates context URL. |
| vcr/holder/openid_test.go | Updates holder tests for config-id driven requests and nonce endpoint + retry flows. |
| vcr/holder/openid.go | Migrates holder flow to resolve config from metadata and use nonce endpoint + retry logic. |
| vcr/api/openid4vci/v0/issuer_test.go | Updates issuer API wrapper tests and adds nonce endpoint test. |
| vcr/api/openid4vci/v0/issuer.go | Updates credential response shape and adds nonce endpoint handler. |
| vcr/api/openid4vci/v0/holder_test.go | Updates holder API wrapper test offer payload to v1.0 structure. |
| vcr/api/openid4vci/v0/generated.go | Regenerates server interface/types/routes for nonce endpoint; removes 403 credential response type. |
| vcr/api/openid4vci/v0/api.go | Exposes nonce response type alias. |
| docs/_static/vcr/openid4vci_v0.yaml | Updates OpenAPI spec for v1.0 shapes and adds nonce endpoint definition. |
| codegen/configs/vcr_openid4vci_v0.yaml | Includes NonceResponse in generated models. |
| auth/oauth/types.go | Extends credential issuer metadata with nonce_endpoint. |
| auth/client/iam/openid4vp_test.go | Updates IAM client tests for v1.0 credentials response and nonce endpoint. |
| auth/client/iam/openid4vp.go | Adds nonce request method and updates credential retrieval signature. |
| auth/client/iam/mock.go | Updates IAM client mock signatures and adds RequestNonce. |
| auth/client/iam/interface.go | Extends IAM client interface to support nonce and config-id based credential requests. |
| auth/client/iam/client.go | Implements nonce endpoint call and v1.0 credential request/response parsing + structured errors. |
| auth/api/iam/session.go | Persists nonce endpoint + credential configuration ID for the OpenID4VCI flow. |
| auth/api/iam/openid4vci_test.go | Updates callback flow tests for nonce endpoint usage and invalid_nonce retry behavior. |
| auth/api/iam/openid4vci.go | Updates OpenID4VCI callback logic to fetch nonce, request credential by config ID, and retry on invalid_nonce. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
❌ 4 blocking issues (14 total)
@qltysh one-click actions:
|
|
There's an important disctinction, which I think I failed to mention. We have an OpenID4VCI implementation in That does not apply, however, to the OpenID4VCI client in the I believe we should only update the latter part (and document the first part better...) |
Replace draft-era error codes (unsupported_credential_type, unsupported_credential_format) with the complete set of 7 Credential Endpoint error codes from OpenID4VCI v1.0 Section 8.3.1.2.
Core structural changes for OpenID4VCI v1.0 alignment:
- Metadata uses credential_configurations_supported map keyed by
credential_configuration_id (replaces credentials_supported array)
- Credential offers reference configuration IDs instead of inline
credential definitions (credential_configuration_ids field)
- Typed grant structs replace untyped maps in offers
- Credential requests use credential_configuration_id
- Issuer matches credentials to configurations via findCredentialConfigID
- Config IDs generated as {CredentialType}_{format}
- InvalidNonce used for nonce errors (was InvalidProof in draft)
- server_error returns HTTP 500 (was incorrectly 400)
Wallet-side changes for v1.0 alignment: - Holder resolves credential_configuration_id from issuer metadata instead of using inline credential definitions from offers - Credential requests use credential_configuration_id (v1.0 preferred) - Typed grant structs replace untyped map access - ServerError used for upstream failures (not InvalidRequest) - API handler returns non-pointer Credential in response
- Update OpenAPI schemas for v1.0 field names and structures - Update error code documentation for Credential Endpoint - Remove PreAuthorizedGrantAnonymousAccessSupported from VP authorization server metadata (belongs in VCI issuer metadata only per Section 12.3)
Three spec compliance fixes found during detailed v1.0 review:
- Credential response: use `credentials` array of wrapper objects
with `credential` key per Section 8.3
- Credential request: use `proofs` (plural) with
`{"jwt": ["..."]}` structure per Section 8.2.1
- Error response: remove c_nonce/c_nonce_expires_in fields (wallet
should use Nonce Endpoint), make c_nonce optional in holder
Update auth/ module OpenID4VCI client code for v1.0 compliance: - Use Nonce Endpoint instead of c_nonce from token response - Add credential_configuration_id to credential request and session - Change CredentialResponseEntry.Credential to json.RawMessage - Add invalid_nonce retry logic in callback - Add RequestNonce to IAM client interface - Remove c_nonce_expires_in from NonceResponse - Reject non-string @context/type entries in holder metadata parsing - Regenerate mocks and OpenAPI generated code
- Move nil body check before first field access in RequestOpenid4VCICredentialIssuance - Add comma-ok assertion on nonce claim type in issuer validateProof - Validate format field presence in holder resolveCredentialConfiguration - Add iss claim to holder proof JWT for consistency with auth module - Add tests: nil OwnDID, empty credentials, non-string nonce, missing format
…epcopyMap Restore original comments for unchanged OAuth2 error codes. Replace JSON round-trip deepcopy with direct map copy matching the master approach.
Revert deepcopyMap to JSON round-trip: shallow copy is insufficient for nested maps like credential_definition. Remove resolved TODO about credential validation (already done via ValidateDefinitionWithCredential).
- Restore defensive panics in deepcopyMap for unmarshalable data - Make CredentialOffer.Grants a pointer with omitempty (OPTIONAL per Section 4.1.1) - Return false for non-string types in matchesCredential instead of silently skipping - Add iss claim validation in issuer proof verification per v1.0 Appendix F.1, with dedicated test case - Validate non-empty c_nonce from Nonce Endpoint responses - Remove redundant WithContext in RequestNonce
Remove draft-era Format and CredentialDefinition fields from CredentialRequest (v1.0 uses credential_configuration_id only). Rename CredentialConfigurationId(s) to CredentialConfigurationID(s) per Go naming convention for acronyms. JSON wire format unchanged.
The Credential Issuer URL is user-controlled (via the OpenAPI request body), and the Nonce/Credential Endpoints come transitively from the issuer's metadata. CodeQL flagged this as uncontrolled data flowing into a network request. Two layers of protection, mirroring the master security stack: - NewClient now takes a core.HTTPRequestDoer (in production: httpclient.StrictHTTPClient via NewWithCache). That gives us the HTTPS-in-strict check, User-Agent, and 1MB response body limit for free. - A strictMode flag on the client validates each target URL via core.ParsePublicURL on entry to OpenIDCredentialIssuerMetadata, RequestNonce, and RequestCredential. Strict mode adds rejection of IP hosts and reserved hostnames on top of HTTPS-only. The validateURL helper localises the validation pattern; if rules ever change, one place to update.
Per OpenID4VCI 1.0 §8.2 a Credential Request needs either credential_configuration_id or credential_identifier. The OpenAPI schema required the authorization_details field but allowed an empty array, which would let a request reach the issuer with neither identifier set. Add minItems:1 to the array so the empty case is rejected at request validation time.
…tifier Per OpenID4VCI 1.0 §12.2.1, the Credential Issuer Identifier is a URL that "contains scheme, host and, optionally, port number and path components, but no query or fragment components". core.ParsePublicURL did not enforce this; an issuer URL with a query or fragment slipped through and was forwarded into the constructed .well-known URL via url.URL.String(). Reject these explicitly in OpenIDCredentialIssuerMetadata.
Per OpenID4VCI 1.0 §F.1, the proof JWT aud claim MUST be the Credential Issuer Identifier. The handler stored authzServerMetadata.Issuer (the Authorization Server issuer) and used it as the proof audience, which is only correct when the metadata's authorization_servers field is empty (implicit AS == CI). When the issuer delegates to a separate AS, the AS issuer URL differs from the Credential Issuer Identifier and the proof is rejected. Persist credentialIssuerMetadata.CredentialIssuer separately on the session and use it for the proof audience. IssuerURL keeps its meaning as the AS issuer (used elsewhere in the OpenID4VP flow).
Drop the local Error/ErrorCode types in auth/openid4vci. The OpenID4VCI
spec error response shape (`{"error": ..., "error_description": ...}`)
matches RFC 6749 / oauth.OAuth2Error one-to-one. Add the OpenID4VCI-only
extension code (invalid_nonce) to oauth.ErrorCode and let the client and
its callers use the existing oauth types directly.
Per Rein's review of PR #4057.
…lient Per Rein's review: this validation conceptually belongs on the shared HTTP transport (httpclient.StrictHTTPClient) so every outbound call gets the IP/reserved-host check, not just OpenID4VCI. The current placement preserves parity with master (which validated via oauth.IssuerIdToWellKnown) and addresses the CodeQL SSRF finding for this PR. Mark as a follow-up to consolidate the check in the shared client.
Make the comment on the credential parsing block explicit about the reason: json.RawMessage keeps the raw JSON encoding, which for a JWT includes the surrounding quotes. ParseVerifiableCredential rejects that as invalid base64, so the unmarshal-as-string step is required to strip the quotes (and is a no-op for JSON-LD objects via the error fallback).
…known URL credentialIssuerWellKnown was assigning u.EscapedPath() (already encoded) back into u.Path; url.URL.String reescapes via EscapedPath, turning %2F into %252F. Prepend the well-known segment to u.Path (decoded) and to u.RawPath when the latter is set, so original encoding is preserved without double-escaping.
The OpenAPI schema declares minItems: 1 but the StrictServer middleware does not enforce minItems at runtime, so an empty array passed through silently. The handler used a defensive 'if len > 0' guard that left IssuerCredentialConfigurationID empty, which would later produce a malformed Credential Request. Replace the guard with an explicit core.InvalidInputError positioned before the issuer/auth-server metadata fetches, so an invalid local request does not trigger any outbound work. Update requestCredentials test helper to populate AuthorizationDetails, and add a regression test.
The handler only consumes the first entry, so accepting an array with multiple entries silently dropped extras. Add maxItems: 1 to the schema to make the contract explicit, and tighten the handler guard from 'len > 0' to 'len == 1' so both bounds are checked at runtime (the StrictServer middleware enforces neither).
Audit pass against the spec. Several section pointers in code and YAML descriptions were imprecise or wrong: - 'Section 8.2.1.1' for the JWT typ value -> Appendix F.1 (jwt Proof Type) - 'Section 8.2.1' for proofs parameter -> Section 8.2 (with proof type formats listed in Appendix F) - '§10' for notification_id -> §11 (Notification Endpoint) - '§12.2' for the .well-known retrieval -> §12.2.2 (Credential Issuer Metadata Retrieval) - '§5.1' for authorization_details usage in YAML descriptions -> §5.1.1 (Using Authorization Details Parameter) Documentation only; no behaviour change.
|
Can you add OpenID4VCI to these docs: https://nuts-node.readthedocs.io/en/stable/pages/deployment/oauth.html |
List OpenID4VCI 1.0 among the implemented specs and document the wallet flow: Authorization Code with authorization_details, PKCE, Nonce Endpoint, proof JWT, and invalid_nonce recovery. Note the unimplemented operational features (deferred issuance, Notification Endpoint, multiple credentials per call) and the unrelated draft-11 internal flow in vcr/openid4vci.
OpenID4VCI 1.0 (#4057) moved the credential-issuer HTTP client from auth/client/iam into a dedicated auth/openid4vci package and replaced the map-based Credential Request body with typed structs (proofs array, mutually-exclusive credential_configuration_id / credential_identifier). This merge re-applies the credential_details forwarding feature on top of that new base: - RequestCredentialOpts gains a CredentialDetails map[string]any. - RequestCredential merges those keys with the typed body (spec-defined fields always overlay the caller's, so credential_configuration_id / credential_identifier / proofs win). - OAuthSession carries CredentialRequestDetails alongside the new IssuerNonceEndpoint / IssuerCredentialConfigurationID / IssuerCredentialIssuer fields master added. - handleOpenID4VCICallback passes the session's CredentialRequestDetails through requestCredentialWithProof. - v2.yaml keeps the experimental credential_details property; its description is updated to reflect the v1.0 typed overlay (no longer a flat "proof" overwrite). Assisted by AI
OpenID4VCI 1.0 (#4057) moved the credential-issuer HTTP client from auth/client/iam into a dedicated auth/openid4vci package and replaced the map-based Credential Request body with typed structs (proofs array, mutually-exclusive credential_configuration_id / credential_identifier). This merge re-applies the credential_details forwarding feature on top of that new base: - RequestCredentialOpts gains a CredentialDetails map[string]any. - RequestCredential merges those keys with the typed body (spec-defined fields always overlay the caller's, so credential_configuration_id / credential_identifier / proofs win). - OAuthSession carries CredentialRequestDetails alongside the new IssuerNonceEndpoint / IssuerCredentialConfigurationID / IssuerCredentialIssuer fields master added. - handleOpenID4VCICallback passes the session's CredentialRequestDetails through requestCredentialWithProof. - v2.yaml keeps the experimental credential_details property; its description is updated to reflect the v1.0 typed overlay (no longer a flat "proof" overwrite). Assisted by AI

Summary
Migrate the user/browser OpenID4VCI flow in
auth/iamto OpenID4VCI v1.0 (ID-1).The internal node-to-node issuance flow in
vcr/openid4vciis deliberately left on its existing draft-11-inspired wire format. That flow is used in production for Nuts node-to-node issuance and is not a standards-conformant Wallet implementation per OpenID4VCI v1.0 §2. The package godoc documents the divergence and points readers to the newauth/openid4vcipackage for the v1.0 user/browser flow.v1.0 Spec Alignment
auth/openid4vcipackage for v1.0 protocol types, error handling, issuer metadata lookup, Nonce Endpoint calls, and Credential Requests./.well-known/openid-credential-issuer, includingcredential_endpoint,nonce_endpoint, andauthorization_servers.invalid_nonceby fetching a fresh nonce.proofs.jwtand either:credential_identifierwhen the Token Response contains matchingauthorization_details[].credential_identifiers, orcredential_configuration_idwhen the Authorization Server did not returnauthorization_details.credentials[]wrapper and support both JSON string credentials (JWT/SD-JWT) and JSON object credentials (JSON-LD).credential_issuer) as the proof JWTaud, not the Authorization Server issuer.Implementation Improvements
auth/openid4vci.authorization_detailsin the auth OpenAPI schema and requiretype=openid_credentialpluscredential_configuration_id.credential_issuermatches the requested issuer identifier.oauth.OAuth2Errorfor OpenID4VCI endpoint errors instead of introducing a duplicate error type.Scope Notes
vcr/openid4vcistays on the existing internal draft-11-style flow. This PR only adds documentation clarifying that package's purpose and limitations.credential_offer_uri, notification endpoints, and issuer-side v1.0 support are out of scope.