diff --git a/Readme.md b/Readme.md index c92596815..6e9ea5f14 100644 --- a/Readme.md +++ b/Readme.md @@ -41,6 +41,7 @@ go get github.com/adyen/adyen-go-api-library ```go import ( "github.com/adyen/adyen-go-api-library/src/checkout" + "github.com/adyen/adyen-go-api-library/src/common" "github.com/adyen/adyen-go-api-library/src/adyen" ) @@ -58,7 +59,8 @@ res, httpRes, err := client.Checkout.PaymentMethods(&checkout.PaymentMethodsRequ ```go import ( - "github.com/adyen/adyen-go-api-library/src/checkout" + "github.com/adyen/adyen-go-api-library/src/checkout" + "github.com/adyen/adyen-go-api-library/src/common" "github.com/adyen/adyen-go-api-library/src/adyen" ) @@ -77,7 +79,8 @@ res, httpRes, err := client.Checkout.PaymentMethods(&checkout.PaymentMethodsRequ ```go import ( - "github.com/adyen/adyen-go-api-library/src/recurring" + "github.com/adyen/adyen-go-api-library/src/recurring" + "github.com/adyen/adyen-go-api-library/src/common" "github.com/adyen/adyen-go-api-library/src/adyen" ) @@ -101,7 +104,8 @@ res, httpRes, err := client.Recurring.ListRecurringDetails(&recurring.RecurringD ```go import ( - "github.com/adyen/adyen-go-api-library/src/adyen" + "github.com/adyen/adyen-go-api-library/src/adyen" + "github.com/adyen/adyen-go-api-library/src/common" ) client := adyen.NewClient(&common.Config{ @@ -189,7 +193,7 @@ client = adyen.NewClient(&common.Config{ ## Support -If you have any problems, questions or suggestions, create an issue here or send your inquiry to support@adyen.com. +If you have a feature request, or spotted a bug or a technical problem, create a github issue. For other questions, contact our [support team](https://support.adyen.com/hc/en-us/requests/new?ticket_form_id=360000705420m). ## Contributing diff --git a/src/adyen/api.go b/src/adyen/api.go index 0827cdc83..3de7286ac 100644 --- a/src/adyen/api.go +++ b/src/adyen/api.go @@ -25,8 +25,6 @@ const ( EndpointTest = "https://pal-test.adyen.com" EndpointLive = "https://pal-live.adyen.com" EndpointLiveSuffix = "-pal-live.adyenpayments.com" - HppTest = "https://test.adyen.com/hpp" - HppLive = "https://live.adyen.com/hpp" MarketpayEndpointTest = "https://cal-test.adyen.com/cal/services" MarketpayEndpointLive = "https://cal-live.adyen.com/cal/services" MarketpayAccountAPIVersion = "v5" @@ -130,7 +128,7 @@ func NewClient(cfg *common.Config) *APIClient { cfg.DefaultHeader = make(map[string]string) } if cfg.UserAgent == "" { - cfg.UserAgent = fmt.Sprintf("%s %s/%s", cfg.ApplicationName, common.LibName, common.LibVersion) + cfg.UserAgent = fmt.Sprintf("%s/%s", common.LibName, common.LibVersion) } c := &APIClient{} @@ -195,7 +193,6 @@ func (c *APIClient) SetEnvironment(env common.Environment, liveEndpointURLPrefix if env == common.LiveEnv { c.client.Cfg.Environment = env c.client.Cfg.MarketPayEndpoint = MarketpayEndpointLive - c.client.Cfg.HppEndpoint = HppLive if liveEndpointURLPrefix != "" { c.client.Cfg.Endpoint = EndpointProtocol + liveEndpointURLPrefix + EndpointLiveSuffix c.client.Cfg.CheckoutEndpoint = EndpointProtocol + liveEndpointURLPrefix + CheckoutEndpointLiveSuffix @@ -208,7 +205,6 @@ func (c *APIClient) SetEnvironment(env common.Environment, liveEndpointURLPrefix c.client.Cfg.Environment = env c.client.Cfg.Endpoint = EndpointTest c.client.Cfg.MarketPayEndpoint = MarketpayEndpointTest - c.client.Cfg.HppEndpoint = HppTest c.client.Cfg.CheckoutEndpoint = CheckoutEndpointTest c.client.Cfg.TerminalApiCloudEndpoint = TerminalAPIEndpointTest } diff --git a/src/adyen/api_test.go b/src/adyen/api_test.go index 7bfcdda4e..bcdd3ef9d 100644 --- a/src/adyen/api_test.go +++ b/src/adyen/api_test.go @@ -74,10 +74,9 @@ func Test_api(t *testing.T) { }) client = NewClient(&common.Config{ - Username: USER, - Password: PASS, - Environment: "TEST", - ApplicationName: "adyen-api-go-library", + Username: USER, + Password: PASS, + Environment: "TEST", }) t.Run("Create a API request that uses basic auth and should pass", func(t *testing.T) { diff --git a/src/common/configuration.go b/src/common/configuration.go index 530d00cf3..162daa5b7 100644 --- a/src/common/configuration.go +++ b/src/common/configuration.go @@ -61,29 +61,21 @@ const ( const ( LibName = "adyen-go-api-library" - LibVersion = "0.0.1" + LibVersion = "1.0.0" ) // Config stores the configuration of the API client type Config struct { - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - MerchantAccount string `json:"merchantAccount,omitempty"` - Environment Environment `json:"environment,omitempty"` - Endpoint string `json:"endpoint,omitempty"` - MarketPayEndpoint string `json:"marketPayEndpoint,omitempty"` - // Application name: used as HTTP client User-Agent - ApplicationName string `json:"applicationName,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + MerchantAccount string `json:"merchantAccount,omitempty"` + Environment Environment `json:"environment,omitempty"` + Endpoint string `json:"endpoint,omitempty"` + MarketPayEndpoint string `json:"marketPayEndpoint,omitempty"` ApiKey string `json:"apiKey,omitempty"` ConnectionTimeoutMillis time.Duration `json:"connectionTimeoutMillis,omitempty"` - ReadTimeoutMillis time.Duration `json:"readTimeoutMillis,omitempty"` CertificatePath string `json:"certificatePath,omitempty"` - //HPP specific - HppEndpoint string `json:"hppEndpoint,omitempty"` - SkinCode string `json:"skinCode,omitempty"` - HmacKey string `json:"hmacKey,omitempty"` - //Checkout Specific CheckoutEndpoint string `json:"checkoutEndpoint,omitempty"` diff --git a/src/hmacvalidator/hmacvalidator.go b/src/hmacvalidator/hmacvalidator.go new file mode 100644 index 000000000..4ee909ccb --- /dev/null +++ b/src/hmacvalidator/hmacvalidator.go @@ -0,0 +1,87 @@ +package hmacvalidator + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "fmt" + "regexp" + "sort" + "strconv" + "strings" + + "github.com/adyen/adyen-go-api-library/src/notification" +) + +// CalculateHmac calculates the SHA-256 HMAC for the given data and key +func CalculateHmac(data interface{}, secret string) (string, error) { + switch val := data.(type) { + case string: + return encode(val, secret) + default: + src := GetDataToSign(data) + return encode(src, secret) + } +} + +// ValidateHmac calculates the HMAC of the notification request item and checks if it matches with the given key +func ValidateHmac(notificationRequestItem notification.NotificationRequestItem, key string) bool { + expectedSign, err := CalculateHmac(notificationRequestItem, key) + if err != nil { + return false + } + merchantSign := (*notificationRequestItem.AdditionalData)["HmacSignature"] + return expectedSign == merchantSign +} + +// GetDataToSign converts a notification request item to string, which later on can be used for calculating a HMAC +func GetDataToSign(notificationRequestItem interface{}) string { + switch item := notificationRequestItem.(type) { + case notification.NotificationRequestItem: + signedDataList := []string{ + item.PspReference, + item.OriginalReference, + item.MerchantAccountCode, + item.MerchantReference, + strconv.Itoa(int(item.Amount.Value)), + item.Amount.Currency, + item.EventCode, + item.Success, + } + return strings.Join(signedDataList, ":") + case map[string]string: + keys := make([]string, 0) + values := make([]string, 0) + + for k := range item { + keys = append(keys, replacer(k)) + } + sort.Strings(keys) + for _, k := range keys { + values = append(values, replacer(item[k])) + } + + return strings.Join(keys, ":") + ":" + strings.Join(values, ":") + default: + return "" + } +} + +func encode(data string, secret string) (string, error) { + key, err := hex.DecodeString(secret) + if err != nil { + return "", fmt.Errorf("failed to generate HMAC: %s", err.Error()) + } + h := hmac.New(sha256.New, key) + h.Write([]byte(data)) + return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil +} + +func replacer(s string) string { + re1 := regexp.MustCompile(`\\`) + re2 := regexp.MustCompile(`:`) + str := re1.ReplaceAllString(s, "\\\\") + str = re2.ReplaceAllString(str, "\\:") + return str +} diff --git a/src/hmacvalidator/hmacvalidator_test.go b/src/hmacvalidator/hmacvalidator_test.go new file mode 100644 index 000000000..70bfeee10 --- /dev/null +++ b/src/hmacvalidator/hmacvalidator_test.go @@ -0,0 +1,71 @@ +package hmacvalidator + +import ( + "testing" + "time" + + "github.com/adyen/adyen-go-api-library/src/notification" + "github.com/stretchr/testify/assert" +) + +const key = "DFB1EB5485895CFA84146406857104ABB4CBCABDC8AAF103A624C8F6A3EAAB00" +const expectedSign = "ipnxGCaUZ4l8TUW75a71/ghd2Fe5ffvX0pV4TLTntIc=" + +var eventDate = time.Date(1970, time.January, 01, 0, 0, 0, 0, time.UTC) +var notificationRequestItem = notification.NotificationRequestItem{ + AdditionalData: &map[string]interface{}{"HmacSignature": expectedSign}, + Amount: notification.Amount{ + Currency: "EUR", + Value: 1000, + }, + EventCode: "EVENT", + EventDate: &eventDate, + MerchantAccountCode: "merchantAccount", + MerchantReference: "reference", + OriginalReference: "originalReference", + PaymentMethod: "VISA", + PspReference: "pspReference", + Reason: "reason", + Success: "true", +} + +func Test_Hmacvalidator(t *testing.T) { + t.Run("GetDataToSign", func(t *testing.T) { + t.Run("Get correct data", func(t *testing.T) { + data := map[string]string{"MerchantAccount": "ACC", "CurrencyCode": "EUR"} + dataToSign := GetDataToSign(data) + assert.Equal(t, "CurrencyCode:MerchantAccount:EUR:ACC", dataToSign) + }) + t.Run("Get correct data with escaped characters", func(t *testing.T) { + data := map[string]string{"CurrencyCode": "EUR", "MerchantAccount": "ACC:\\", "SessionValidity": "2019-09-21T11:45:24.637Z"} + dataToSign := GetDataToSign(data) + assert.Equal(t, "CurrencyCode:MerchantAccount:SessionValidity:EUR:ACC\\:\\\\:2019-09-21T11\\:45\\:24.637Z", dataToSign) + }) + t.Run("Get correct data to sign", func(t *testing.T) { + data := GetDataToSign(notificationRequestItem) + assert.Equal(t, "pspReference:originalReference:merchantAccount:reference:1000:EUR:EVENT:true", data) + }) + }) + t.Run("CalculateHmac", func(t *testing.T) { + t.Run("Encrypt correctly", func(t *testing.T) { + data := "countryCode:currencyCode:merchantAccount:merchantReference:paymentAmount:sessionValidity:skinCode:NL:EUR:MagentoMerchantTest2:TEST-PAYMENT-2017-02-01-14\\:02\\:05:199:2017-02-02T14\\:02\\:05+01\\:00:PKz2KML1" + encrypted, err := CalculateHmac(data, key) + assert.Nil(t, err) + assert.Equal(t, "34oR8T1whkQWTv9P+SzKyp8zhusf9n0dpqrm9nsqSJs=", encrypted) + }) + t.Run("Get Valid HMAC", func(t *testing.T) { + enc, err := CalculateHmac(notificationRequestItem, key) + assert.Nil(t, err) + assert.Equal(t, expectedSign, enc) + }) + }) + t.Run("ValidateHmac", func(t *testing.T) { + t.Run("Validate HMAC", func(t *testing.T) { + assert.True(t, ValidateHmac(notificationRequestItem, key)) + }) + t.Run("Get Invalid HMAC", func(t *testing.T) { + notificationRequestItem.AdditionalData = &map[string]interface{}{"HmacSignature": "InvalidSignature"} + assert.False(t, ValidateHmac(notificationRequestItem, key)) + }) + }) +} diff --git a/tests/checkout_test.go b/tests/checkout_test.go index 8524c8ee6..bc144040f 100644 --- a/tests/checkout_test.go +++ b/tests/checkout_test.go @@ -8,6 +8,7 @@ package tests import ( "os" + "regexp" "testing" "github.com/adyen/adyen-go-api-library/src/adyen" @@ -46,10 +47,8 @@ func Test_Checkout(t *testing.T) { }) require.NotNil(t, err) - assert.Equal(t, "422 Unprocessable Entity: Required field countryCode not specified (validation: 158)", err.Error()) + assert.Regexp(t, regexp.MustCompile("422 Unprocessable Entity"), err.Error()) require.NotNil(t, httpRes) - assert.Equal(t, 422, httpRes.StatusCode) - assert.Equal(t, "422 Unprocessable Entity", httpRes.Status) require.NotNil(t, res) }) t.Run("Create an API request that should pass", func(t *testing.T) {