diff --git a/auth/api/iam/api.go b/auth/api/iam/api.go index 339d4b0086..bedbba113d 100644 --- a/auth/api/iam/api.go +++ b/auth/api/iam/api.go @@ -773,8 +773,15 @@ func (r Wrapper) RequestServiceAccessToken(ctx context.Context, request RequestS if request.Body.TokenType != nil && strings.EqualFold(string(*request.Body.TokenType), AccessTokenTypeBearer) { useDPoP = false } + // Extract credential_selection from request. + // nil is safe here: downstream code only reads via len() and range. + var credentialSelection map[string]string + if request.Body.CredentialSelection != nil { + credentialSelection = *request.Body.CredentialSelection + } + clientID := r.subjectToBaseURL(request.SubjectID) - tokenResult, err := r.auth.IAMClient().RequestRFC021AccessToken(ctx, clientID.String(), request.SubjectID, request.Body.AuthorizationServer, request.Body.Scope, useDPoP, credentials) + tokenResult, err := r.auth.IAMClient().RequestRFC021AccessToken(ctx, clientID.String(), request.SubjectID, request.Body.AuthorizationServer, request.Body.Scope, useDPoP, credentials, credentialSelection) if err != nil { // this can be an internal server error, a 400 oauth error or a 412 precondition failed if the wallet does not contain the required credentials return nil, err diff --git a/auth/api/iam/api_test.go b/auth/api/iam/api_test.go index 35efca16da..5be4de20e1 100644 --- a/auth/api/iam/api_test.go +++ b/auth/api/iam/api_test.go @@ -353,7 +353,7 @@ func TestWrapper_HandleAuthorizeRequest(t *testing.T) { ctx.iamClient.EXPECT().ClientMetadata(gomock.Any(), "https://example.com/.well-known/authorization-server/oauth2/verifier").Return(&clientMetadata, nil) pdEndpoint := "https://example.com/oauth2/verifier/presentation_definition?scope=test" ctx.iamClient.EXPECT().PresentationDefinition(gomock.Any(), pdEndpoint).Return(&pe.PresentationDefinition{}, nil) - ctx.wallet.EXPECT().BuildSubmission(gomock.Any(), []did.DID{holderDID}, nil, pe.PresentationDefinition{}, gomock.Any()).Return(&vc.VerifiablePresentation{}, &pe.PresentationSubmission{}, nil) + ctx.wallet.EXPECT().BuildSubmission(gomock.Any(), []did.DID{holderDID}, nil, pe.PresentationDefinition{}, nil, gomock.Any()).Return(&vc.VerifiablePresentation{}, &pe.PresentationSubmission{}, nil) ctx.iamClient.EXPECT().PostAuthorizationResponse(gomock.Any(), vc.VerifiablePresentation{}, pe.PresentationSubmission{}, "https://example.com/oauth2/verifier/response", "state").Return("https://example.com/iam/holder/redirect", nil) res, err := ctx.client.HandleAuthorizeRequest(callCtx, HandleAuthorizeRequestRequestObject{ @@ -886,7 +886,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) { request := RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body} request.Params.CacheControl = to.Ptr("no-cache") // Initial call to populate cache - ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil).Return(response, nil).Times(2) + ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil, nil).Return(response, nil).Times(2) token, err := ctx.client.RequestServiceAccessToken(nil, request) // Test call to check cache is bypassed @@ -907,7 +907,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) { TokenType: "Bearer", ExpiresIn: to.Ptr(900), } - ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil).Return(response, nil) + ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil, nil).Return(response, nil) token, err := ctx.client.RequestServiceAccessToken(nil, request) @@ -946,7 +946,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) { t.Run("cache expired", func(t *testing.T) { cacheKey := accessTokenRequestCacheKey(request) _ = ctx.client.accessTokenCache().Delete(cacheKey) - ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil).Return(&oauth.TokenResponse{AccessToken: "other"}, nil) + ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil, nil).Return(&oauth.TokenResponse{AccessToken: "other"}, nil) otherToken, err := ctx.client.RequestServiceAccessToken(nil, request) @@ -963,7 +963,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) { Scope: "first second", TokenType: &tokenTypeBearer, } - ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", false, nil).Return(&oauth.TokenResponse{}, nil) + ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", false, nil, nil).Return(&oauth.TokenResponse{}, nil) _, err := ctx.client.RequestServiceAccessToken(nil, RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body}) @@ -972,7 +972,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) { t.Run("ok with expired cache by ttl", func(t *testing.T) { ctx := newTestClient(t) request := RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body} - ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil).Return(&oauth.TokenResponse{ExpiresIn: to.Ptr(5)}, nil) + ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil, nil).Return(&oauth.TokenResponse{ExpiresIn: to.Ptr(5)}, nil) _, err := ctx.client.RequestServiceAccessToken(nil, request) @@ -981,7 +981,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) { }) t.Run("error - no matching credentials", func(t *testing.T) { ctx := newTestClient(t) - ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil).Return(nil, pe.ErrNoCredentials) + ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil, nil).Return(nil, pe.ErrNoCredentials) _, err := ctx.client.RequestServiceAccessToken(nil, RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body}) @@ -997,8 +997,8 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) { ctx.client.storageEngine = mockStorage request := RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body} - ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil).Return(&oauth.TokenResponse{AccessToken: "first"}, nil) - ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil).Return(&oauth.TokenResponse{AccessToken: "second"}, nil) + ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil, nil).Return(&oauth.TokenResponse{AccessToken: "first"}, nil) + ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil, nil).Return(&oauth.TokenResponse{AccessToken: "second"}, nil) token1, err := ctx.client.RequestServiceAccessToken(nil, request) require.NoError(t, err) @@ -1023,7 +1023,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) { {ID: to.Ptr(ssi.MustParseURI("not empty"))}, } request := RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body} - ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, *body.Credentials).Return(response, nil) + ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, *body.Credentials, nil).Return(response, nil) _, err := ctx.client.RequestServiceAccessToken(nil, request) diff --git a/auth/api/iam/generated.go b/auth/api/iam/generated.go index 5dbe21544d..859cff5efd 100644 --- a/auth/api/iam/generated.go +++ b/auth/api/iam/generated.go @@ -134,6 +134,15 @@ type ServiceAccessTokenRequest struct { // used to locate the OAuth2 Authorization Server metadata. AuthorizationServer string `json:"authorization_server"` + // CredentialSelection Optional key-value mapping for credential selection when the wallet contains multiple + // credentials matching a single input descriptor. Each key must match a field id declared + // in the Presentation Definition's input descriptor constraints. The value narrows the + // match to credentials where that field equals the given value. + // + // The selection must narrow to exactly one credential per input descriptor. + // Zero matches or multiple matches will result in an error. + CredentialSelection *map[string]string `json:"credential_selection,omitempty"` + // Credentials Additional credentials to present (if required by the authorizer), in addition to those in the requester's wallet. // They must be in the form of a Verifiable Credential in JSON form. // The serialized form (JWT or JSON-LD) in the resulting Verifiable Presentation depends on the capability of the authorizing party. diff --git a/auth/api/iam/openid4vp.go b/auth/api/iam/openid4vp.go index 53868ed600..fe6bb045ec 100644 --- a/auth/api/iam/openid4vp.go +++ b/auth/api/iam/openid4vp.go @@ -318,7 +318,7 @@ func (r Wrapper) handleAuthorizeRequestFromVerifier(ctx context.Context, subject map[did.DID][]vc.VerifiableCredential{userSession.Wallet.DID: userSession.Wallet.Credentials}, ) } - vp, submission, err := targetWallet.BuildSubmission(ctx, []did.DID{walletDID}, nil, *presentationDefinition, buildParams) + vp, submission, err := targetWallet.BuildSubmission(ctx, []did.DID{walletDID}, nil, *presentationDefinition, nil, buildParams) if err != nil { if errors.Is(err, pe.ErrNoCredentials) { return r.sendAndHandleDirectPostError(ctx, oauth.OAuth2Error{Code: oauth.InvalidRequest, Description: fmt.Sprintf("wallet could not fulfill requirements (PD ID: %s, wallet: %s): %s", presentationDefinition.Id, walletDID, err.Error())}, responseURI, state) diff --git a/auth/api/iam/openid4vp_test.go b/auth/api/iam/openid4vp_test.go index bcc4137bdc..1f0135363c 100644 --- a/auth/api/iam/openid4vp_test.go +++ b/auth/api/iam/openid4vp_test.go @@ -345,7 +345,7 @@ func TestWrapper_handleAuthorizeRequestFromVerifier(t *testing.T) { putState(ctx, "state", authzCodeSession) ctx.iamClient.EXPECT().ClientMetadata(gomock.Any(), "https://example.com/.well-known/authorization-server/iam/verifier").Return(&clientMetadata, nil) ctx.iamClient.EXPECT().PresentationDefinition(gomock.Any(), pdEndpoint).Return(&pe.PresentationDefinition{}, nil) - ctx.wallet.EXPECT().BuildSubmission(gomock.Any(), []did.DID{holderDID}, nil, pe.PresentationDefinition{}, gomock.Any()).Return(nil, nil, assert.AnError) + ctx.wallet.EXPECT().BuildSubmission(gomock.Any(), []did.DID{holderDID}, nil, pe.PresentationDefinition{}, nil, gomock.Any()).Return(nil, nil, assert.AnError) expectPostError(t, ctx, oauth.ServerError, assert.AnError.Error(), responseURI, "state") _, err := ctx.client.handleAuthorizeRequestFromVerifier(httpRequestCtx, holderSubjectID, params, pe.WalletOwnerOrganization) @@ -358,7 +358,7 @@ func TestWrapper_handleAuthorizeRequestFromVerifier(t *testing.T) { putState(ctx, "state", authzCodeSession) ctx.iamClient.EXPECT().ClientMetadata(gomock.Any(), "https://example.com/.well-known/authorization-server/iam/verifier").Return(&clientMetadata, nil) ctx.iamClient.EXPECT().PresentationDefinition(gomock.Any(), pdEndpoint).Return(&pe.PresentationDefinition{}, nil) - ctx.wallet.EXPECT().BuildSubmission(gomock.Any(), []did.DID{holderDID}, nil, pe.PresentationDefinition{}, gomock.Any()).Return(nil, nil, pe.ErrNoCredentials) + ctx.wallet.EXPECT().BuildSubmission(gomock.Any(), []did.DID{holderDID}, nil, pe.PresentationDefinition{}, nil, gomock.Any()).Return(nil, nil, pe.ErrNoCredentials) expectPostError(t, ctx, oauth.InvalidRequest, "wallet could not fulfill requirements (PD ID: , wallet: did:web:example.com:iam:holder): missing credentials", responseURI, "state") _, err := ctx.client.handleAuthorizeRequestFromVerifier(httpRequestCtx, holderSubjectID, params, pe.WalletOwnerOrganization) diff --git a/auth/client/iam/interface.go b/auth/client/iam/interface.go index 5016708d9d..5ccf9caaf5 100644 --- a/auth/client/iam/interface.go +++ b/auth/client/iam/interface.go @@ -44,8 +44,10 @@ type Client interface { // PresentationDefinition returns the presentation definition from the given endpoint. PresentationDefinition(ctx context.Context, endpoint string) (*pe.PresentationDefinition, error) // RequestRFC021AccessToken is called by the local EHR node to request an access token from a remote OAuth2 Authorization Server using Nuts RFC021. + // credentials are additional VCs to include alongside wallet-stored credentials. + // credentialSelection maps PD field IDs to expected values to disambiguate when multiple credentials match an input descriptor. RequestRFC021AccessToken(ctx context.Context, clientID string, subjectDID string, authServerURL string, scopes string, useDPoP bool, - credentials []vc.VerifiableCredential) (*oauth.TokenResponse, error) + credentials []vc.VerifiableCredential, credentialSelection map[string]string) (*oauth.TokenResponse, error) // OpenIdCredentialIssuerMetadata returns the metadata of the remote credential issuer. // oauthIssuer is the URL of the issuer as specified by RFC 8414 (OAuth 2.0 Authorization Server Metadata). diff --git a/auth/client/iam/mock.go b/auth/client/iam/mock.go index add1a059cd..b6ad933a61 100644 --- a/auth/client/iam/mock.go +++ b/auth/client/iam/mock.go @@ -194,18 +194,18 @@ func (mr *MockClientMockRecorder) RequestObjectByPost(ctx, requestURI, walletMet } // RequestRFC021AccessToken mocks base method. -func (m *MockClient) RequestRFC021AccessToken(ctx context.Context, clientID, subjectDID, authServerURL, scopes string, useDPoP bool, credentials []vc.VerifiableCredential) (*oauth.TokenResponse, error) { +func (m *MockClient) RequestRFC021AccessToken(ctx context.Context, clientID, subjectDID, authServerURL, scopes string, useDPoP bool, credentials []vc.VerifiableCredential, credentialSelection map[string]string) (*oauth.TokenResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RequestRFC021AccessToken", ctx, clientID, subjectDID, authServerURL, scopes, useDPoP, credentials) + ret := m.ctrl.Call(m, "RequestRFC021AccessToken", ctx, clientID, subjectDID, authServerURL, scopes, useDPoP, credentials, credentialSelection) ret0, _ := ret[0].(*oauth.TokenResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // RequestRFC021AccessToken indicates an expected call of RequestRFC021AccessToken. -func (mr *MockClientMockRecorder) RequestRFC021AccessToken(ctx, clientID, subjectDID, authServerURL, scopes, useDPoP, credentials any) *gomock.Call { +func (mr *MockClientMockRecorder) RequestRFC021AccessToken(ctx, clientID, subjectDID, authServerURL, scopes, useDPoP, credentials, credentialSelection any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestRFC021AccessToken", reflect.TypeOf((*MockClient)(nil).RequestRFC021AccessToken), ctx, clientID, subjectDID, authServerURL, scopes, useDPoP, credentials) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestRFC021AccessToken", reflect.TypeOf((*MockClient)(nil).RequestRFC021AccessToken), ctx, clientID, subjectDID, authServerURL, scopes, useDPoP, credentials, credentialSelection) } // VerifiableCredentials mocks base method. diff --git a/auth/client/iam/openid4vp.go b/auth/client/iam/openid4vp.go index 0f9e370601..bf7f8fef68 100644 --- a/auth/client/iam/openid4vp.go +++ b/auth/client/iam/openid4vp.go @@ -235,7 +235,7 @@ func (c *OpenID4VPClient) AccessToken(ctx context.Context, code string, tokenEnd } func (c *OpenID4VPClient) RequestRFC021AccessToken(ctx context.Context, clientID string, subjectID string, authServerURL string, scopes string, - useDPoP bool, additionalCredentials []vc.VerifiableCredential) (*oauth.TokenResponse, error) { + useDPoP bool, additionalCredentials []vc.VerifiableCredential, credentialSelection map[string]string) (*oauth.TokenResponse, error) { iamClient := c.httpClient metadata, err := c.AuthorizationServerMetadata(ctx, authServerURL) if err != nil { @@ -296,7 +296,7 @@ func (c *OpenID4VPClient) RequestRFC021AccessToken(ctx context.Context, clientID additionalWalletCredentials[subjectDID] = append(additionalWalletCredentials[subjectDID], credential.AutoCorrectSelfAttestedCredential(curr, subjectDID)) } } - vp, submission, err := c.wallet.BuildSubmission(ctx, subjectDIDs, additionalWalletCredentials, *presentationDefinition, params) + vp, submission, err := c.wallet.BuildSubmission(ctx, subjectDIDs, additionalWalletCredentials, *presentationDefinition, credentialSelection, params) if err != nil { return nil, err } diff --git a/auth/client/iam/openid4vp_test.go b/auth/client/iam/openid4vp_test.go index e5c4ef6840..c14ff83a37 100644 --- a/auth/client/iam/openid4vp_test.go +++ b/auth/client/iam/openid4vp_test.go @@ -251,9 +251,9 @@ func TestRelyingParty_RequestRFC021AccessToken(t *testing.T) { t.Run("fulfills the Presentation Definition", func(t *testing.T) { ctx := createClientServerTestContext(t) ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), subjectID).Return([]did.DID{primaryWalletDID, secondaryWalletDID}, nil) - ctx.wallet.EXPECT().BuildSubmission(gomock.Any(), []did.DID{primaryWalletDID, secondaryWalletDID}, gomock.Any(), gomock.Any(), gomock.Any()).Return(createdVP, &pe.PresentationSubmission{}, nil) + ctx.wallet.EXPECT().BuildSubmission(gomock.Any(), []did.DID{primaryWalletDID, secondaryWalletDID}, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(createdVP, &pe.PresentationSubmission{}, nil) - response, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, false, nil) + response, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, false, nil, nil) assert.NoError(t, err) require.NotNil(t, response) @@ -263,9 +263,9 @@ func TestRelyingParty_RequestRFC021AccessToken(t *testing.T) { t.Run("no DID fulfills the Presentation Definition", func(t *testing.T) { ctx := createClientServerTestContext(t) ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), subjectID).Return([]did.DID{primaryWalletDID, secondaryWalletDID}, nil) - ctx.wallet.EXPECT().BuildSubmission(gomock.Any(), []did.DID{primaryWalletDID, secondaryWalletDID}, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil, pe.ErrNoCredentials) + ctx.wallet.EXPECT().BuildSubmission(gomock.Any(), []did.DID{primaryWalletDID, secondaryWalletDID}, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil, pe.ErrNoCredentials) - response, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, false, nil) + response, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, false, nil, nil) assert.ErrorIs(t, err, pe.ErrNoCredentials) assert.Nil(t, response) @@ -275,7 +275,7 @@ func TestRelyingParty_RequestRFC021AccessToken(t *testing.T) { ctx.authzServerMetadata.DIDMethodsSupported = []string{"other"} ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), subjectID).Return([]did.DID{primaryWalletDID, secondaryWalletDID}, nil) - response, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, false, nil) + response, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, false, nil, nil) require.Error(t, err) assert.ErrorIs(t, err, ErrPreconditionFailed) @@ -301,8 +301,8 @@ func TestRelyingParty_RequestRFC021AccessToken(t *testing.T) { }, }, } - ctx.wallet.EXPECT().BuildSubmission(gomock.Any(), []did.DID{primaryWalletDID, secondaryWalletDID}, gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(_ context.Context, _ []did.DID, additionalCredentials map[did.DID][]vc.VerifiableCredential, _ pe.PresentationDefinition, _ holder.BuildParams) (*vc.VerifiablePresentation, *pe.PresentationSubmission, error) { + ctx.wallet.EXPECT().BuildSubmission(gomock.Any(), []did.DID{primaryWalletDID, secondaryWalletDID}, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(_ context.Context, _ []did.DID, additionalCredentials map[did.DID][]vc.VerifiableCredential, _ pe.PresentationDefinition, _ map[string]string, _ holder.BuildParams) (*vc.VerifiablePresentation, *pe.PresentationSubmission, error) { // Assert self-attested credentials require.Len(t, additionalCredentials, 2) require.Len(t, additionalCredentials[primaryWalletDID], 1) @@ -312,7 +312,7 @@ func TestRelyingParty_RequestRFC021AccessToken(t *testing.T) { return createdVP, &pe.PresentationSubmission{}, nil }) - response, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, false, credentials) + response, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, false, credentials, nil) assert.NoError(t, err) require.NotNil(t, response) @@ -324,9 +324,9 @@ func TestRelyingParty_RequestRFC021AccessToken(t *testing.T) { ctx.keyResolver.EXPECT().ResolveKey(primaryWalletDID, nil, resolver.NutsSigningKeyType).Return(primaryKID, nil, nil) ctx.jwtSigner.EXPECT().SignDPoP(context.Background(), gomock.Any(), primaryKID).Return("dpop", nil) ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), subjectID).Return([]did.DID{primaryWalletDID, secondaryWalletDID}, nil) - ctx.wallet.EXPECT().BuildSubmission(gomock.Any(), []did.DID{primaryWalletDID, secondaryWalletDID}, gomock.Any(), gomock.Any(), gomock.Any()).Return(createdVP, &pe.PresentationSubmission{}, nil) + ctx.wallet.EXPECT().BuildSubmission(gomock.Any(), []did.DID{primaryWalletDID, secondaryWalletDID}, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(createdVP, &pe.PresentationSubmission{}, nil) - response, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, true, nil) + response, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, true, nil, nil) assert.NoError(t, err) require.NotNil(t, response) @@ -346,9 +346,9 @@ func TestRelyingParty_RequestRFC021AccessToken(t *testing.T) { _, _ = writer.Write(oauthErrorBytes) } ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), subjectID).Return([]did.DID{primaryWalletDID, secondaryWalletDID}, nil) - ctx.wallet.EXPECT().BuildSubmission(gomock.Any(), []did.DID{primaryWalletDID, secondaryWalletDID}, gomock.Any(), gomock.Any(), gomock.Any()).Return(createdVP, &pe.PresentationSubmission{}, nil) + ctx.wallet.EXPECT().BuildSubmission(gomock.Any(), []did.DID{primaryWalletDID, secondaryWalletDID}, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(createdVP, &pe.PresentationSubmission{}, nil) - _, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, false, nil) + _, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, false, nil, nil) require.Error(t, err) oauthError, ok := err.(oauth.OAuth2Error) @@ -365,7 +365,7 @@ func TestRelyingParty_RequestRFC021AccessToken(t *testing.T) { return } - _, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, false, nil) + _, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, false, nil, nil) require.Error(t, err) assert.True(t, errors.As(err, &oauth.OAuth2Error{})) @@ -375,7 +375,7 @@ func TestRelyingParty_RequestRFC021AccessToken(t *testing.T) { ctx := createClientServerTestContext(t) ctx.metadata = nil - _, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, false, nil) + _, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, false, nil, nil) require.Error(t, err) assert.ErrorIs(t, err, ErrInvalidClientCall) @@ -389,7 +389,7 @@ func TestRelyingParty_RequestRFC021AccessToken(t *testing.T) { _, _ = writer.Write([]byte("{")) } - _, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, false, nil) + _, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, false, nil, nil) require.Error(t, err) assert.ErrorIs(t, err, ErrBadGateway) @@ -398,9 +398,9 @@ func TestRelyingParty_RequestRFC021AccessToken(t *testing.T) { t.Run("error - failed to build vp", func(t *testing.T) { ctx := createClientServerTestContext(t) ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), subjectID).Return([]did.DID{primaryWalletDID, secondaryWalletDID}, nil) - ctx.wallet.EXPECT().BuildSubmission(gomock.Any(), []did.DID{primaryWalletDID, secondaryWalletDID}, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil, assert.AnError) + ctx.wallet.EXPECT().BuildSubmission(gomock.Any(), []did.DID{primaryWalletDID, secondaryWalletDID}, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil, assert.AnError) - _, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, false, nil) + _, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, false, nil, nil) assert.Error(t, err) }) diff --git a/docs/_static/auth/v2.yaml b/docs/_static/auth/v2.yaml index c032a1ff64..76d99cb7be 100644 --- a/docs/_static/auth/v2.yaml +++ b/docs/_static/auth/v2.yaml @@ -443,6 +443,23 @@ components: } } ] + credential_selection: + type: object + description: | + Optional key-value mapping for credential selection when the wallet contains multiple + credentials matching a single input descriptor. Each key must match a field ID declared + in the Presentation Definition's input descriptor constraints. The value narrows the + match to credentials where that field equals the given value. + + The selection must narrow to exactly one credential per input descriptor. + Zero matches or multiple matches will result in an error. + + When omitted and multiple credentials match an input descriptor, + the first matching credential is used. + additionalProperties: + type: string + example: + patient_id: "123456789" token_type: type: string description: "The type of access token that is preferred, default: DPoP" diff --git a/e2e-tests/browser/client/iam/generated.go b/e2e-tests/browser/client/iam/generated.go index 9ed0f8cbac..51c1f31772 100644 --- a/e2e-tests/browser/client/iam/generated.go +++ b/e2e-tests/browser/client/iam/generated.go @@ -128,6 +128,15 @@ type ServiceAccessTokenRequest struct { // used to locate the OAuth2 Authorization Server metadata. AuthorizationServer string `json:"authorization_server"` + // CredentialSelection Optional key-value mapping for credential selection when the wallet contains multiple + // credentials matching a single input descriptor. Each key must match a field id declared + // in the Presentation Definition's input descriptor constraints. The value narrows the + // match to credentials where that field equals the given value. + // + // The selection must narrow to exactly one credential per input descriptor. + // Zero matches or multiple matches will result in an error. + CredentialSelection *map[string]string `json:"credential_selection,omitempty"` + // Credentials Additional credentials to present (if required by the authorizer), in addition to those in the requester's wallet. // They must be in the form of a Verifiable Credential in JSON form. // The serialized form (JWT or JSON-LD) in the resulting Verifiable Presentation depends on the capability of the authorizing party. diff --git a/e2e-tests/oauth-flow/rfc021/do-test.sh b/e2e-tests/oauth-flow/rfc021/do-test.sh index 2eb90fa14e..c4dba8743b 100755 --- a/e2e-tests/oauth-flow/rfc021/do-test.sh +++ b/e2e-tests/oauth-flow/rfc021/do-test.sh @@ -207,6 +207,76 @@ else exitWithDockerLogs 1 fi +echo "-------------------------------------------" +echo "Test credential_selection (named params)..." +echo "-------------------------------------------" +# Issue a second NutsOrganizationCredential with a different org name +REQUEST="{\"type\":\"NutsOrganizationCredential\",\"issuer\":\"${VENDOR_B_DID}\", \"credentialSubject\": {\"id\":\"${VENDOR_B_DID}\", \"organization\":{\"name\":\"Second Org B.V.\", \"city\":\"Othertown\"}},\"withStatusList2021Revocation\": true}" +VENDOR_B_CREDENTIAL_2=$(echo "$REQUEST" | curl -X POST --data-binary @- http://localhost:28081/internal/vcr/v2/issuer/vc -H "Content-Type:application/json") +if echo "$VENDOR_B_CREDENTIAL_2" | grep -q "VerifiableCredential"; then + echo "Second NutsOrganizationCredential issued" +else + echo "FAILED: Could not issue second NutsOrganizationCredential" 1>&2 + echo "$VENDOR_B_CREDENTIAL_2" + exitWithDockerLogs 1 +fi + +# Store second credential in wallet +RESPONSE=$(echo "$VENDOR_B_CREDENTIAL_2" | curl -X POST --data-binary @- http://localhost:28081/internal/vcr/v2/holder/vendorB/vc -H "Content-Type:application/json") +if [ "$RESPONSE" == "" ]; then + echo "Second VC stored in wallet" +else + echo "FAILED: Could not store second NutsOrganizationCredential in wallet" 1>&2 + echo "$RESPONSE" + exitWithDockerLogs 1 +fi + +# Request access token with credential_selection to select the second org credential +REQUEST=$( +cat <<'EOF' +{ + "authorization_server": "https://nodeA/oauth2/vendorA", + "scope": "test", + "credential_selection": { + "organization_name": "Second Org B.V." + }, + "credentials": [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://nuts.nl/credentials/v1" + ], + "type": ["VerifiableCredential", "NutsEmployeeCredential"], + "credentialSubject": { + "name": "Jane Doe", + "roleName": "Nurse", + "identifier": "654321" + } + } + ] +} +EOF +) +RESPONSE=$(echo "$REQUEST" | curl -X POST -s --data-binary @- http://localhost:28081/internal/auth/v2/vendorB/request-service-access-token -H "Content-Type: application/json" -H "Cache-Control: no-cache") +if echo "$RESPONSE" | grep -q "access_token"; then + echo "credential_selection: access token obtained successfully" +else + echo "FAILED: Could not get access token with credential_selection" 1>&2 + echo "$RESPONSE" + exitWithDockerLogs 1 +fi + +# Verify introspection contains the correct org (Second Org B.V.) +SELECTION_ACCESS_TOKEN=$(echo "$RESPONSE" | sed -E 's/.*"access_token":"([^"]*).*/\1/') +RESPONSE=$(curl -X POST -s --data "token=$SELECTION_ACCESS_TOKEN" http://localhost:18081/internal/auth/v2/accesstoken/introspect_extended) +if echo "$RESPONSE" | grep -q "Second Org B.V."; then + echo "credential_selection: correct organization selected" +else + echo "FAILED: credential_selection did not select the correct organization" 1>&2 + echo "$RESPONSE" + exitWithDockerLogs 1 +fi + echo "------------------------------------" echo "Revoking credential..." echo "------------------------------------" diff --git a/e2e-tests/oauth-flow/rfc021/node-A/presentationexchangemapping.json b/e2e-tests/oauth-flow/rfc021/node-A/presentationexchangemapping.json index 4e0cd617c5..31ea2fa6d8 100644 --- a/e2e-tests/oauth-flow/rfc021/node-A/presentationexchangemapping.json +++ b/e2e-tests/oauth-flow/rfc021/node-A/presentationexchangemapping.json @@ -31,6 +31,7 @@ } }, { + "id": "organization_name", "path": [ "$.credentialSubject.organization.name" ], diff --git a/vcr/holder/interface.go b/vcr/holder/interface.go index 0483b17943..5776f30f6f 100644 --- a/vcr/holder/interface.go +++ b/vcr/holder/interface.go @@ -51,7 +51,9 @@ type Wallet interface { // BuildSubmission builds a Verifiable Presentation based on the given presentation definition. // additionalCredentials can be given to have the submission consider extra credentials that are not in the wallet. - BuildSubmission(ctx context.Context, walletDIDs []did.DID, additionalCredentials map[did.DID][]vc.VerifiableCredential, presentationDefinition pe.PresentationDefinition, params BuildParams) (*vc.VerifiablePresentation, *pe.PresentationSubmission, error) + // credentialSelection optionally maps PD field IDs to expected values to disambiguate credential selection + // when multiple credentials match a single input descriptor. + BuildSubmission(ctx context.Context, walletDIDs []did.DID, additionalCredentials map[did.DID][]vc.VerifiableCredential, presentationDefinition pe.PresentationDefinition, credentialSelection map[string]string, params BuildParams) (*vc.VerifiablePresentation, *pe.PresentationSubmission, error) // List returns all credentials in the wallet for the given holder. // If the wallet does not contain any credentials for the given holder, it returns an empty list. diff --git a/vcr/holder/memory_wallet.go b/vcr/holder/memory_wallet.go index 8e466719a0..9adbb3af0f 100644 --- a/vcr/holder/memory_wallet.go +++ b/vcr/holder/memory_wallet.go @@ -27,6 +27,7 @@ import ( "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/crypto" + "github.com/nuts-foundation/nuts-node/vcr/pe" "github.com/nuts-foundation/nuts-node/vdr/resolver" "github.com/piprate/json-gold/ld" @@ -59,7 +60,7 @@ func (m memoryWallet) BuildPresentation(ctx context.Context, credentials []vc.Ve }.buildPresentation(ctx, signerDID, credentials, options) } -func (m memoryWallet) BuildSubmission(ctx context.Context, walletDIDs []did.DID, additionalCredentials map[did.DID][]vc.VerifiableCredential, presentationDefinition pe.PresentationDefinition, params BuildParams) (*vc.VerifiablePresentation, *pe.PresentationSubmission, error) { +func (m memoryWallet) BuildSubmission(ctx context.Context, walletDIDs []did.DID, additionalCredentials map[did.DID][]vc.VerifiableCredential, presentationDefinition pe.PresentationDefinition, credentialSelection map[string]string, params BuildParams) (*vc.VerifiablePresentation, *pe.PresentationSubmission, error) { wallets := make(map[did.DID][]vc.VerifiableCredential) for _, walletDID := range walletDIDs { wallets[walletDID] = m.credentials[walletDID] @@ -71,7 +72,7 @@ func (m memoryWallet) BuildSubmission(ctx context.Context, walletDIDs []did.DID, documentLoader: m.documentLoader, signer: m.signer, keyResolver: m.keyResolver, - }.buildSubmission(ctx, wallets, presentationDefinition, params) + }.buildSubmission(ctx, wallets, presentationDefinition, credentialSelection, params) } func (m memoryWallet) List(_ context.Context, holderDID did.DID) ([]vc.VerifiableCredential, error) { diff --git a/vcr/holder/mock.go b/vcr/holder/mock.go index bd81a024fd..e12b0f8019 100644 --- a/vcr/holder/mock.go +++ b/vcr/holder/mock.go @@ -61,9 +61,9 @@ func (mr *MockWalletMockRecorder) BuildPresentation(ctx, credentials, options, s } // BuildSubmission mocks base method. -func (m *MockWallet) BuildSubmission(ctx context.Context, walletDIDs []did.DID, additionalCredentials map[did.DID][]vc.VerifiableCredential, presentationDefinition pe.PresentationDefinition, params BuildParams) (*vc.VerifiablePresentation, *pe.PresentationSubmission, error) { +func (m *MockWallet) BuildSubmission(ctx context.Context, walletDIDs []did.DID, additionalCredentials map[did.DID][]vc.VerifiableCredential, presentationDefinition pe.PresentationDefinition, credentialSelection map[string]string, params BuildParams) (*vc.VerifiablePresentation, *pe.PresentationSubmission, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "BuildSubmission", ctx, walletDIDs, additionalCredentials, presentationDefinition, params) + ret := m.ctrl.Call(m, "BuildSubmission", ctx, walletDIDs, additionalCredentials, presentationDefinition, credentialSelection, params) ret0, _ := ret[0].(*vc.VerifiablePresentation) ret1, _ := ret[1].(*pe.PresentationSubmission) ret2, _ := ret[2].(error) @@ -71,9 +71,9 @@ func (m *MockWallet) BuildSubmission(ctx context.Context, walletDIDs []did.DID, } // BuildSubmission indicates an expected call of BuildSubmission. -func (mr *MockWalletMockRecorder) BuildSubmission(ctx, walletDIDs, additionalCredentials, presentationDefinition, params any) *gomock.Call { +func (mr *MockWalletMockRecorder) BuildSubmission(ctx, walletDIDs, additionalCredentials, presentationDefinition, credentialSelection, params any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildSubmission", reflect.TypeOf((*MockWallet)(nil).BuildSubmission), ctx, walletDIDs, additionalCredentials, presentationDefinition, params) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildSubmission", reflect.TypeOf((*MockWallet)(nil).BuildSubmission), ctx, walletDIDs, additionalCredentials, presentationDefinition, credentialSelection, params) } // Diagnostics mocks base method. diff --git a/vcr/holder/presenter.go b/vcr/holder/presenter.go index 182f434d41..4c7fda522e 100644 --- a/vcr/holder/presenter.go +++ b/vcr/holder/presenter.go @@ -48,7 +48,7 @@ type presenter struct { } func (p presenter) buildSubmission(ctx context.Context, credentials map[did.DID][]vc.VerifiableCredential, presentationDefinition pe.PresentationDefinition, - params BuildParams) (*vc.VerifiablePresentation, *pe.PresentationSubmission, error) { + credentialSelection map[string]string, params BuildParams) (*vc.VerifiablePresentation, *pe.PresentationSubmission, error) { // match against the wallet's credentials // if there's a match, create a VP and call the token endpoint // If the token endpoint succeeds, return the access token @@ -57,6 +57,15 @@ func (p presenter) buildSubmission(ctx context.Context, credentials map[did.DID] for holderDID, creds := range credentials { builder.AddWallet(holderDID, creds) } + // If credential selection is provided, create a selector that narrows + // credential selection per input descriptor by field ID values. + if len(credentialSelection) > 0 { + selector, err := pe.NewFieldSelector(credentialSelection, presentationDefinition) + if err != nil { + return nil, nil, err + } + builder.SetCredentialSelector(selector) + } // Find supported VP format, matching support from: // - what the local Nuts node supports diff --git a/vcr/holder/presenter_test.go b/vcr/holder/presenter_test.go index efc47e70b8..9671b719b1 100644 --- a/vcr/holder/presenter_test.go +++ b/vcr/holder/presenter_test.go @@ -315,7 +315,7 @@ func TestPresenter_buildSubmission(t *testing.T) { w := presenter{documentLoader: jsonldManager.DocumentLoader(), signer: keyStore, keyResolver: keyResolver} - vp, submission, err := w.buildSubmission(ctx, credentials, presentationDefinition, BuildParams{Audience: verifierDID.String(), Expires: time.Now().Add(time.Second), Format: vpFormats, Nonce: ""}) + vp, submission, err := w.buildSubmission(ctx, credentials, presentationDefinition, nil, BuildParams{Audience: verifierDID.String(), Expires: time.Now().Add(time.Second), Format: vpFormats, Nonce: ""}) assert.NoError(t, err) require.NotNil(t, vp) @@ -327,7 +327,7 @@ func TestPresenter_buildSubmission(t *testing.T) { w := NewSQLWallet(nil, keyStore, nil, jsonldManager, storageEngine) - vp, submission, err := w.BuildSubmission(ctx, []did.DID{nutsWalletDID}, nil, presentationDefinition, BuildParams{Audience: verifierDID.String(), Expires: time.Now().Add(time.Second), Format: vpFormats, Nonce: ""}) + vp, submission, err := w.BuildSubmission(ctx, []did.DID{nutsWalletDID}, nil, presentationDefinition, nil, BuildParams{Audience: verifierDID.String(), Expires: time.Now().Add(time.Second), Format: vpFormats, Nonce: ""}) assert.ErrorIs(t, err, pe.ErrNoCredentials) assert.Nil(t, vp) @@ -339,10 +339,67 @@ func TestPresenter_buildSubmission(t *testing.T) { w := NewSQLWallet(nil, keyStore, testVerifier{}, jsonldManager, storageEngine) _ = w.Put(context.Background(), credentials[nutsWalletDID]...) - _, _, err := w.BuildSubmission(ctx, []did.DID{nutsWalletDID}, nil, presentationDefinition, BuildParams{Audience: verifierDID.String(), DIDMethods: []string{"test"}, Expires: time.Now().Add(time.Second), Format: vpFormats, Nonce: ""}) + _, _, err := w.BuildSubmission(ctx, []did.DID{nutsWalletDID}, nil, presentationDefinition, nil, BuildParams{Audience: verifierDID.String(), DIDMethods: []string{"test"}, Expires: time.Now().Add(time.Second), Format: vpFormats, Nonce: ""}) assert.ErrorIs(t, err, pe.ErrNoCredentials) }) + t.Run("ok - credential_selection narrows to correct credential", func(t *testing.T) { + resetStore(t, storageEngine.GetSQLDatabase()) + ctrl := gomock.NewController(t) + keyResolver := resolver.NewMockKeyResolver(ctrl) + keyResolver.EXPECT().ResolveKey(nutsWalletDID, nil, resolver.NutsSigningKeyType).Return(key.KID, key.PublicKey, nil) + + // Two NutsOrganizationCredentials with different org names, same subject DID + vc1 := test.ValidNutsOrganizationCredential(t) // org name: "Because we care B.V." + vc2JSON := `{ + "@context": ["https://nuts.nl/credentials/v1","https://www.w3.org/2018/credentials/v1","https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json"], + "type": ["NutsOrganizationCredential", "VerifiableCredential"], + "issuer": "did:nuts:CuE3qeFGGLhEAS3gKzhMCeqd1dGa9at5JCbmCfyMU2Ey", + "issuanceDate": "2022-06-01T15:34:40.65319+02:00", + "credentialSubject": {"id": "did:nuts:CuE3qeFGGLhEAS3gKzhMCeqd1dGa9at5JCbmCfyMU2Ey", "organization": {"name": "Second Org B.V.", "city": "Othertown"}} + }` + var vc2 vc.VerifiableCredential + require.NoError(t, vc2.UnmarshalJSON([]byte(vc2JSON))) + + // PD with org_name field ID + pdWithSelection := pe.PresentationDefinition{ + InputDescriptors: []*pe.InputDescriptor{ + { + Id: "org_cred", + Constraints: &pe.Constraints{ + Fields: []pe.Field{ + { + Path: []string{"$.type"}, + Filter: &pe.Filter{Type: "string", Const: to.Ptr("NutsOrganizationCredential")}, + }, + { + Id: to.Ptr("org_name"), + Path: []string{"$.credentialSubject.organization.name"}, + }, + }, + }, + }, + }, + } + + twoOrgCreds := map[did.DID][]vc.VerifiableCredential{ + nutsWalletDID: {vc1, vc2}, + } + w := presenter{documentLoader: jsonldManager.DocumentLoader(), signer: keyStore, keyResolver: keyResolver} + + vp, submission, err := w.buildSubmission(ctx, twoOrgCreds, pdWithSelection, + map[string]string{"org_name": "Second Org B.V."}, + BuildParams{Audience: verifierDID.String(), Expires: time.Now().Add(time.Second), Format: vpFormats, Nonce: ""}) + + require.NoError(t, err) + require.NotNil(t, vp) + require.NotNil(t, submission) + // The VP should contain vc2 (Second Org B.V.), not vc1 + require.Len(t, vp.VerifiableCredential, 1) + subjects := vp.VerifiableCredential[0].CredentialSubject + require.Len(t, subjects, 1) + assert.Equal(t, "Second Org B.V.", subjects[0]["organization"].(map[string]interface{})["name"]) + }) t.Run("ok - empty presentation", func(t *testing.T) { resetStore(t, storageEngine.GetSQLDatabase()) ctrl := gomock.NewController(t) @@ -351,7 +408,7 @@ func TestPresenter_buildSubmission(t *testing.T) { keyResolver.EXPECT().ResolveKey(webWalletDID, nil, resolver.NutsSigningKeyType).Return(key.KID, key.PublicKey, nil).AnyTimes() w := presenter{documentLoader: jsonldManager.DocumentLoader(), signer: keyStore, keyResolver: keyResolver} - vp, submission, err := w.buildSubmission(ctx, credentials, pe.PresentationDefinition{}, BuildParams{Audience: verifierDID.String(), Expires: time.Now().Add(time.Second), Format: vpFormats, Nonce: ""}) + vp, submission, err := w.buildSubmission(ctx, credentials, pe.PresentationDefinition{}, nil, BuildParams{Audience: verifierDID.String(), Expires: time.Now().Add(time.Second), Format: vpFormats, Nonce: ""}) assert.Nil(t, err) assert.NotNil(t, vp) diff --git a/vcr/holder/sql_wallet.go b/vcr/holder/sql_wallet.go index c66b6b92da..7323332558 100644 --- a/vcr/holder/sql_wallet.go +++ b/vcr/holder/sql_wallet.go @@ -34,6 +34,7 @@ import ( "github.com/nuts-foundation/nuts-node/storage" "github.com/nuts-foundation/nuts-node/vcr/credential" "github.com/nuts-foundation/nuts-node/vcr/credential/store" + "github.com/nuts-foundation/nuts-node/vcr/log" "github.com/nuts-foundation/nuts-node/vcr/pe" "github.com/nuts-foundation/nuts-node/vcr/types" @@ -79,7 +80,7 @@ type BuildParams struct { Nonce string } -func (h sqlWallet) BuildSubmission(ctx context.Context, walletDIDs []did.DID, credentials map[did.DID][]vc.VerifiableCredential, presentationDefinition pe.PresentationDefinition, params BuildParams) (*vc.VerifiablePresentation, *pe.PresentationSubmission, error) { +func (h sqlWallet) BuildSubmission(ctx context.Context, walletDIDs []did.DID, credentials map[did.DID][]vc.VerifiableCredential, presentationDefinition pe.PresentationDefinition, credentialSelection map[string]string, params BuildParams) (*vc.VerifiablePresentation, *pe.PresentationSubmission, error) { if credentials == nil { credentials = make(map[did.DID][]vc.VerifiableCredential) } @@ -97,7 +98,7 @@ func (h sqlWallet) BuildSubmission(ctx context.Context, walletDIDs []did.DID, cr documentLoader: h.jsonldManager.DocumentLoader(), signer: h.keyStore, keyResolver: h.keyResolver, - }.buildSubmission(ctx, credentials, presentationDefinition, params) + }.buildSubmission(ctx, credentials, presentationDefinition, credentialSelection, params) } func (h sqlWallet) BuildPresentation(ctx context.Context, credentials []vc.VerifiableCredential, options PresentationOptions, signerDID *did.DID, validateVC bool) (*vc.VerifiablePresentation, error) {