From 31c00a5e3c6c0ef2ea27a1045ec1fe4e6a409132 Mon Sep 17 00:00:00 2001 From: Bob Stasyszyn Date: Mon, 30 Oct 2023 13:45:41 -0400 Subject: [PATCH] chore: Additional events on failures (#1499) Signed-off-by: Bob Stasyszyn --- pkg/restapi/resterr/error.go | 2 + pkg/restapi/v1/issuer/controller.go | 2 +- pkg/restapi/v1/issuer/controller_test.go | 2 +- pkg/service/oidc4ci/oidc4ci_service.go | 21 +- .../oidc4ci_service_exchange_code_test.go | 2 +- pkg/service/oidc4ci/oidc4ci_service_state.go | 9 +- .../oidc4ci/oidc4ci_service_state_test.go | 2 +- pkg/service/oidc4ci/oidc4ci_service_test.go | 273 ++++++++++++------ pkg/service/oidc4vp/api.go | 2 +- pkg/service/oidc4vp/oidc4vp_service.go | 20 +- pkg/service/oidc4vp/oidc4vp_service_test.go | 30 +- test/bdd/pkg/v1/oidc4vc/oidc4ci.go | 4 +- 12 files changed, 245 insertions(+), 124 deletions(-) diff --git a/pkg/restapi/resterr/error.go b/pkg/restapi/resterr/error.go index ed99501cb..69b82c826 100644 --- a/pkg/restapi/resterr/error.go +++ b/pkg/restapi/resterr/error.go @@ -41,6 +41,7 @@ const ( PresentationDefinitionMismatch ErrorCode = "presentation-definition-mismatch" ClaimsNotReceived ErrorCode = "claims-not-received" ClaimsNotFound ErrorCode = "claims-not-found" + ClaimsValidationErr ErrorCode = "invalid-claims" DataNotFound ErrorCode = "data-not-found" OpStateKeyDuplication ErrorCode = "op-state-key duplication" CredentialTemplateNotConfigured ErrorCode = "credential-template-not-configured" @@ -52,6 +53,7 @@ const ( CredentialFormatNotSupported ErrorCode = "credential-format-not-supported" VCOptionsNotConfigured ErrorCode = "vc-options-not-configured" InvalidIssuerURL ErrorCode = "invalid-issuer-url" + InvalidStateTransition ErrorCode = "invalid-state-transition" ) type Component = string diff --git a/pkg/restapi/v1/issuer/controller.go b/pkg/restapi/v1/issuer/controller.go index e039d5c4b..5b7062ad8 100644 --- a/pkg/restapi/v1/issuer/controller.go +++ b/pkg/restapi/v1/issuer/controller.go @@ -736,7 +736,7 @@ func (c *Controller) PrepareCredential(e echo.Context) error { } if err = c.validateClaims(result.Credential, result.CredentialTemplate, result.EnforceStrictValidation); err != nil { - return fmt.Errorf("validate claims: %w", err) + return resterr.NewCustomError(resterr.ClaimsValidationErr, err) } signedCredential, err := c.signCredential( diff --git a/pkg/restapi/v1/issuer/controller_test.go b/pkg/restapi/v1/issuer/controller_test.go index d8e83c862..027d5788d 100644 --- a/pkg/restapi/v1/issuer/controller_test.go +++ b/pkg/restapi/v1/issuer/controller_test.go @@ -1542,7 +1542,7 @@ func TestController_PrepareCredential(t *testing.T) { req := `{"tx_id":"123","type":"UniversityDegreeCredential","format":"ldp_vc"}` ctx := echoContext(withRequestBody([]byte(req))) - assert.EqualError(t, c.PrepareCredential(ctx), "validate claims: validation error") + assert.EqualError(t, c.PrepareCredential(ctx), "invalid-claims: validation error") }) } diff --git a/pkg/service/oidc4ci/oidc4ci_service.go b/pkg/service/oidc4ci/oidc4ci_service.go index 008dff9af..d01c42356 100644 --- a/pkg/service/oidc4ci/oidc4ci_service.go +++ b/pkg/service/oidc4ci/oidc4ci_service.go @@ -261,8 +261,11 @@ func (s *Service) PrepareClaimDataAuthorizationRequest( } if err = s.store.Update(ctx, tx); err != nil { - s.sendFailedTransactionEvent(ctx, tx, err) - return nil, err + e := resterr.NewSystemError(resterr.TransactionStoreComponent, "Update", err) + + s.sendFailedTransactionEvent(ctx, tx, e) + + return nil, e } if err = s.sendIssuanceAuthRequestPreparedTxEvent(ctx, tx); err != nil { @@ -361,7 +364,7 @@ func (s *Service) updateAuthorizationDetails(ctx context.Context, ad *Authorizat tx.AuthorizationDetails = ad if err := s.store.Update(ctx, tx); err != nil { - return fmt.Errorf("update tx: %w", err) + return resterr.NewSystemError(resterr.TransactionStoreComponent, "Update", err) } return nil @@ -454,13 +457,21 @@ func (s *Service) PrepareCredential( expectedAudience := fmt.Sprintf("%s/oidc/idp/%s/%s", s.issuerVCSPublicHost, tx.ProfileID, tx.ProfileVersion) if req.AudienceClaim == "" || req.AudienceClaim != expectedAudience { - return nil, resterr.NewValidationError(resterr.InvalidOrMissingProofOIDCErr, req.AudienceClaim, + e := resterr.NewValidationError(resterr.InvalidOrMissingProofOIDCErr, req.AudienceClaim, errors.New("invalid aud")) + + s.sendFailedTransactionEvent(ctx, tx, e) + + return nil, e } claimData, err := s.getClaimsData(ctx, tx) if err != nil { - return nil, fmt.Errorf("get claims data: %w", err) + e := fmt.Errorf("get claims data: %w", err) + + s.sendFailedTransactionEvent(ctx, tx, e) + + return nil, e } contexts := tx.CredentialTemplate.Contexts diff --git a/pkg/service/oidc4ci/oidc4ci_service_exchange_code_test.go b/pkg/service/oidc4ci/oidc4ci_service_exchange_code_test.go index 28ffde936..8613b9260 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_exchange_code_test.go +++ b/pkg/service/oidc4ci/oidc4ci_service_exchange_code_test.go @@ -280,7 +280,7 @@ func TestExchangeCodeInvalidState(t *testing.T) { resp, err := svc.ExchangeAuthorizationCode(context.TODO(), "sadsadas") assert.Empty(t, resp) - assert.ErrorContains(t, err, "unexpected transaction from 5 to 4") + assert.ErrorContains(t, err, "unexpected transition from 5 to 4") } func TestExchangeCodePublishError(t *testing.T) { diff --git a/pkg/service/oidc4ci/oidc4ci_service_state.go b/pkg/service/oidc4ci/oidc4ci_service_state.go index 13df632bd..a1fd52695 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_state.go +++ b/pkg/service/oidc4ci/oidc4ci_service_state.go @@ -6,7 +6,11 @@ SPDX-License-Identifier: Apache-2.0 package oidc4ci -import "fmt" +import ( + "fmt" + + "github.com/trustbloc/vcs/pkg/restapi/resterr" +) func (s *Service) validateStateTransition( oldState TransactionState, @@ -37,5 +41,6 @@ func (s *Service) validateStateTransition( return nil } - return fmt.Errorf("unexpected transaction from %v to %v", oldState, newState) + return resterr.NewCustomError(resterr.InvalidStateTransition, + fmt.Errorf("unexpected transition from %v to %v", oldState, newState)) } diff --git a/pkg/service/oidc4ci/oidc4ci_service_state_test.go b/pkg/service/oidc4ci/oidc4ci_service_state_test.go index 6a21081fb..eadd43428 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_state_test.go +++ b/pkg/service/oidc4ci/oidc4ci_service_state_test.go @@ -55,5 +55,5 @@ func TestInvalidTransition(t *testing.T) { assert.NoError(t, err) assert.ErrorContains(t, s.validateStateTransition(TransactionStateUnknown, TransactionStateIssuanceInitiated), - "unexpected transaction from 0 to 1") + "unexpected transition from 0 to 1") } diff --git a/pkg/service/oidc4ci/oidc4ci_service_test.go b/pkg/service/oidc4ci/oidc4ci_service_test.go index 48bc745ed..3663d690c 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_test.go +++ b/pkg/service/oidc4ci/oidc4ci_service_test.go @@ -164,7 +164,7 @@ func TestService_PushAuthorizationDetails(t *testing.T) { } }, check: func(t *testing.T, err error) { - require.ErrorContains(t, err, "update tx") + require.ErrorContains(t, err, "update error") }, }, } @@ -214,12 +214,7 @@ func TestService_PrepareClaimDataAuthorizationRequest(t *testing.T) { }).Times(2) mocks.eventService.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). - DoAndReturn(func(ctx context.Context, topic string, messages ...*spi.Event) error { - assert.Len(t, messages, 1) - assert.Equal(t, messages[0].Type, spi.IssuerOIDCInteractionAuthorizationRequestPrepared) - - return nil - }) + DoAndReturn(expectedPublishEventFunc(t, spi.IssuerOIDCInteractionAuthorizationRequestPrepared)) req = &oidc4ci.PrepareClaimDataAuthorizationRequest{ OpState: "opState", @@ -363,13 +358,13 @@ func TestService_PrepareClaimDataAuthorizationRequest(t *testing.T) { }, }, nil) - mocks.eventService.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). - DoAndReturn(func(ctx context.Context, topic string, messages ...*spi.Event) error { - assert.Len(t, messages, 1) - assert.Equal(t, messages[0].Type, spi.IssuerOIDCInteractionFailed) - - return nil - }) + mocks.eventService.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()).DoAndReturn( + expectedPublishErrorEventFunc(t, + resterr.SystemError, + "update error", + resterr.TransactionStoreComponent, + ), + ) mocks.transactionStore.EXPECT().Update(gomock.Any(), gomock.Any()).Return(errors.New("update error")) @@ -384,7 +379,7 @@ func TestService_PrepareClaimDataAuthorizationRequest(t *testing.T) { } }, check: func(t *testing.T, resp *oidc4ci.PrepareClaimDataAuthorizationResponse, err error) { - require.ErrorContains(t, err, "update tx") + require.ErrorContains(t, err, "update error") require.Empty(t, resp) }, }, @@ -404,13 +399,13 @@ func TestService_PrepareClaimDataAuthorizationRequest(t *testing.T) { }, }, nil) - mocks.eventService.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). - DoAndReturn(func(ctx context.Context, topic string, messages ...*spi.Event) error { - assert.Len(t, messages, 1) - assert.Equal(t, messages[0].Type, spi.IssuerOIDCInteractionFailed) - - return nil - }) + mocks.eventService.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()).DoAndReturn( + expectedPublishErrorEventFunc(t, + resterr.InvalidStateTransition, + "unexpected transition from 5 to 3", + "", + ), + ) req = &oidc4ci.PrepareClaimDataAuthorizationRequest{ OpState: "opState", @@ -423,7 +418,7 @@ func TestService_PrepareClaimDataAuthorizationRequest(t *testing.T) { } }, check: func(t *testing.T, resp *oidc4ci.PrepareClaimDataAuthorizationResponse, err error) { - require.ErrorContains(t, err, "unexpected transaction from 5 to 3") + require.ErrorContains(t, err, "unexpected transition from 5 to 3") require.Empty(t, resp) }, }, @@ -443,13 +438,13 @@ func TestService_PrepareClaimDataAuthorizationRequest(t *testing.T) { }, }, nil) - mocks.eventService.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). - DoAndReturn(func(ctx context.Context, topic string, messages ...*spi.Event) error { - assert.Len(t, messages, 1) - assert.Equal(t, messages[0].Type, spi.IssuerOIDCInteractionFailed) - - return nil - }) + mocks.eventService.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()).DoAndReturn( + expectedPublishErrorEventFunc(t, + resterr.SystemError, + "store update error", + resterr.TransactionStoreComponent, + ), + ) mocks.transactionStore.EXPECT().Update(gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, tx *oidc4ci.Transaction) error { @@ -1024,7 +1019,7 @@ func TestValidatePreAuthCode(t *testing.T) { }, nil) resp, err := srv.ValidatePreAuthorizedCodeRequest(context.TODO(), "1234", "567", "123abc") - assert.ErrorContains(t, err, "unexpected transaction from 5 to 2") + assert.ErrorContains(t, err, "unexpected transition from 5 to 2") assert.Nil(t, resp) }) @@ -1195,23 +1190,19 @@ func TestValidatePreAuthCode(t *testing.T) { func TestService_PrepareCredential(t *testing.T) { var ( - mockTransactionStore = NewMockTransactionStore(gomock.NewController(t)) - mockClaimDataStore = NewMockClaimDataStore(gomock.NewController(t)) - eventMock = NewMockEventService(gomock.NewController(t)) - crypto = NewMockDataProtector(gomock.NewController(t)) - httpClient *http.Client - req *oidc4ci.PrepareCredential + httpClient *http.Client + req *oidc4ci.PrepareCredential ) tests := []struct { name string - setup func() + setup func(m *mocks) check func(t *testing.T, resp *oidc4ci.PrepareCredentialResult, err error) }{ { name: "Success", - setup: func() { - mockTransactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ + setup: func(m *mocks) { + m.transactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ ID: "txID", TransactionData: oidc4ci.TransactionData{ IssuerToken: "issuer-access-token", @@ -1236,13 +1227,13 @@ func TestService_PrepareCredential(t *testing.T) { }, } - mockTransactionStore.EXPECT().Update(gomock.Any(), gomock.Any()). + m.transactionStore.EXPECT().Update(gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, tx *oidc4ci.Transaction) error { assert.Equal(t, oidc4ci.TransactionStateCredentialsIssued, tx.State) return nil }) - eventMock.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). + m.eventService.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). DoAndReturn(func(ctx context.Context, topic string, messages ...*spi.Event) error { assert.Len(t, messages, 1) assert.Equal(t, messages[0].Type, spi.IssuerOIDCInteractionSucceeded) @@ -1262,8 +1253,8 @@ func TestService_PrepareCredential(t *testing.T) { }, { name: "Success LDP", - setup: func() { - mockTransactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ + setup: func(m *mocks) { + m.transactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ ID: "txID", TransactionData: oidc4ci.TransactionData{ IssuerToken: "issuer-access-token", @@ -1289,13 +1280,13 @@ func TestService_PrepareCredential(t *testing.T) { }, } - mockTransactionStore.EXPECT().Update(gomock.Any(), gomock.Any()). + m.transactionStore.EXPECT().Update(gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, tx *oidc4ci.Transaction) error { assert.Equal(t, oidc4ci.TransactionStateCredentialsIssued, tx.State) return nil }) - eventMock.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). + m.eventService.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). DoAndReturn(func(ctx context.Context, topic string, messages ...*spi.Event) error { assert.Len(t, messages, 1) assert.Equal(t, messages[0].Type, spi.IssuerOIDCInteractionSucceeded) @@ -1318,8 +1309,8 @@ func TestService_PrepareCredential(t *testing.T) { }, { name: "Success LDP with name and description", - setup: func() { - mockTransactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ + setup: func(m *mocks) { + m.transactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ ID: "txID", TransactionData: oidc4ci.TransactionData{ IssuerToken: "issuer-access-token", @@ -1347,13 +1338,13 @@ func TestService_PrepareCredential(t *testing.T) { }, } - mockTransactionStore.EXPECT().Update(gomock.Any(), gomock.Any()). + m.transactionStore.EXPECT().Update(gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, tx *oidc4ci.Transaction) error { assert.Equal(t, oidc4ci.TransactionStateCredentialsIssued, tx.State) return nil }) - eventMock.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). + m.eventService.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). DoAndReturn(func(ctx context.Context, topic string, messages ...*spi.Event) error { assert.Len(t, messages, 1) assert.Equal(t, messages[0].Type, spi.IssuerOIDCInteractionSucceeded) @@ -1380,9 +1371,9 @@ func TestService_PrepareCredential(t *testing.T) { }, { name: "Success pre-authorized flow", - setup: func() { + setup: func(m *mocks) { claimID := uuid.NewString() - mockTransactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ + m.transactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ ID: "txID", TransactionData: oidc4ci.TransactionData{ IssuerToken: "issuer-access-token", @@ -1395,7 +1386,7 @@ func TestService_PrepareCredential(t *testing.T) { }, }, nil) - eventMock.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). + m.eventService.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). DoAndReturn(func(ctx context.Context, topic string, messages ...*spi.Event) error { assert.Len(t, messages, 1) assert.Equal(t, messages[0].Type, spi.IssuerOIDCInteractionSucceeded) @@ -1403,7 +1394,7 @@ func TestService_PrepareCredential(t *testing.T) { return nil }) - mockTransactionStore.EXPECT().Update(gomock.Any(), gomock.Any()). + m.transactionStore.EXPECT().Update(gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, tx *oidc4ci.Transaction) error { assert.Equal(t, oidc4ci.TransactionStateCredentialsIssued, tx.State) return nil @@ -1416,9 +1407,9 @@ func TestService_PrepareCredential(t *testing.T) { }, } - mockClaimDataStore.EXPECT().GetAndDelete(gomock.Any(), claimID).Return(clData, nil) + m.claimDataStore.EXPECT().GetAndDelete(gomock.Any(), claimID).Return(clData, nil) - crypto.EXPECT().Decrypt(gomock.Any(), clData.EncryptedData). + m.crypto.EXPECT().Decrypt(gomock.Any(), clData.EncryptedData). DoAndReturn(func(ctx context.Context, chunks *dataprotect.EncryptedData) ([]byte, error) { b, _ := json.Marshal(map[string]interface{}{}) return b, nil @@ -1436,8 +1427,8 @@ func TestService_PrepareCredential(t *testing.T) { }, { name: "Failed to get claims for pre-authorized flow", - setup: func() { - mockTransactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ + setup: func(m *mocks) { + m.transactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ ID: "txID", TransactionData: oidc4ci.TransactionData{ IssuerToken: "issuer-access-token", @@ -1450,10 +1441,17 @@ func TestService_PrepareCredential(t *testing.T) { }, }, nil) - eventMock.EXPECT().Publish(gomock.Any(), gomock.Any(), gomock.Any()).Times(0) - mockTransactionStore.EXPECT().Update(gomock.Any(), gomock.Any()).Times(0) + m.eventService.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). + DoAndReturn(func(ctx context.Context, topic string, messages ...*spi.Event) error { + assert.Len(t, messages, 1) + assert.Equal(t, messages[0].Type, spi.IssuerOIDCInteractionFailed) + + return nil + }) + + m.transactionStore.EXPECT().Update(gomock.Any(), gomock.Any()).Times(0) - mockClaimDataStore.EXPECT().GetAndDelete(gomock.Any(), gomock.Any()).Return(nil, errors.New("get error")) + m.claimDataStore.EXPECT().GetAndDelete(gomock.Any(), gomock.Any()).Return(nil, errors.New("get error")) req = &oidc4ci.PrepareCredential{ TxID: "txID", @@ -1467,8 +1465,8 @@ func TestService_PrepareCredential(t *testing.T) { }, { name: "Failed to send event for pre-authorized flow", - setup: func() { - mockTransactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ + setup: func(m *mocks) { + m.transactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ ID: "txID", TransactionData: oidc4ci.TransactionData{ IssuerToken: "issuer-access-token", @@ -1481,7 +1479,7 @@ func TestService_PrepareCredential(t *testing.T) { }, }, nil) - mockTransactionStore.EXPECT().Update(gomock.Any(), gomock.Any()). + m.transactionStore.EXPECT().Update(gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, tx *oidc4ci.Transaction) error { assert.Equal(t, oidc4ci.TransactionStateCredentialsIssued, tx.State) return nil @@ -1492,14 +1490,14 @@ func TestService_PrepareCredential(t *testing.T) { EncryptedNonce: []byte{0x0, 0x2}, }, } - crypto.EXPECT().Decrypt(gomock.Any(), clData.EncryptedData). + m.crypto.EXPECT().Decrypt(gomock.Any(), clData.EncryptedData). DoAndReturn(func(ctx context.Context, chunks *dataprotect.EncryptedData) ([]byte, error) { b, _ := json.Marshal(map[string]interface{}{}) return b, nil }) - mockClaimDataStore.EXPECT().GetAndDelete(gomock.Any(), gomock.Any()).Return(clData, nil) + m.claimDataStore.EXPECT().GetAndDelete(gomock.Any(), gomock.Any()).Return(clData, nil) - eventMock.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). + m.eventService.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). DoAndReturn(func(ctx context.Context, topic string, messages ...*spi.Event) error { assert.Len(t, messages, 1) assert.Equal(t, messages[0].Type, spi.IssuerOIDCInteractionSucceeded) @@ -1519,8 +1517,8 @@ func TestService_PrepareCredential(t *testing.T) { }, { name: "Failed to update tx state", - setup: func() { - mockTransactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ + setup: func(m *mocks) { + m.transactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ ID: "txID", TransactionData: oidc4ci.TransactionData{ IssuerToken: "issuer-access-token", @@ -1533,7 +1531,7 @@ func TestService_PrepareCredential(t *testing.T) { }, }, nil) - mockTransactionStore.EXPECT().Update(gomock.Any(), gomock.Any()). + m.transactionStore.EXPECT().Update(gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, tx *oidc4ci.Transaction) error { assert.Equal(t, oidc4ci.TransactionStateCredentialsIssued, tx.State) return errors.New("store err") @@ -1545,14 +1543,14 @@ func TestService_PrepareCredential(t *testing.T) { EncryptedNonce: []byte{0x0, 0x2}, }, } - crypto.EXPECT().Decrypt(gomock.Any(), clData.EncryptedData). + m.crypto.EXPECT().Decrypt(gomock.Any(), clData.EncryptedData). DoAndReturn(func(ctx context.Context, chunks *dataprotect.EncryptedData) ([]byte, error) { b, _ := json.Marshal(map[string]interface{}{}) return b, nil }) - mockClaimDataStore.EXPECT().GetAndDelete(gomock.Any(), gomock.Any()).Return(clData, nil) + m.claimDataStore.EXPECT().GetAndDelete(gomock.Any(), gomock.Any()).Return(clData, nil) - eventMock.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). + m.eventService.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). DoAndReturn(func(ctx context.Context, topic string, messages ...*spi.Event) error { assert.Len(t, messages, 1) assert.Equal(t, messages[0].Type, spi.IssuerOIDCInteractionFailed) @@ -1572,8 +1570,8 @@ func TestService_PrepareCredential(t *testing.T) { }, { name: "Fail to find transaction by op state", - setup: func() { - mockTransactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return( + setup: func(m *mocks) { + m.transactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return( nil, errors.New("get error")) req = &oidc4ci.PrepareCredential{ @@ -1587,12 +1585,12 @@ func TestService_PrepareCredential(t *testing.T) { }, { name: "Credential template not configured", - setup: func() { - mockTransactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ + setup: func(m *mocks) { + m.transactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ TransactionData: oidc4ci.TransactionData{}, }, nil) - eventMock.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). + m.eventService.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). DoAndReturn(func(ctx context.Context, topic string, messages ...*spi.Event) error { assert.Len(t, messages, 1) assert.Equal(t, messages[0].Type, spi.IssuerOIDCInteractionFailed) @@ -1612,13 +1610,21 @@ func TestService_PrepareCredential(t *testing.T) { }, { name: "Fail to make request to claim endpoint", - setup: func() { - mockTransactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ + setup: func(m *mocks) { + m.transactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ TransactionData: oidc4ci.TransactionData{ CredentialTemplate: &profileapi.CredentialTemplate{}, }, }, nil) + m.eventService.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). + DoAndReturn(func(ctx context.Context, topic string, messages ...*spi.Event) error { + assert.Len(t, messages, 1) + assert.Equal(t, messages[0].Type, spi.IssuerOIDCInteractionFailed) + + return nil + }) + httpClient = &http.Client{ Transport: &mockTransport{ func(req *http.Request) (*http.Response, error) { @@ -1639,13 +1645,21 @@ func TestService_PrepareCredential(t *testing.T) { }, { name: "Claim endpoint returned other than 200 OK status code", - setup: func() { - mockTransactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ + setup: func(m *mocks) { + m.transactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ TransactionData: oidc4ci.TransactionData{ CredentialTemplate: &profileapi.CredentialTemplate{}, }, }, nil) + m.eventService.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). + DoAndReturn(func(ctx context.Context, topic string, messages ...*spi.Event) error { + assert.Len(t, messages, 1) + assert.Equal(t, messages[0].Type, spi.IssuerOIDCInteractionFailed) + + return nil + }) + httpClient = &http.Client{ Transport: &mockTransport{ func(req *http.Request) (*http.Response, error) { @@ -1669,13 +1683,21 @@ func TestService_PrepareCredential(t *testing.T) { }, { name: "Fail to read response body from claim endpoint when status is not 200 OK", - setup: func() { - mockTransactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ + setup: func(m *mocks) { + m.transactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ TransactionData: oidc4ci.TransactionData{ CredentialTemplate: &profileapi.CredentialTemplate{}, }, }, nil) + m.eventService.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). + DoAndReturn(func(ctx context.Context, topic string, messages ...*spi.Event) error { + assert.Len(t, messages, 1) + assert.Equal(t, messages[0].Type, spi.IssuerOIDCInteractionFailed) + + return nil + }) + httpClient = &http.Client{ Transport: &mockTransport{ func(req *http.Request) (*http.Response, error) { @@ -1699,8 +1721,8 @@ func TestService_PrepareCredential(t *testing.T) { }, { name: "Fail to decode claim data", - setup: func() { - mockTransactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ + setup: func(m *mocks) { + m.transactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ TransactionData: oidc4ci.TransactionData{ CredentialTemplate: &profileapi.CredentialTemplate{}, }, @@ -1717,6 +1739,14 @@ func TestService_PrepareCredential(t *testing.T) { }, } + m.eventService.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). + DoAndReturn(func(ctx context.Context, topic string, messages ...*spi.Event) error { + assert.Len(t, messages, 1) + assert.Equal(t, messages[0].Type, spi.IssuerOIDCInteractionFailed) + + return nil + }) + req = &oidc4ci.PrepareCredential{ TxID: "txID", AudienceClaim: "/oidc/idp//", @@ -1729,8 +1759,8 @@ func TestService_PrepareCredential(t *testing.T) { }, { name: "Invalid audience claim", - setup: func() { - mockTransactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ + setup: func(m *mocks) { + m.transactionStore.EXPECT().Get(gomock.Any(), oidc4ci.TxID("txID")).Return(&oidc4ci.Transaction{ ID: "txID", TransactionData: oidc4ci.TransactionData{ IssuerToken: "issuer-access-token", @@ -1741,6 +1771,14 @@ func TestService_PrepareCredential(t *testing.T) { }, }, nil) + m.eventService.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). + DoAndReturn(func(ctx context.Context, topic string, messages ...*spi.Event) error { + assert.Len(t, messages, 1) + assert.Equal(t, messages[0].Type, spi.IssuerOIDCInteractionFailed) + + return nil + }) + claimData := `{"surname":"Smith","givenName":"Pat","jobTitle":"Worker"}` httpClient = &http.Client{ @@ -1768,15 +1806,22 @@ func TestService_PrepareCredential(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tt.setup() + m := &mocks{ + transactionStore: NewMockTransactionStore(gomock.NewController(t)), + claimDataStore: NewMockClaimDataStore(gomock.NewController(t)), + eventService: NewMockEventService(gomock.NewController(t)), + crypto: NewMockDataProtector(gomock.NewController(t)), + } + + tt.setup(m) svc, err := oidc4ci.NewService(&oidc4ci.Config{ - TransactionStore: mockTransactionStore, - ClaimDataStore: mockClaimDataStore, + TransactionStore: m.transactionStore, + ClaimDataStore: m.claimDataStore, HTTPClient: httpClient, - EventService: eventMock, + EventService: m.eventService, EventTopic: spi.IssuerEventTopic, - DataProtector: crypto, + DataProtector: m.crypto, }) require.NoError(t, err) @@ -1818,3 +1863,45 @@ type failReader struct{} func (f *failReader) Read([]byte) (int, error) { return 0, errors.New("read error") } + +type eventPublishFunc func(ctx context.Context, topic string, messages ...*spi.Event) error + +func expectedPublishEventFunc( + t *testing.T, + eventType spi.EventType, +) eventPublishFunc { + t.Helper() + + return func(ctx context.Context, topic string, messages ...*spi.Event) error { + require.Len(t, messages, 1) + require.Equal(t, eventType, messages[0].Type) + + return nil + } +} + +func expectedPublishErrorEventFunc( + t *testing.T, + errCode resterr.ErrorCode, + errMessage string, + errComponent resterr.Component, +) eventPublishFunc { + t.Helper() + + return func(ctx context.Context, topic string, messages ...*spi.Event) error { + require.Len(t, messages, 1) + require.Equal(t, spi.IssuerOIDCInteractionFailed, messages[0].Type) + + var ep oidc4ci.EventPayload + require.NoError(t, json.Unmarshal(messages[0].Data, &ep)) + + assert.Equalf(t, string(errCode), ep.ErrorCode, "unexpected error code") + assert.Equalf(t, errComponent, ep.ErrorComponent, "unexpected error component") + + if errMessage != "" { + assert.Containsf(t, ep.Error, errMessage, "unexpected error message") + } + + return nil + } +} diff --git a/pkg/service/oidc4vp/api.go b/pkg/service/oidc4vp/api.go index 1283204e5..945b760b0 100644 --- a/pkg/service/oidc4vp/api.go +++ b/pkg/service/oidc4vp/api.go @@ -61,7 +61,7 @@ type EventPayload struct { Filter *Filter `json:"filter,omitempty"` AuthorizationRequest string `json:"authorizationRequest,omitempty"` Error string `json:"error,omitempty"` - ErrorCode string `json:"errorCode"` + ErrorCode string `json:"errorCode,omitempty"` ErrorComponent string `json:"errorComponent,omitempty"` } diff --git a/pkg/service/oidc4vp/oidc4vp_service.go b/pkg/service/oidc4vp/oidc4vp_service.go index cfc68b616..5cdc818bd 100644 --- a/pkg/service/oidc4vp/oidc4vp_service.go +++ b/pkg/service/oidc4vp/oidc4vp_service.go @@ -200,6 +200,7 @@ func (s *Service) sendOIDCInteractionInitiatedEvent( ) error { ep := createTxEventPayload(tx, profile) ep.AuthorizationRequest = authorizationRequest + ep.Filter = getFilter(tx.PresentationDefinition) event, err := CreateEvent(spi.VerifierOIDCInteractionInitiated, tx.ID, ep) if err != nil { @@ -780,20 +781,10 @@ func CreateEvent( } func createTxEventPayload(tx *Transaction, profile *profileapi.Verifier) *EventPayload { - fieldsMap := make(map[string]struct{}) - var presentationDefID string if tx.PresentationDefinition != nil { presentationDefID = tx.PresentationDefinition.ID - - for _, desc := range tx.PresentationDefinition.InputDescriptors { - if desc.Constraints != nil { - for _, f := range desc.Constraints.Fields { - fieldsMap[f.ID] = struct{}{} - } - } - } } return &EventPayload{ @@ -802,10 +793,17 @@ func createTxEventPayload(tx *Transaction, profile *profileapi.Verifier) *EventP ProfileVersion: profile.Version, OrgID: profile.OrganizationID, PresentationDefinitionID: presentationDefID, - Filter: &Filter{Fields: getConstraintFields(tx.PresentationDefinition)}, } } +func getFilter(def *presexch.PresentationDefinition) *Filter { + if def == nil { + return nil + } + + return &Filter{Fields: getConstraintFields(def)} +} + func getConstraintFields(def *presexch.PresentationDefinition) []string { if def == nil { return nil diff --git a/pkg/service/oidc4vp/oidc4vp_service_test.go b/pkg/service/oidc4vp/oidc4vp_service_test.go index f727ef12f..6d248c6ae 100644 --- a/pkg/service/oidc4vp/oidc4vp_service_test.go +++ b/pkg/service/oidc4vp/oidc4vp_service_test.go @@ -27,8 +27,6 @@ import ( "github.com/trustbloc/kms-go/wrapper/localsuite" "github.com/trustbloc/vc-go/proof/testsupport" - "github.com/trustbloc/vcs/internal/mock/vcskms" - "github.com/trustbloc/did-go/doc/did" ldcontext "github.com/trustbloc/did-go/doc/ld/context" lddocloader "github.com/trustbloc/did-go/doc/ld/documentloader" @@ -42,6 +40,7 @@ import ( "github.com/trustbloc/vc-go/presexch" "github.com/trustbloc/vc-go/verifiable" + "github.com/trustbloc/vcs/internal/mock/vcskms" vcsverifiable "github.com/trustbloc/vcs/pkg/doc/verifiable" "github.com/trustbloc/vcs/pkg/event/spi" "github.com/trustbloc/vcs/pkg/internal/testutil" @@ -693,7 +692,9 @@ func TestService_RetrieveClaims(t *testing.T) { t.Run("Success JWT", func(t *testing.T) { mockEventSvc := NewMockeventService(gomock.NewController(t)) - mockEventSvc.EXPECT().Publish(gomock.Any(), spi.VerifierEventTopic, gomock.Any()).Times(1) + mockEventSvc.EXPECT().Publish(gomock.Any(), spi.VerifierEventTopic, gomock.Any()).DoAndReturn( + expectedPublishEventFunc(t, spi.VerifierOIDCInteractionClaimsRetrieved), + ) svc := oidc4vp.NewService(&oidc4vp.Config{EventSvc: mockEventSvc, EventTopic: spi.VerifierEventTopic}) @@ -721,7 +722,9 @@ func TestService_RetrieveClaims(t *testing.T) { t.Run("Success JsonLD", func(t *testing.T) { mockEventSvc := NewMockeventService(gomock.NewController(t)) - mockEventSvc.EXPECT().Publish(gomock.Any(), spi.VerifierEventTopic, gomock.Any()).Times(1) + mockEventSvc.EXPECT().Publish(gomock.Any(), spi.VerifierEventTopic, gomock.Any()).DoAndReturn( + expectedPublishEventFunc(t, spi.VerifierOIDCInteractionClaimsRetrieved), + ) svc := oidc4vp.NewService(&oidc4vp.Config{EventSvc: mockEventSvc, EventTopic: spi.VerifierEventTopic}) ldvc, err := verifiable.ParseCredential([]byte(sampleVCJsonLD), @@ -746,9 +749,11 @@ func TestService_RetrieveClaims(t *testing.T) { require.NotEmpty(t, claims["http://example.gov/credentials/3732"].ExpirationDate) }) - t.Run("Error", func(t *testing.T) { + t.Run("Empty claims", func(t *testing.T) { mockEventSvc := NewMockeventService(gomock.NewController(t)) - mockEventSvc.EXPECT().Publish(gomock.Any(), spi.VerifierEventTopic, gomock.Any()).Times(1) + mockEventSvc.EXPECT().Publish(gomock.Any(), spi.VerifierEventTopic, gomock.Any()).DoAndReturn( + expectedPublishEventFunc(t, spi.VerifierOIDCInteractionClaimsRetrieved), + ) svc := oidc4vp.NewService(&oidc4vp.Config{EventSvc: mockEventSvc, EventTopic: spi.VerifierEventTopic}) credential, err := verifiable.CreateCredential(verifiable.CredentialContents{ @@ -1172,3 +1177,16 @@ func Test_GetSupportedVPFormats(t *testing.T) { }) } } + +type eventPublishFunc func(ctx context.Context, topic string, messages ...*spi.Event) error + +func expectedPublishEventFunc(t *testing.T, eventType spi.EventType) eventPublishFunc { + t.Helper() + + return func(ctx context.Context, topic string, messages ...*spi.Event) error { + require.Len(t, messages, 1) + require.Equal(t, eventType, messages[0].Type) + + return nil + } +} diff --git a/test/bdd/pkg/v1/oidc4vc/oidc4ci.go b/test/bdd/pkg/v1/oidc4vc/oidc4ci.go index 78ee32b7d..bfb10d947 100644 --- a/test/bdd/pkg/v1/oidc4vc/oidc4ci.go +++ b/test/bdd/pkg/v1/oidc4vc/oidc4ci.go @@ -180,7 +180,7 @@ func (s *Steps) initiateCredentialIssuanceWithClaimsSchemaValidationError() erro return errors.New("error expected") } - if !strings.Contains(err.Error(), "validate claims: validation error: [(root): name is required; degree: type is required]") { + if !strings.Contains(err.Error(), "validation error: [(root): name is required; degree: type is required]") { return fmt.Errorf("unexpected error: %w", err) } @@ -437,7 +437,7 @@ func (s *Steps) runOIDC4CIAuthWithInvalidClaims() error { return fmt.Errorf("error expected, got nil") } - if !strings.Contains(err.Error(), "validate claims: validation error: [(root): name is required; degree: type is required]") { + if !strings.Contains(err.Error(), "validation error: [(root): name is required; degree: type is required]") { return fmt.Errorf("unexpected error: %w", err) }