Skip to content

feat(openid4vci): client compliance (issue #3152)#4058

Draft
JorisHeadease wants to merge 28 commits into
masterfrom
feature/openid4vci-client-compliance
Draft

feat(openid4vci): client compliance (issue #3152)#4058
JorisHeadease wants to merge 28 commits into
masterfrom
feature/openid4vci-client-compliance

Conversation

@JorisHeadease
Copy link
Copy Markdown
Contributor

@JorisHeadease JorisHeadease commented Mar 11, 2026

Summary

Client-side OpenID4VCI v1.0 compliance improvements per issue #3152, building on the wire format migration in #4057.

Planned commits

  • Validate authorization_details against issuer metadata (Section 5.1.1)
  • Validate proof_signing_alg_values_supported (Appendix F.1)
  • Detect deferred credential issuance (Section 7.3)
  • PAR support (Section 5.1.4)
  • credential_identifiers in token response (Section 6.2)
  • Scope-based credential requests (Section 5.1.2)
  • signed_metadata (Section 11.2)

Skipped

  • DPoP (Section 6 + 7) The credential request's proof JWT (Section 8.2) already covers the same threat surface: it's signed by the wallet's DID key, verified against the DID document, and includes a nonce. A stolen access token alone is useless without the wallet's private key. DPoP would add a second PoP layer with no practical security gain. Worth revisiting if an external issuer requires it.

  • credential_identifiers_supported Not part of the v1 spec?

  • 11.3 pre-authorized_grant_anonymous_access_supported The issue notes this is "incorrectly added to our metadata since we do not support pre-authorized_code flow". This was already addressed in the v1 wire format migration (feat(openid4vci): migrate to v1.0 spec #4057). The field was restored because we do support the pre-authorized code flow on the issuer side.

  • Notification endpoint (Section 10) Deferred, depends on credential lifecycle management.

Closes #3152

JorisHeadease and others added 19 commits February 24, 2026 17:04
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.
Change Credential field from map[string]interface{} to
json.RawMessage to support any credential format (JSON-LD objects,
JWT strings). Fixes Copilot review finding and aligns vcr module
with auth module's type.
Delete duplicate CredentialRequest, CredentialResponse, and
CredentialResponseEntry types from auth/client/iam. Use the
canonical types from vcr/openid4vci as single source of truth.
Replace local jwtTypeOpenID4VCIProof const with
openid4vci.JWTTypeOpenID4VCIProof.
@JorisHeadease JorisHeadease linked an issue Mar 11, 2026 that may be closed by this pull request
@qltysh
Copy link
Copy Markdown
Contributor

qltysh Bot commented Mar 11, 2026

Qlty


Coverage Impact

⬆️ Merging this pull request will increase total coverage on master by 0.06%.

Modified Files with Diff Coverage (12)

RatingFile% DiffUncovered Line #s
Coverage rating: C Coverage rating: C
crypto/jwx.go0.0%169-177
Coverage rating: B Coverage rating: B
vcr/issuer/openid.go81.6%149, 168, 245-249...
Coverage rating: B Coverage rating: B
auth/oauth/types.go100.0%
Coverage rating: A Coverage rating: B
vcr/holder/openid.go84.0%133-138, 215-216...
Coverage rating: A Coverage rating: A
vcr/openid4vci/validators.go100.0%
Coverage rating: B Coverage rating: B
auth/client/iam/openid4vp.go83.3%366-367
Coverage rating: C Coverage rating: B
vcr/api/openid4vci/v0/issuer.go87.5%115-116
Coverage rating: B Coverage rating: C
vcr/openid4vci/issuer_client.go64.6%103, 126-127...
Coverage rating: A Coverage rating: A
vcr/issuer/openid_store.go100.0%
Coverage rating: B Coverage rating: A
auth/api/iam/openid4vci.go93.2%46-48, 97-98...
Coverage rating: B Coverage rating: C
auth/client/iam/client.go76.7%250-251, 254-255...
New Coverage rating: A
vcr/openid4vci/types.go100.0%
Total83.4%
🤖 Increase coverage with AI coding...
In the `feature/openid4vci-client-compliance` branch, add test coverage for this new code:

- `auth/api/iam/openid4vci.go` -- Lines 46-48, 97-98, 125-126, 277-278, 287-288, and 368-369
- `auth/client/iam/client.go` -- Lines 250-251, 254-255, 259-260, 268-269, 271-272, 311-312, 323-324, 326-327, 394-395, 399-400, 404-405, 409-410, 415-416, and 457
- `auth/client/iam/openid4vp.go` -- Line 366-367
- `crypto/jwx.go` -- Line 169-177
- `vcr/api/openid4vci/v0/issuer.go` -- Line 115-116
- `vcr/holder/openid.go` -- Lines 133-138, 215-216, 219-220, 230-231, 234-235, 261-262, 266-267, 298-299, and 319
- `vcr/issuer/openid.go` -- Lines 149, 168, 245-249, 278-279, 333-337, 386-396, 423, 467-468, 491-492, 524, 528, 575, 586-587, 591-592, 596-597, 599-600, 604-605, 620-621, and 625-626
- `vcr/openid4vci/issuer_client.go` -- Lines 103, 126-127, 130-131, 135-136, 138-143, 147-148, and 163-164

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

Validate authorization_details entries per v1.0 Section 5.1.1:
- type must be "openid_credential"
- credential_configuration_id is required and must exist in issuer
  credential_configurations_supported
- Inject locations field when authorization_servers is present
- Sanitize entries to only known keys to prevent arbitrary JSON
  passthrough
- Reject multiple entries (single credential issuance only)

Add CredentialConfigurationsSupported to OpenIDCredentialIssuerMetadata.

Also fix nil context usage throughout openid4vci_test.go: use
context.Background() for method calls and gomock.Any() for mock
expectations.
Check holder's signing algorithm against the issuer's advertised
proof_signing_alg_values_supported (v1.0 Appendix F.1) in both the
authorization code flow and pre-authorized code flow. Shared
validation logic extracted to openid4vci.ValidateProofSigningAlg.
Detect transaction_id in credential responses (v1.0 Section 8.3) and
return a clear error instead of a generic "no credentials" message.
The transaction_id value is logged at warn level but excluded from
error messages to prevent leaking issuer-internal state.
Use Pushed Authorization Requests when the AS metadata advertises a
pushed_authorization_request_endpoint. All authorization parameters are
POSTed server-to-server; the browser redirect carries only client_id
and the returned request_uri. Falls back to query parameters when PAR
is not advertised.
When the token response includes authorization_details with
credential_identifiers (v1.0 Section 6.2), use credential_identifier
instead of credential_configuration_id in the credential request
(Section 8.2). Adds GetRaw to TokenResponse for accessing non-string
additional parameters.
Add scope as an alternative to authorization_details for requesting
credentials (v1.0 Section 5.1.2). The scope is resolved against the
issuer's credential_configurations_supported metadata. Both scope and
authorization_details can be provided simultaneously per the spec.
Only a single scope value is supported, consistent with the single-entry
restriction for authorization_details.
When the credential issuer's metadata contains signed_metadata (v1.0
Section 12.2.3), verify the JWT signature using the issuer's key from
the DID document. Validates typ header, required claims (sub, iat), and
compares metadata claims against the unsigned metadata. Rejects metadata
if verification fails; proceeds without it if absent (field is OPTIONAL).
The proof JWT audience (aud) must be the Credential Issuer Identifier
per v1.0 Section 8.2.1.1, not the Authorization Server issuer. These
differ when the credential issuer delegates to a separate AS.
@JorisHeadease JorisHeadease force-pushed the feature/openid4vci-client-compliance branch from bcb7311 to 6b9902b Compare March 16, 2026 15:24
Copy link
Copy Markdown
Contributor

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

Implements OpenID4VCI v1.0 (ID1) client-side compliance improvements, aligning the Nuts node’s OID4VCI wallet/client flows with issuer metadata requirements and interoperability expectations from issue #3152.

Changes:

  • Add support for PAR (RFC 9126), scope-based credential requests, and token-response credential_identifierscredential_identifier credential requests.
  • Add signed issuer metadata (signed_metadata) verification and proof signing algorithm validation against issuer metadata.
  • Detect deferred issuance (transaction_id) and fail fast with a clear “not supported” error.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
vcr/openid4vci/types_test.go Adds tests for deferred responses and proof signing algorithm extraction/validation helpers.
vcr/openid4vci/types.go Extends OID4VCI types with transaction_id, credential_identifier, and proof-alg validation helpers.
vcr/openid4vci/issuer_client_test.go Adds coverage for deferred issuance error handling in issuer client.
vcr/openid4vci/issuer_client.go Detects transaction_id and returns “deferred issuance not supported”.
vcr/holder/openid_test.go Adds tests for proof signing algorithm validation during holder issuance flow.
vcr/holder/openid.go Validates holder proof signing algorithm against issuer metadata before requesting credentials.
docs/_static/auth/v2.yaml Updates API schema: makes authorization_details optional and documents scope.
crypto/jwx.go Adds JWTTyp helper to read JWT typ header without validation.
auth/oauth/types_test.go Adds tests for TokenResponse.GetRaw.
auth/oauth/types.go Adds GetRaw, PAR metadata field, and OID4VCI issuer metadata extensions (signed_metadata, configurations).
auth/client/iam/openid4vp_test.go Adds tests for signed_metadata verification and PAR; adapts credential request signature changes.
auth/client/iam/openid4vp.go Adds PAR method and updates VerifiableCredentials call signature to include credential_identifier.
auth/client/iam/mock.go Updates mocks for new PAR and updated VerifiableCredentials signature.
auth/client/iam/interface.go Extends IAM client interface with PAR and credential_identifier support.
auth/client/iam/client.go Implements PAR call and signed_metadata verification; sends credential_identifier in credential requests.
auth/api/iam/session.go Stores issuer-supported proof signing algorithms in session for later validation.
auth/api/iam/openid4vci_test.go Adds extensive test coverage for new OID4VCI behaviors (PAR, scope, identifiers, alg validation, deferred).
auth/api/iam/openid4vci.go Adds scope support, authorization_details validation/sanitization, PAR flow, credential_identifier usage, deferred detection, and alg validation.
auth/api/iam/generated.go Updates generated request body: authorization_details optional and adds scope.
Comments suppressed due to low confidence (2)

auth/client/iam/client.go:37

  • Import block is not gofmt-sorted (third-party imports are mixed with stdlib). This will fail gofmt/lint checks and makes diffs noisier. Please run gofmt (or re-order imports into stdlib / third-party groups).
import (
	"bytes"
	"context"
	stdcrypto "crypto"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/lestrrat-go/jwx/v2/jws"
	"github.com/lestrrat-go/jwx/v2/jwt"
	"github.com/nuts-foundation/nuts-node/crypto"
	"github.com/nuts-foundation/nuts-node/vdr/resolver"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"

auth/client/iam/openid4vp_test.go:40

  • Import block is not gofmt-sorted (stdlib and third-party imports are interleaved). This commonly breaks CI formatting/lint checks; please run gofmt or re-order the imports.
import (
	"context"
	"crypto/ecdsa"
	"crypto/tls"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/lestrrat-go/jwx/v2/jwa"
	"github.com/lestrrat-go/jwx/v2/jws"
	"github.com/lestrrat-go/jwx/v2/jwt"
	"github.com/nuts-foundation/nuts-node/http/client"
	test2 "github.com/nuts-foundation/nuts-node/test"
	"github.com/nuts-foundation/nuts-node/vcr/credential"
	"github.com/nuts-foundation/nuts-node/vdr/didsubject"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"
	"time"


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread auth/client/iam/client.go Outdated
Require credential_endpoint to be present in the signed_metadata JWT
rather than silently skipping validation when absent. The spec requires
all metadata parameters to be present as top-level claims in the JWT.

Based on Copilot PR review feedback.
@JorisHeadease
Copy link
Copy Markdown
Contributor Author

JorisHeadease commented Mar 16, 2026

PR ready, keeping it as draft until #4057 is merged in.

@JorisHeadease JorisHeadease force-pushed the feature/openid4vci-v1 branch from 2474568 to a72d50a Compare May 8, 2026 13:18
Base automatically changed from feature/openid4vci-v1 to master May 11, 2026 12:08
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.

Align OpenID4VCI client with implementers draft

2 participants