diff --git a/auth/oauth/types.go b/auth/oauth/types.go index c0a6d769d2..902116baa5 100644 --- a/auth/oauth/types.go +++ b/auth/oauth/types.go @@ -21,9 +21,10 @@ package oauth import ( "encoding/json" + "net/url" + "github.com/lestrrat-go/jwx/v2/jwk" "github.com/nuts-foundation/nuts-node/core" - "net/url" ) // this file contains constants, variables and helper functions for OAuth related code @@ -382,7 +383,7 @@ type OAuthClientMetadata struct { /*********** OpenID4VCI ***********/ // CredentialOfferEndpoint contains a URL where the pre-authorized_code flow offers a credential. - // https://openid.bitbucket.io/connect/openid-4-verifiable-credential-issuance-1_0.html#name-client-metadata + // https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-client-metadata // TODO: openid4vci duplicate. Also defined on /.well-known/openid-credential-wallet to be /n2n/identity/{did}/openid4vci/credential_offer CredentialOfferEndpoint string `json:"credential_offer_endpoint,omitempty"` diff --git a/vcr/issuer/openid.go b/vcr/issuer/openid.go index 423cb0cf03..1a46f5d3f3 100644 --- a/vcr/issuer/openid.go +++ b/vcr/issuer/openid.go @@ -24,6 +24,12 @@ import ( "encoding/json" "errors" "fmt" + "io/fs" + "net/http" + "os" + "path/filepath" + "time" + "github.com/google/uuid" "github.com/lestrrat-go/jwx/v2/jws" "github.com/lestrrat-go/jwx/v2/jwt" @@ -37,11 +43,6 @@ import ( "github.com/nuts-foundation/nuts-node/vcr/log" "github.com/nuts-foundation/nuts-node/vcr/openid4vci" "github.com/nuts-foundation/nuts-node/vdr/resolver" - "io/fs" - "net/http" - "os" - "path/filepath" - "time" ) // Flow is an active OpenID4VCI credential issuance flow. @@ -107,14 +108,14 @@ func NewOpenIDHandler(issuerDID did.DID, issuerIdentifierURL string, definitions } type openidHandler struct { - issuerIdentifierURL string - issuerDID did.DID - definitionsDIR string - credentialsSupported []map[string]interface{} - keyResolver resolver.KeyResolver - store OpenIDStore - walletClientCreator func(ctx context.Context, httpClient core.HTTPRequestDoer, walletMetadataURL string) (openid4vci.WalletAPIClient, error) - httpClient core.HTTPRequestDoer + issuerIdentifierURL string + issuerDID did.DID + definitionsDIR string + credentialConfigurationsSupported map[string]interface{} + keyResolver resolver.KeyResolver + store OpenIDStore + walletClientCreator func(ctx context.Context, httpClient core.HTTPRequestDoer, walletMetadataURL string) (openid4vci.WalletAPIClient, error) + httpClient core.HTTPRequestDoer } func (i *openidHandler) Metadata() openid4vci.CredentialIssuerMetadata { @@ -123,8 +124,8 @@ func (i *openidHandler) Metadata() openid4vci.CredentialIssuerMetadata { CredentialEndpoint: core.JoinURLPaths(i.issuerIdentifierURL, "/openid4vci/credential"), } - // deepcopy the i.credentialsSupported slice to prevent concurrent access to the slice. - metadata.CredentialsSupported = deepcopy(i.credentialsSupported) + // deepcopy the i.credentialConfigurationsSupported map to prevent concurrent access to the map. + metadata.CredentialConfigurationsSupported = deepcopy(i.credentialConfigurationsSupported) return metadata } @@ -443,8 +444,9 @@ func (i *openidHandler) createOffer(ctx context.Context, credential vc.Verifiabl } func (i *openidHandler) loadCredentialDefinitions() error { + i.credentialConfigurationsSupported = make(map[string]interface{}) - // retrieve the definitions from assets and add to the list of CredentialsSupported + // retrieve the definitions from assets and add to the map of CredentialConfigurationsSupported definitionsDir, err := assets.FS.ReadDir("definitions") if err != nil { return err @@ -459,7 +461,9 @@ func (i *openidHandler) loadCredentialDefinitions() error { if err != nil { return err } - i.credentialsSupported = append(i.credentialsSupported, definitionMap) + // Use the filename (without extension) as the credential configuration ID + configID := definition.Name()[:len(definition.Name())-len(filepath.Ext(definition.Name()))] + i.credentialConfigurationsSupported[configID] = definitionMap } // now add all credential definition from config.DefinitionsDIR @@ -478,7 +482,9 @@ func (i *openidHandler) loadCredentialDefinitions() error { if err != nil { return fmt.Errorf("failed to parse credential definition from %s: %w", path, err) } - i.credentialsSupported = append(i.credentialsSupported, definitionMap) + // Use the filename (without extension) as the credential configuration ID + configID := d.Name()[:len(d.Name())-len(filepath.Ext(d.Name()))] + i.credentialConfigurationsSupported[configID] = definitionMap } return nil }) @@ -487,12 +493,14 @@ func (i *openidHandler) loadCredentialDefinitions() error { return err } -func deepcopy(src []map[string]interface{}) []map[string]interface{} { - dst := make([]map[string]interface{}, len(src)) - for i := range src { - dst[i] = make(map[string]interface{}) - for k, v := range src[i] { - dst[i][k] = v +func deepcopy(src map[string]interface{}) map[string]interface{} { + dst := make(map[string]interface{}, len(src)) + for k, v := range src { + // Deep copy nested maps if needed + if nestedMap, ok := v.(map[string]interface{}); ok { + dst[k] = deepcopy(nestedMap) + } else { + dst[k] = v } } return dst diff --git a/vcr/issuer/openid_test.go b/vcr/issuer/openid_test.go index f81e25baca..0249a4d3c8 100644 --- a/vcr/issuer/openid_test.go +++ b/vcr/issuer/openid_test.go @@ -21,6 +21,10 @@ package issuer import ( "context" "errors" + "net/http" + "testing" + "time" + ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" @@ -33,9 +37,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" - "net/http" - "testing" - "time" ) var issuerDID = did.MustParseDID("did:nuts:issuer") @@ -67,7 +68,7 @@ func TestNew(t *testing.T) { iss, err := NewOpenIDHandler(issuerDID, issuerIdentifier, "./test/valid", nil, nil, storage.NewTestInMemorySessionDatabase(t)) require.NoError(t, err) - assert.Len(t, iss.(*openidHandler).credentialsSupported, 3) + assert.Len(t, iss.(*openidHandler).credentialConfigurationsSupported, 3) }) t.Run("error - invalid json", func(t *testing.T) { @@ -93,10 +94,14 @@ func Test_memoryIssuer_Metadata(t *testing.T) { assert.Equal(t, "https://example.com/did:nuts:issuer", metadata.CredentialIssuer) assert.Equal(t, "https://example.com/did:nuts:issuer/openid4vci/credential", metadata.CredentialEndpoint) - require.Len(t, metadata.CredentialsSupported, 3) - assert.Equal(t, "ldp_vc", metadata.CredentialsSupported[0]["format"]) - require.Len(t, metadata.CredentialsSupported[0]["cryptographic_binding_methods_supported"], 1) - assert.Equal(t, metadata.CredentialsSupported[0]["credential_definition"], + require.Len(t, metadata.CredentialConfigurationsSupported, 3) + // Check that NutsAuthorizationCredential is present as a configuration ID + authzConfig, ok := metadata.CredentialConfigurationsSupported["NutsAuthorizationCredential"] + require.True(t, ok, "NutsAuthorizationCredential should be in credential_configurations_supported") + authzConfigMap := authzConfig.(map[string]interface{}) + assert.Equal(t, "ldp_vc", authzConfigMap["format"]) + require.Len(t, authzConfigMap["cryptographic_binding_methods_supported"], 1) + assert.Equal(t, authzConfigMap["credential_definition"], map[string]interface{}{ "@context": []interface{}{"https://www.w3.org/2018/credentials/v1", "https://www.nuts.nl/credentials/v1"}, "type": []interface{}{"VerifiableCredential", "NutsAuthorizationCredential"}, diff --git a/vcr/openid4vci/types.go b/vcr/openid4vci/types.go index e5d030b005..532be947b9 100644 --- a/vcr/openid4vci/types.go +++ b/vcr/openid4vci/types.go @@ -21,8 +21,9 @@ package openid4vci import ( - ssi "github.com/nuts-foundation/go-did" "time" + + ssi "github.com/nuts-foundation/go-did" ) // PreAuthorizedCodeGrant is the grant type used for pre-authorized code grant from the OpenID4VCI specification. @@ -62,8 +63,9 @@ type CredentialIssuerMetadata struct { // CredentialEndpoint defines where the wallet can send a request to retrieve a credential. CredentialEndpoint string `json:"credential_endpoint"` - // CredentialsSupported defines metadata about which credential types the credential issuer can issue. - CredentialsSupported []map[string]interface{} `json:"credentials_supported"` + // CredentialConfigurationsSupported defines metadata about the credential configurations supported by the credential issuer. + // This replaces credentials_supported from draft versions. + CredentialConfigurationsSupported map[string]interface{} `json:"credential_configurations_supported"` } // OAuth2ClientMetadata defines the OAuth2 Client Metadata, extended with OpenID4VCI parameters. diff --git a/vcr/openid4vci/validators.go b/vcr/openid4vci/validators.go index b9f854fbb0..418d013c7c 100644 --- a/vcr/openid4vci/validators.go +++ b/vcr/openid4vci/validators.go @@ -20,6 +20,7 @@ package openid4vci import ( "errors" + ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/vc" ) @@ -49,7 +50,7 @@ func (cd *CredentialDefinition) Validate(isOffer bool) error { // CredentialDefinition is assumed to be valid, see ValidateCredentialDefinition. func ValidateDefinitionWithCredential(credential vc.VerifiableCredential, definition CredentialDefinition) error { // From spec: When the format value is ldp_vc, ..., including credential_definition object, MUST NOT be processed using JSON-LD rules. - // https://openid.bitbucket.io/connect/editors-draft/openid-4-verifiable-credential-issuance-1_0.html#name-format-identifier-2 + // https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-format-profiles // compare contexts. The credential may contain extra contexts for signatures or proofs if len(credential.Context) < len(definition.Context) || !isSubset(credential.Context, definition.Context) {