From 257df376a40fd6045bc464d535aecdaaf8cfcba2 Mon Sep 17 00:00:00 2001 From: Misha Sizov Date: Mon, 14 Oct 2024 12:10:22 +0300 Subject: [PATCH] feat: add interaction_details on ack request Signed-off-by: Misha Sizov --- api/spec/openapi.gen.go | 408 +++++----- .../wallet-cli/pkg/oidc4vci/oidc4vci_flow.go | 7 +- .../wallet-cli/pkg/oidc4vp/oidc4vp_flow.go | 8 + docs/v1/openapi.yaml | 8 +- pkg/restapi/v1/oidc4ci/controller.go | 11 +- pkg/restapi/v1/oidc4ci/controller_test.go | 7 +- pkg/restapi/v1/oidc4ci/openapi.gen.go | 7 +- pkg/restapi/v1/verifier/controller.go | 40 +- pkg/restapi/v1/verifier/controller_test.go | 384 +++++---- pkg/service/oidc4ci/api.go | 18 +- .../oidc4ci/oidc4ci_acknowledgement.go | 24 +- .../oidc4ci/oidc4ci_acknowledgement_test.go | 13 + pkg/service/oidc4vp/api.go | 17 +- pkg/service/oidc4vp/oidc4vp_service.go | 18 +- pkg/service/oidc4vp/oidc4vp_service_test.go | 735 +++++++++++++----- .../oidc4vp/oidc4vp_wallet_notification.go | 10 +- .../oidc4vp_wallet_notification_test.go | 121 +-- .../cognito-config/db/local_5a9GzRvB.json | 9 +- test/bdd/fixtures/profile/profiles.json | 30 + test/bdd/pkg/v1/oidc4vc/oidc4vci.go | 40 + test/bdd/pkg/v1/oidc4vc/oidc4vp.go | 41 +- test/bdd/pkg/v1/oidc4vc/steps.go | 2 +- 22 files changed, 1286 insertions(+), 672 deletions(-) diff --git a/api/spec/openapi.gen.go b/api/spec/openapi.gen.go index 6b28622bf..939cd42f9 100644 --- a/api/spec/openapi.gen.go +++ b/api/spec/openapi.gen.go @@ -19,211 +19,211 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "HwAA//+8NaAm4zQBAA==", + "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", + "9R+/fPzy/wMAAP//oZPWngs1AQA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/component/wallet-cli/pkg/oidc4vci/oidc4vci_flow.go b/component/wallet-cli/pkg/oidc4vci/oidc4vci_flow.go index 271590386..dfc7c37b0 100644 --- a/component/wallet-cli/pkg/oidc4vci/oidc4vci_flow.go +++ b/component/wallet-cli/pkg/oidc4vci/oidc4vci_flow.go @@ -1150,7 +1150,10 @@ func (f *Flow) handleIssuanceAck( ) ackRequest := oidc4civ1.AckRequest{ - Credentials: []oidc4civ1.AcpRequestItem{}, + Credentials: []oidc4civ1.AckRequestItem{}, + InteractionDetails: lo.ToPtr(map[string]interface{}{ + "notification_ids": notificationIDs, + }), } for _, notificationID := range notificationIDs { @@ -1158,7 +1161,7 @@ func (f *Flow) handleIssuanceAck( continue } - ackRequest.Credentials = append(ackRequest.Credentials, oidc4civ1.AcpRequestItem{ + ackRequest.Credentials = append(ackRequest.Credentials, oidc4civ1.AckRequestItem{ Event: "credential_accepted", EventDescription: nil, IssuerIdentifier: wellKnown.CredentialIssuer, diff --git a/component/wallet-cli/pkg/oidc4vp/oidc4vp_flow.go b/component/wallet-cli/pkg/oidc4vp/oidc4vp_flow.go index 28f3ebc1e..3130a6edb 100644 --- a/component/wallet-cli/pkg/oidc4vp/oidc4vp_flow.go +++ b/component/wallet-cli/pkg/oidc4vp/oidc4vp_flow.go @@ -9,6 +9,7 @@ package oidc4vp import ( "bytes" "context" + "encoding/base64" "encoding/json" "fmt" "io" @@ -196,12 +197,15 @@ func (f *Flow) Run(ctx context.Context) error { vps, presentationSubmission, err := f.queryWallet(&pd, requestObject.ClientMetadata.VPFormats) if err != nil { if strings.Contains(err.Error(), "no matching credentials found") { + interactionDetailsBytes, _ := json.Marshal(map[string]string{"transaction_id": requestObject.State}) + // Send wallet notification no_match_found. v := url.Values{} v.Add("error", "access_denied") v.Add("error_description", "no_match_found") v.Add("state", requestObject.State) + v.Add("interaction_details", base64.StdEncoding.EncodeToString(interactionDetailsBytes)) if e := f.postAuthorizationResponse(ctx, requestObject.ResponseURI, []byte(v.Encode())); e != nil { slog.Error("failed to send wallet notification", "err", e) @@ -487,6 +491,10 @@ func (f *Flow) sendAuthorizationResponse( v.Add("presentation_submission", string(presentationSubmissionJSON)) v.Add("state", requestObject.State) + interactionDetailsBytes, _ := json.Marshal(map[string]string{"transaction_id": requestObject.State}) + + v.Add("interaction_details", base64.StdEncoding.EncodeToString(interactionDetailsBytes)) + f.perfInfo.CreateAuthorizedResponse = time.Since(start) return f.postAuthorizationResponse(ctx, requestObject.ResponseURI, []byte(v.Encode())) diff --git a/docs/v1/openapi.yaml b/docs/v1/openapi.yaml index 278245d2a..16e5f1b3d 100644 --- a/docs/v1/openapi.yaml +++ b/docs/v1/openapi.yaml @@ -2316,12 +2316,14 @@ components: credentials: type: array items: - $ref: '#/components/schemas/AcpRequestItem' + $ref: '#/components/schemas/AckRequestItem' + interaction_details: + type: object required: - credentials - AcpRequestItem: + AckRequestItem: type: object - description: AcpRequestItem + description: AckRequestItem properties: notification_id: type: string diff --git a/pkg/restapi/v1/oidc4ci/controller.go b/pkg/restapi/v1/oidc4ci/controller.go index 9c2152ac2..606e1d91c 100644 --- a/pkg/restapi/v1/oidc4ci/controller.go +++ b/pkg/restapi/v1/oidc4ci/controller.go @@ -648,11 +648,12 @@ func (c *Controller) OidcAcknowledgement(e echo.Context) error { var finalErr error for _, r := range body.Credentials { if err := c.ackService.Ack(req.Context(), oidc4ci.AckRemote{ - HashedToken: hashToken(token), - ID: r.NotificationId, - Event: r.Event, - EventDescription: lo.FromPtr(r.EventDescription), - IssuerIdentifier: lo.FromPtr(r.IssuerIdentifier), + HashedToken: hashToken(token), + ID: r.NotificationId, + Event: r.Event, + EventDescription: lo.FromPtr(r.EventDescription), + IssuerIdentifier: lo.FromPtr(r.IssuerIdentifier), + InteractionDetails: lo.FromPtr(body.InteractionDetails), }); err != nil { finalErr = errors.Join(finalErr, err) } diff --git a/pkg/restapi/v1/oidc4ci/controller_test.go b/pkg/restapi/v1/oidc4ci/controller_test.go index e62e08df7..36a1ec727 100644 --- a/pkg/restapi/v1/oidc4ci/controller_test.go +++ b/pkg/restapi/v1/oidc4ci/controller_test.go @@ -4559,12 +4559,17 @@ func TestController_Ack(t *testing.T) { assert.Equal(t, "tx_id", remote.ID) assert.Equal(t, "credential_accepted", remote.Event) assert.Equal(t, "err_txt", remote.EventDescription) + assert.Equal(t, map[string]interface{}{ + "userId": "userId", + "transactionId": "transactionId", + }, remote.InteractionDetails) return nil }) req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer([]byte(`{ - "credentials" : [{"notification_id" : "tx_id", "event" : "credential_accepted", "event_description" : "err_txt"}] + "credentials" : [{"notification_id" : "tx_id", "event" : "credential_accepted", "event_description" : "err_txt"}], + "interaction_details": {"userId": "userId", "transactionId": "transactionId"} }`))) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) req.Header.Set("Authorization", "Bearer xxxx") diff --git a/pkg/restapi/v1/oidc4ci/openapi.gen.go b/pkg/restapi/v1/oidc4ci/openapi.gen.go index 16dcc329b..534d3e92e 100644 --- a/pkg/restapi/v1/oidc4ci/openapi.gen.go +++ b/pkg/restapi/v1/oidc4ci/openapi.gen.go @@ -47,11 +47,12 @@ type AckErrorResponse struct { // Ack response. type AckRequest struct { - Credentials []AcpRequestItem `json:"credentials"` + Credentials []AckRequestItem `json:"credentials"` + InteractionDetails *map[string]interface{} `json:"interaction_details,omitempty"` } -// AcpRequestItem -type AcpRequestItem struct { +// AckRequestItem +type AckRequestItem struct { // Type of the notification event. Event string `json:"event"` diff --git a/pkg/restapi/v1/verifier/controller.go b/pkg/restapi/v1/verifier/controller.go index 5bd67964f..d5b8a6038 100644 --- a/pkg/restapi/v1/verifier/controller.go +++ b/pkg/restapi/v1/verifier/controller.go @@ -11,6 +11,7 @@ package verifier import ( "context" + "encoding/base64" "encoding/json" "errors" "fmt" @@ -67,6 +68,7 @@ type rawAuthorizationResponse struct { Error string ErrorDescription string State string + InteractionDetails map[string]interface{} } type IDTokenClaims struct { @@ -464,9 +466,10 @@ func (c *Controller) CheckAuthorizationResponse(e echo.Context) error { // Error authorization response // Spec: https://openid.github.io/OpenID4VP/openid-4-verifiable-presentations-wg-draft.html#section-6.4 return c.oidc4VPService.HandleWalletNotification(ctx, &oidc4vp.WalletNotification{ - TxID: oidc4vp.TxID(rawAuthResp.State), - Error: rawAuthResp.Error, - ErrorDescription: rawAuthResp.ErrorDescription, + TxID: oidc4vp.TxID(rawAuthResp.State), + Error: rawAuthResp.Error, + ErrorDescription: rawAuthResp.ErrorDescription, + InteractionDetails: rawAuthResp.InteractionDetails, }) } @@ -645,10 +648,11 @@ func (c *Controller) verifyAuthorizationResponseTokens( } return &oidc4vp.AuthorizationResponseParsed{ - CustomScopeClaims: idTokenClaims.CustomScopeClaims, - VPTokens: processedVPTokens, - AttestationVP: idTokenClaims.AttestationVP, - Attachments: idTokenClaims.Attachments, + CustomScopeClaims: idTokenClaims.CustomScopeClaims, + VPTokens: processedVPTokens, + AttestationVP: idTokenClaims.AttestationVP, + Attachments: idTokenClaims.Attachments, + InteractionDetails: authResp.InteractionDetails, }, nil } @@ -865,6 +869,28 @@ func decodeAuthorizationResponse(ctx echo.Context) (*rawAuthorizationResponse, e return nil, err } + var rawInteractionDetails string + err = decodeFormValue(&rawInteractionDetails, "interaction_details", req.PostForm) + if err == nil { + var rawInteractionDetailsBytes []byte + + rawInteractionDetailsBytes, err = base64.StdEncoding.DecodeString(rawInteractionDetails) + if err != nil { + return nil, resterr.NewValidationError( + resterr.InvalidValue, + "interaction_details", + fmt.Errorf("base64 decode: %w", err)) + } + + err = json.Unmarshal(rawInteractionDetailsBytes, &res.InteractionDetails) + if err != nil { + return nil, resterr.NewValidationError( + resterr.InvalidValue, + "interaction_details", + fmt.Errorf("json decode: %w", err)) + } + } + err = decodeFormValue(&res.Error, "error", req.PostForm) if err == nil { // Error authorization response diff --git a/pkg/restapi/v1/verifier/controller_test.go b/pkg/restapi/v1/verifier/controller_test.go index 8991effbd..05a6f1c65 100644 --- a/pkg/restapi/v1/verifier/controller_test.go +++ b/pkg/restapi/v1/verifier/controller_test.go @@ -10,6 +10,7 @@ import ( "bytes" "context" _ "embed" + "encoding/base64" "encoding/json" "errors" "fmt" @@ -23,7 +24,6 @@ import ( "github.com/labstack/echo/v4" "github.com/samber/lo" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" vdrmock "github.com/trustbloc/did-go/vdr/mock" "github.com/trustbloc/kms-go/spi/kms" "github.com/trustbloc/vc-go/presexch" @@ -149,21 +149,21 @@ func TestController_PostVerifyCredentials(t *testing.T) { t.Run("Success JSON-LD", func(t *testing.T) { c := createContextWithBody([]byte(sampleVCJsonLD)) err := controller.PostVerifyCredentials(c, profileID, profileVersion) - require.NoError(t, err) + assert.NoError(t, err) }) t.Run("Success JWT", func(t *testing.T) { c := createContextWithBody([]byte(sampleVCJWT)) err := controller.PostVerifyCredentials(c, profileID, profileVersion) - require.NoError(t, err) + assert.NoError(t, err) }) t.Run("Failed", func(t *testing.T) { c := createContextWithBody([]byte("abc")) err := controller.PostVerifyCredentials(c, profileID, profileVersion) - require.Error(t, err) + assert.Error(t, err) }) } @@ -198,11 +198,11 @@ func TestController_VerifyCredentials(t *testing.T) { var body VerifyCredentialData err := util.ReadBody(c, &body) - require.NoError(t, err) + assert.NoError(t, err) rsp, err := controller.verifyCredential(c.Request().Context(), &body, profileID, profileVersion, tenantID) - require.NoError(t, err) - require.Equal(t, &VerifyCredentialResponse{Checks: &[]VerifyCredentialCheckResult{{}}}, rsp) + assert.NoError(t, err) + assert.Equal(t, &VerifyCredentialResponse{Checks: &[]VerifyCredentialCheckResult{{}}}, rsp) }) t.Run("Success JWT", func(t *testing.T) { @@ -211,11 +211,11 @@ func TestController_VerifyCredentials(t *testing.T) { var body VerifyCredentialData err := util.ReadBody(c, &body) - require.NoError(t, err) + assert.NoError(t, err) rsp, err := controller.verifyCredential(c.Request().Context(), &body, profileID, profileVersion, tenantID) - require.NoError(t, err) - require.Equal(t, &VerifyCredentialResponse{Checks: &[]VerifyCredentialCheckResult{{}}}, rsp) + assert.NoError(t, err) + assert.Equal(t, &VerifyCredentialResponse{Checks: &[]VerifyCredentialCheckResult{{}}}, rsp) }) t.Run("Failed", func(t *testing.T) { @@ -284,10 +284,10 @@ func TestController_VerifyCredentials(t *testing.T) { e := testCase.getCtx() err := util.ReadBody(e, &body) - require.NoError(t, err) + assert.NoError(t, err) rsp, err := failedController.verifyCredential(e.Request().Context(), &body, profileID, profileVersion, tenantID) - require.Error(t, err) - require.Nil(t, rsp) + assert.Error(t, err) + assert.Nil(t, rsp) }) } }) @@ -321,21 +321,21 @@ func TestController_PostVerifyPresentation(t *testing.T) { t.Run("Success JSON-LD", func(t *testing.T) { c := createContextWithBody([]byte(sampleVPJsonLD)) err := controller.PostVerifyPresentation(c, profileID, profileVersion) - require.NoError(t, err) + assert.NoError(t, err) }) t.Run("Success JWT", func(t *testing.T) { c := createContextWithBody([]byte(sampleVPJWT)) err := controller.PostVerifyPresentation(c, profileID, profileVersion) - require.NoError(t, err) + assert.NoError(t, err) }) t.Run("Failed", func(t *testing.T) { c := createContextWithBody([]byte("abc")) err := controller.PostVerifyPresentation(c, profileID, profileVersion) - require.Error(t, err) + assert.Error(t, err) }) } @@ -370,11 +370,11 @@ func TestController_VerifyPresentation(t *testing.T) { var body VerifyPresentationData err := util.ReadBody(c, &body) - require.NoError(t, err) + assert.NoError(t, err) rsp, err := controller.verifyPresentation(c.Request().Context(), &body, profileID, profileVersion, tenantID) - require.NoError(t, err) - require.Equal(t, &VerifyPresentationResponse{Checks: &[]VerifyPresentationCheckResult{{}}}, rsp) + assert.NoError(t, err) + assert.Equal(t, &VerifyPresentationResponse{Checks: &[]VerifyPresentationCheckResult{{}}}, rsp) }) t.Run("Success JWT", func(t *testing.T) { @@ -383,11 +383,11 @@ func TestController_VerifyPresentation(t *testing.T) { var body VerifyPresentationData err := util.ReadBody(c, &body) - require.NoError(t, err) + assert.NoError(t, err) rsp, err := controller.verifyPresentation(c.Request().Context(), &body, profileID, profileVersion, tenantID) - require.NoError(t, err) - require.Equal(t, &VerifyPresentationResponse{Checks: &[]VerifyPresentationCheckResult{{}}}, rsp) + assert.NoError(t, err) + assert.Equal(t, &VerifyPresentationResponse{Checks: &[]VerifyPresentationCheckResult{{}}}, rsp) }) t.Run("Failed", func(t *testing.T) { @@ -456,10 +456,10 @@ func TestController_VerifyPresentation(t *testing.T) { e := testCase.getCtx() err := util.ReadBody(e, &body) - require.NoError(t, err) + assert.NoError(t, err) rsp, err := failedController.verifyPresentation(e.Request().Context(), &body, profileID, profileVersion, tenantID) - require.Error(t, err) - require.Nil(t, rsp) + assert.Error(t, err) + assert.Nil(t, rsp) }) } }) @@ -501,7 +501,7 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { ) presentationSubmission, err := json.Marshal(map[string]interface{}{}) - require.NoError(t, err) + assert.NoError(t, err) body := "vp_token=" + vpToken + "&id_token=" + signedClaimsJWTResult.JWT + @@ -523,7 +523,7 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { }) err = c.CheckAuthorizationResponse(ctx) - require.NoError(t, err) + assert.NoError(t, err) }) t.Run("Success LDP", func(t *testing.T) { @@ -555,10 +555,10 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { }) vpToken, err := vpSigned.MarshalJSON() - require.NoError(t, err) + assert.NoError(t, err) presentationSubmission, err := json.Marshal(map[string]interface{}{}) - require.NoError(t, err) + assert.NoError(t, err) mockEventSvc := NewMockeventService(gomock.NewController(t)) mockEventSvc.EXPECT().Publish(gomock.Any(), spi.VerifierEventTopic, gomock.Any()).Times(0) @@ -577,13 +577,17 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { VPToken: []string{string(vpToken)}, PresentationSubmission: string(presentationSubmission), State: "txid", + InteractionDetails: map[string]interface{}{ + "key1": "value1", + }, }, ) - require.NoError(t, err) + assert.NoError(t, err) - require.Nil(t, authorisationResponseParsed.CustomScopeClaims) - require.Contains(t, authorisationResponseParsed.VPTokens[0].Presentation.Type, "PresentationSubmission") + assert.Nil(t, authorisationResponseParsed.CustomScopeClaims) + assert.Contains(t, authorisationResponseParsed.VPTokens[0].Presentation.Type, "PresentationSubmission") + assert.Equal(t, map[string]interface{}{"key1": "value1"}, authorisationResponseParsed.InteractionDetails) }) t.Run("Success LDP With Attachments", func(t *testing.T) { @@ -620,10 +624,10 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { }) vpToken, err := vpSigned.MarshalJSON() - require.NoError(t, err) + assert.NoError(t, err) presentationSubmission, err := json.Marshal(map[string]interface{}{}) - require.NoError(t, err) + assert.NoError(t, err) mockEventSvc := NewMockeventService(gomock.NewController(t)) mockEventSvc.EXPECT().Publish(gomock.Any(), spi.VerifierEventTopic, gomock.Any()).Times(0) @@ -645,14 +649,15 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { }, ) - require.NoError(t, err) + assert.NoError(t, err) - require.Nil(t, authorisationResponseParsed.CustomScopeClaims) - require.Contains(t, authorisationResponseParsed.VPTokens[0].Presentation.Type, "PresentationSubmission") + assert.Nil(t, authorisationResponseParsed.CustomScopeClaims) + assert.Contains(t, authorisationResponseParsed.VPTokens[0].Presentation.Type, "PresentationSubmission") - require.Len(t, authorisationResponseParsed.Attachments, 1) - require.EqualValues(t, "data:image/svg;base64,YmFzZTY0Y29udGVudC1odHRwczovL2xvY2FsaG9zdC9jYXQucG5n", + assert.Len(t, authorisationResponseParsed.Attachments, 1) + assert.EqualValues(t, "data:image/svg;base64,YmFzZTY0Y29udGVudC1odHRwczovL2xvY2FsaG9zdC9jYXQucG5n", authorisationResponseParsed.Attachments["id1"]) + assert.Nil(t, authorisationResponseParsed.InteractionDetails) }) t.Run("Success JWT ID1", func(t *testing.T) { @@ -713,9 +718,10 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { }, ) - require.NoError(t, err) - require.Equal(t, customScopeClaims, responseParsed.CustomScopeClaims) - require.Contains(t, responseParsed.VPTokens[0].Presentation.Type, "PresentationSubmission") + assert.NoError(t, err) + assert.Equal(t, customScopeClaims, responseParsed.CustomScopeClaims) + assert.Contains(t, responseParsed.VPTokens[0].Presentation.Type, "PresentationSubmission") + assert.Nil(t, responseParsed.InteractionDetails) }) t.Run("Success CWT", func(t *testing.T) { @@ -776,22 +782,27 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { }, ) - require.NoError(t, err) - require.Equal(t, customScopeClaims, responseParsed.CustomScopeClaims) - require.Contains(t, responseParsed.VPTokens[0].Presentation.Type, "PresentationSubmission") + assert.NoError(t, err) + assert.Equal(t, customScopeClaims, responseParsed.CustomScopeClaims) + assert.Contains(t, responseParsed.VPTokens[0].Presentation.Type, "PresentationSubmission") + assert.Nil(t, responseParsed.InteractionDetails) }) t.Run("Success error AR response", func(t *testing.T) { + interactionDetails := base64.StdEncoding.EncodeToString([]byte(`{"key1":"value1"}`)) + body := "error=invalid_request" + "&error_description=unsupported%20client_id_scheme" + - "&state=txid" + "&state=txid" + + "&interaction_details=" + interactionDetails ctx := createContextApplicationForm([]byte(body)) svc.EXPECT().HandleWalletNotification(gomock.Any(), &oidc4vp.WalletNotification{ - TxID: oidc4vp.TxID("txid"), - Error: "invalid_request", - ErrorDescription: "unsupported client_id_scheme", + TxID: oidc4vp.TxID("txid"), + Error: "invalid_request", + ErrorDescription: "unsupported client_id_scheme", + InteractionDetails: map[string]interface{}{"key1": "value1"}, }).Return(nil) c := NewController(&Config{ @@ -800,7 +811,7 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { }) err := c.CheckAuthorizationResponse(ctx) - require.NoError(t, err) + assert.NoError(t, err) }) t.Run("Error: failed to HandleWalletNotification", func(t *testing.T) { @@ -822,7 +833,7 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { }) err := c.CheckAuthorizationResponse(ctx) - require.ErrorContains(t, err, "handle wallet notification error") + assert.ErrorContains(t, err, "handle wallet notification error") }) t.Run("Presentation submission missed", func(t *testing.T) { @@ -873,7 +884,7 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { ep := &oidc4ci.EventPayload{} jsonData, err := json.Marshal(msg.Data.(map[string]interface{})) - require.NoError(t, err) + assert.NoError(t, err) assert.NoError(t, json.Unmarshal(jsonData, ep)) @@ -981,7 +992,7 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { ) presentationSubmission, err := json.Marshal(map[string]interface{}{}) - require.NoError(t, err) + assert.NoError(t, err) body := "vp_token=" + vpToken + "&id_token=" + signedClaimsJWTResult.JWT + @@ -1037,7 +1048,7 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { ) presentationSubmission, err := json.Marshal(map[string]interface{}{}) - require.NoError(t, err) + assert.NoError(t, err) body := "vp_token=" + vpToken + "&id_token=" + signedClaimsJWTResult.JWT + @@ -1094,7 +1105,7 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { ) presentationSubmission, err := json.Marshal(map[string]interface{}{}) - require.NoError(t, err) + assert.NoError(t, err) body := "vp_token=" + vpToken + "&id_token=" + signedClaimsJWTResult.JWT + @@ -1149,7 +1160,7 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { ) presentationSubmission, err := json.Marshal(map[string]interface{}{}) - require.NoError(t, err) + assert.NoError(t, err) body := "vp_token=" + vpTokenSignedJWTResult.JWT + "&id_token=" + vpTokenSignedJWTResult.JWT + @@ -1206,7 +1217,7 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { ) presentationSubmission, err := json.Marshal(map[string]interface{}{}) - require.NoError(t, err) + assert.NoError(t, err) body := "vp_token=" + vpToken + "&id_token=" + signedClaimsJWTResult.JWT + @@ -1261,7 +1272,7 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { ) presentationSubmission, err := json.Marshal(map[string]interface{}{}) - require.NoError(t, err) + assert.NoError(t, err) body := "vp_token=" + vpTokenSignedJWTResult.JWT + "&id_token=" + signedClaimsJWTResult.JWT + @@ -1309,7 +1320,7 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { ) presentationSubmission, err := json.Marshal(map[string]interface{}{}) - require.NoError(t, err) + assert.NoError(t, err) body := "vp_token=" + vpToken + "&id_token=" + signedClaimsJWTResult.JWT + @@ -1354,7 +1365,7 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { "PresentationSubmission", }, }).MarshalJSON() - require.NoError(t, err) + assert.NoError(t, err) vpSigned := testutil.SignedVP(t, vpb, @@ -1365,10 +1376,10 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { }) vpToken, err := vpSigned.Presentation.MarshalJSON() - require.NoError(t, err) + assert.NoError(t, err) presentationSubmission, err := json.Marshal(map[string]interface{}{}) - require.NoError(t, err) + assert.NoError(t, err) body := "vp_token=" + string(vpToken) + "&id_token=" + signedClaimsJWTResult.JWT + @@ -1425,10 +1436,10 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { }) vpToken, err := vpSigned.MarshalJSON() - require.NoError(t, err) + assert.NoError(t, err) presentationSubmission, err := json.Marshal(map[string]interface{}{}) - require.NoError(t, err) + assert.NoError(t, err) body := "vp_token=" + string(vpToken) + "&id_token=" + signedClaimsJWTResult.JWT + @@ -1484,10 +1495,10 @@ func TestController_CheckAuthorizationResponse(t *testing.T) { }) vpToken, err := vpSigned.MarshalJSON() - require.NoError(t, err) + assert.NoError(t, err) presentationSubmission, err := json.Marshal(map[string]interface{}{}) - require.NoError(t, err) + assert.NoError(t, err) body := "vp_token=" + string(vpToken) + "&id_token=" + signedClaimsJWTResult.JWT + @@ -1551,7 +1562,7 @@ func TestController_RetrieveInteractionsClaim(t *testing.T) { }) err := c.RetrieveInteractionsClaim(createContext("orgID1"), "txid") - require.NoError(t, err) + assert.NoError(t, err) }) t.Run("Success - delete claims error", func(t *testing.T) { @@ -1590,7 +1601,7 @@ func TestController_RetrieveInteractionsClaim(t *testing.T) { }) err := c.RetrieveInteractionsClaim(createContext("orgID1"), "txid") - require.NoError(t, err) + assert.NoError(t, err) }) t.Run("Error - claims expired", func(t *testing.T) { @@ -1626,8 +1637,8 @@ func TestController_RetrieveInteractionsClaim(t *testing.T) { }) err := c.RetrieveInteractionsClaim(createContext("orgID1"), "txid") - require.Error(t, err) - require.Contains(t, err.Error(), + assert.Error(t, err) + assert.Contains(t, err.Error(), "claims are either retrieved or expired for transaction 'txid'") }) @@ -1663,8 +1674,8 @@ func TestController_RetrieveInteractionsClaim(t *testing.T) { }) err := c.RetrieveInteractionsClaim(createContext("orgID1"), "txid") - require.Error(t, err) - require.Contains(t, err.Error(), "claims were not received for transaction 'txid'") + assert.Error(t, err) + assert.Contains(t, err.Error(), "claims were not received for transaction 'txid'") }) t.Run("Tx not found", func(t *testing.T) { @@ -1745,16 +1756,35 @@ func TestController_RetrieveInteractionsClaim(t *testing.T) { } func TestController_decodeAuthorizationResponse(t *testing.T) { - t.Run("Success", func(t *testing.T) { + t.Run("Success: without interaction details", func(t *testing.T) { + body := "vp_token=toke1&" + + "&id_token=toke2" + + "&state=txid" + + ctx := createContextApplicationForm([]byte(body)) + + ar, err := decodeAuthorizationResponse(ctx) + assert.NoError(t, err) + assert.NotNil(t, ar) + }) + + t.Run("Success: with interaction details", func(t *testing.T) { + interactionDetails := map[string]interface{}{ + "key1": "value1", + } + interactionDetailsBytes, err := json.Marshal(interactionDetails) + assert.NoError(t, err) + body := "vp_token=toke1&" + "&id_token=toke2" + + "&interaction_details=" + base64.StdEncoding.EncodeToString(interactionDetailsBytes) + "&state=txid" ctx := createContextApplicationForm([]byte(body)) ar, err := decodeAuthorizationResponse(ctx) - require.NoError(t, err) - require.NotNil(t, ar) + assert.NoError(t, err) + assert.NotNil(t, ar) }) t.Run("Success: authorization error response", func(t *testing.T) { @@ -1765,12 +1795,12 @@ func TestController_decodeAuthorizationResponse(t *testing.T) { ctx := createContextApplicationForm([]byte(body)) ar, err := decodeAuthorizationResponse(ctx) - require.NoError(t, err) - require.NotNil(t, ar) + assert.NoError(t, err) + assert.NotNil(t, ar) - require.Equal(t, ar.State, "txid") - require.Equal(t, ar.Error, "invalid_request") - require.Equal(t, ar.ErrorDescription, "unsupported client_id_scheme") + assert.Equal(t, ar.State, "txid") + assert.Equal(t, ar.Error, "invalid_request") + assert.Equal(t, ar.ErrorDescription, "unsupported client_id_scheme") }) t.Run("Success - vp token is an array", func(t *testing.T) { @@ -1781,8 +1811,8 @@ func TestController_decodeAuthorizationResponse(t *testing.T) { ctx := createContextApplicationForm([]byte(body)) ar, err := decodeAuthorizationResponse(ctx) - require.NoError(t, err) - require.NotNil(t, ar) + assert.NoError(t, err) + assert.NotNil(t, ar) }) t.Run("Missed state", func(t *testing.T) { @@ -1806,6 +1836,32 @@ func TestController_decodeAuthorizationResponse(t *testing.T) { requireValidationError(t, resterr.InvalidValue, "state", err) }) + t.Run("Error: interaction_details contains invalid data: base64", func(t *testing.T) { + body := "vp_token=toke1&" + + "&id_token=toke2" + + "&interaction_details= " + + "&state=txid" + + ctx := createContextApplicationForm([]byte(body)) + + _, err := decodeAuthorizationResponse(ctx) + assert.ErrorContains(t, err, "base64 decode") + requireValidationError(t, resterr.InvalidValue, "interaction_details", err) + }) + + t.Run("Error: interaction_details contains invalid data: json", func(t *testing.T) { + body := "vp_token=toke1&" + + "&id_token=toke2" + + "&interaction_details=abcd" + + "&state=txid" + + ctx := createContextApplicationForm([]byte(body)) + + _, err := decodeAuthorizationResponse(ctx) + assert.ErrorContains(t, err, "json decode") + requireValidationError(t, resterr.InvalidValue, "interaction_details", err) + }) + t.Run("Error: authorization error response: missed error_description", func(t *testing.T) { body := "error=invalid_request" + "&state=txid" @@ -2114,40 +2170,40 @@ func Test_getVerifyPresentationOptions(t *testing.T) { } func requireAuthError(t *testing.T, actual error) { - require.IsType(t, &resterr.CustomError{}, actual) + assert.IsType(t, &resterr.CustomError{}, actual) actualErr := &resterr.CustomError{} - require.True(t, errors.As(actual, &actualErr)) + assert.True(t, errors.As(actual, &actualErr)) - require.Equal(t, resterr.Unauthorized, actualErr.Code) + assert.Equal(t, resterr.Unauthorized, actualErr.Code) } func requireValidationError(t *testing.T, expectedCode resterr.ErrorCode, incorrectValueName string, actual error) { - require.IsType(t, &resterr.CustomError{}, actual) + assert.IsType(t, &resterr.CustomError{}, actual) actualErr := &resterr.CustomError{} - require.True(t, errors.As(actual, &actualErr)) + assert.True(t, errors.As(actual, &actualErr)) - require.Equal(t, expectedCode, actualErr.Code) - require.Equal(t, incorrectValueName, actualErr.IncorrectValue) - require.Error(t, actualErr.Err) + assert.Equal(t, expectedCode, actualErr.Code) + assert.Equal(t, incorrectValueName, actualErr.IncorrectValue) + assert.Error(t, actualErr.Err) } func requireSystemError(t *testing.T, component, failedOperation string, actual error) { //nolint: unparam - require.IsType(t, &resterr.CustomError{}, actual) + assert.IsType(t, &resterr.CustomError{}, actual) actualErr := &resterr.CustomError{} - require.True(t, errors.As(actual, &actualErr)) - require.Equal(t, resterr.SystemError, actualErr.Code) - require.Equal(t, component, actualErr.Component) - require.Equal(t, failedOperation, actualErr.FailedOperation) - require.Error(t, actualErr.Err) + assert.True(t, errors.As(actual, &actualErr)) + assert.Equal(t, resterr.SystemError, actualErr.Code) + assert.Equal(t, component, actualErr.Component) + assert.Equal(t, failedOperation, actualErr.FailedOperation) + assert.Error(t, actualErr.Err) } func requireCustomError(t *testing.T, expectedCode resterr.ErrorCode, actual error) { - require.IsType(t, &resterr.CustomError{}, actual) + assert.IsType(t, &resterr.CustomError{}, actual) actualErr := &resterr.CustomError{} - require.True(t, errors.As(actual, &actualErr)) + assert.True(t, errors.As(actual, &actualErr)) - require.Equal(t, expectedCode, actualErr.Code) - require.Error(t, actualErr.Err) + assert.Equal(t, expectedCode, actualErr.Code) + assert.Error(t, actualErr.Err) } func TestController_AuthFailed(t *testing.T) { @@ -2217,7 +2273,7 @@ func TestController_InitiateOidcInteraction(t *testing.T) { }) c := createContext(tenantID) err := controller.InitiateOidcInteraction(c, profileID, profileVersion) - require.NoError(t, err) + assert.NoError(t, err) }) t.Run("Profile not found", func(t *testing.T) { @@ -2269,8 +2325,8 @@ func TestController_initiateOidcInteraction(t *testing.T) { }, }) - require.NoError(t, err) - require.NotNil(t, result) + assert.NoError(t, err) + assert.NotNil(t, result) }) t.Run("Success - With Presentation Definition and PD filters", func(t *testing.T) { @@ -2285,7 +2341,7 @@ func TestController_initiateOidcInteraction(t *testing.T) { var pd presexch.PresentationDefinition err := json.Unmarshal([]byte(testPD), &pd) - require.NoError(t, err) + assert.NoError(t, err) result, err := controller.initiateOidcInteraction(context.TODO(), &InitiateOIDC4VPData{ @@ -2304,8 +2360,8 @@ func TestController_initiateOidcInteraction(t *testing.T) { }, }) - require.NoError(t, err) - require.NotNil(t, result) + assert.NoError(t, err) + assert.NotNil(t, result) }) t.Run("Success - With Multiple Presentation Definitions, "+ @@ -2321,12 +2377,12 @@ func TestController_initiateOidcInteraction(t *testing.T) { var pd presexch.PresentationDefinition err := json.Unmarshal([]byte(testPD), &pd) - require.NoError(t, err) + assert.NoError(t, err) var pd2 presexch.PresentationDefinition err = json.Unmarshal([]byte(testPD), &pd2) - require.NoError(t, err) + assert.NoError(t, err) pd2.ID = "some-id" @@ -2350,8 +2406,8 @@ func TestController_initiateOidcInteraction(t *testing.T) { }, }) - require.NoError(t, err) - require.NotNil(t, result) + assert.NoError(t, err) + assert.NotNil(t, result) }) t.Run("Error - With Multiple Presentation Definitions, "+ @@ -2367,12 +2423,12 @@ func TestController_initiateOidcInteraction(t *testing.T) { var pd presexch.PresentationDefinition err := json.Unmarshal([]byte(testPD), &pd) - require.NoError(t, err) + assert.NoError(t, err) var pd2 presexch.PresentationDefinition err = json.Unmarshal([]byte(testPD), &pd2) - require.NoError(t, err) + assert.NoError(t, err) pd2.ID = "some-other-id" @@ -2393,9 +2449,9 @@ func TestController_initiateOidcInteraction(t *testing.T) { }, }) - require.Error(t, err) - require.Nil(t, result) - require.Contains(t, err.Error(), + assert.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "invalid-value[presentationDefinitionID]: presentation definition id= not found for profile with id=profile-id") }) @@ -2413,7 +2469,7 @@ func TestController_initiateOidcInteraction(t *testing.T) { var pd presexch.PresentationDefinition err := json.Unmarshal([]byte(testPDWithFieldIDs), &pd) - require.NoError(t, err) + assert.NoError(t, err) result, err := controller.initiateOidcInteraction(context.TODO(), &InitiateOIDC4VPData{ @@ -2431,9 +2487,9 @@ func TestController_initiateOidcInteraction(t *testing.T) { }, }) - require.Error(t, err) - require.Nil(t, result) - require.Contains(t, err.Error(), "invalid-value[presentationDefinitionFilters]: failed to compile regex") + assert.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "invalid-value[presentationDefinitionFilters]: failed to compile regex") }) t.Run("Should have oidc config", func(t *testing.T) { @@ -2504,8 +2560,8 @@ func TestController_initiateOidcInteraction(t *testing.T) { func TestMatchField(t *testing.T) { t.Run("Success", func(t *testing.T) { _, matched, err := matchField(nil, "id") - require.NoError(t, err) - require.False(t, matched) + assert.NoError(t, err) + assert.False(t, matched) }) } @@ -2514,11 +2570,11 @@ func TestCopyPresentationDefinition(t *testing.T) { var pd *presexch.PresentationDefinition err := json.Unmarshal([]byte(testPDWithFieldIDs), &pd) - require.NoError(t, err) + assert.NoError(t, err) copied, err := copyPresentationDefinition(pd) - require.NoError(t, err) - require.Equal(t, pd, copied) + assert.NoError(t, err) + assert.Equal(t, pd, copied) }) } @@ -2527,56 +2583,56 @@ func TestApplyFieldsFilter(t *testing.T) { var pd presexch.PresentationDefinition err := json.Unmarshal([]byte(testPDWithFieldIDs), &pd) - require.NoError(t, err) + assert.NoError(t, err) result, err := applyFieldsFilter(&pd, []string{"degree_type_id"}) - require.NoError(t, err) + assert.NoError(t, err) - require.Len(t, result.InputDescriptors[0].Constraints.Fields, 0) - require.Len(t, result.InputDescriptors[1].Constraints.Fields, 1) + assert.Len(t, result.InputDescriptors[0].Constraints.Fields, 0) + assert.Len(t, result.InputDescriptors[1].Constraints.Fields, 1) }) t.Run("Fail - field not found", func(t *testing.T) { var pd presexch.PresentationDefinition err := json.Unmarshal([]byte(testPDWithFieldIDs), &pd) - require.NoError(t, err) + assert.NoError(t, err) _, err = applyFieldsFilter(&pd, []string{"degree_type_id", "random_field"}) - require.ErrorContains(t, err, "field random_field not found") + assert.ErrorContains(t, err, "field random_field not found") }) t.Run("Success - empty string filter(accept fields with empty ID)", func(t *testing.T) { var pd presexch.PresentationDefinition err := json.Unmarshal([]byte(testPD), &pd) - require.NoError(t, err) + assert.NoError(t, err) result, err := applyFieldsFilter(&pd, []string{""}) - require.NoError(t, err) + assert.NoError(t, err) - require.Len(t, result.InputDescriptors[0].Constraints.Fields, 1) - require.Len(t, result.InputDescriptors[1].Constraints.Fields, 1) + assert.Len(t, result.InputDescriptors[0].Constraints.Fields, 1) + assert.Len(t, result.InputDescriptors[1].Constraints.Fields, 1) }) t.Run("Success - supply fields filter", func(t *testing.T) { var pd presexch.PresentationDefinition err := json.Unmarshal([]byte(testPDWithFieldIDs), &pd) - require.NoError(t, err) + assert.NoError(t, err) result, err := applyFieldsFilter(&pd, []string{"degree_type_id"}) - require.NoError(t, err) + assert.NoError(t, err) - require.Len(t, result.InputDescriptors[0].Constraints.Fields, 0) - require.Len(t, result.InputDescriptors[1].Constraints.Fields, 1) + assert.Len(t, result.InputDescriptors[0].Constraints.Fields, 0) + assert.Len(t, result.InputDescriptors[1].Constraints.Fields, 1) }) t.Run("Success - test prefix filter", func(t *testing.T) { var pd presexch.PresentationDefinition err := json.Unmarshal([]byte(testPD), &pd) - require.NoError(t, err) + assert.NoError(t, err) const testPrefix = "*_test_prefix" @@ -2584,17 +2640,17 @@ func TestApplyFieldsFilter(t *testing.T) { pd.InputDescriptors[1].Constraints.Fields[0].ID = testPrefix + "_second" result, err := applyFieldsFilter(&pd, []string{testPrefix}) - require.NoError(t, err) + assert.NoError(t, err) - require.Len(t, result.InputDescriptors[0].Constraints.Fields, 1) - require.Len(t, result.InputDescriptors[1].Constraints.Fields, 1) + assert.Len(t, result.InputDescriptors[0].Constraints.Fields, 1) + assert.Len(t, result.InputDescriptors[1].Constraints.Fields, 1) }) t.Run("Fail - test invalid regex", func(t *testing.T) { var pd presexch.PresentationDefinition err := json.Unmarshal([]byte(testPD), &pd) - require.NoError(t, err) + assert.NoError(t, err) const testPrefix = `*[ ]\K(?