Skip to content

Commit

Permalink
Adding debug webhook verify method (#18)
Browse files Browse the repository at this point in the history
- This Closes #1
- Gofmt project again, whoops!
- Adding a more verbose verify webhook method that outputs an error for
each different thing that might go wrong with a webhook request. With
tests.
  • Loading branch information
rickywiens authored Sep 28, 2018
1 parent 85aba54 commit 12eded9
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 13 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.swp
coverage.*
.vscode
.idea
46 changes: 46 additions & 0 deletions oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
Expand Down Expand Up @@ -93,3 +95,47 @@ func (app App) VerifyWebhookRequest(httpRequest *http.Request) bool {

return hmac.Equal(actualMac, expectedMac)
}

// Verifies a webhook http request, sent by Shopify.
// The body of the request is still readable after invoking the method.
// This method has more verbose error output which is useful for debugging.
func (app App) VerifyWebhookRequestVerbose(httpRequest *http.Request) (bool, error) {
if app.ApiSecret == "" {
return false, errors.New("ApiSecret is empty")
}

shopifySha256 := httpRequest.Header.Get(shopifyChecksumHeader)
if shopifySha256 == "" {
return false, fmt.Errorf("header %s not set", shopifyChecksumHeader)
}

decodedReceivedHMAC, err := base64.StdEncoding.DecodeString(shopifySha256)
if err != nil {
return false, err
}
if len(decodedReceivedHMAC) != 32 {
return false, fmt.Errorf("received HMAC is not of length 32, it is of length %d", len(decodedReceivedHMAC))
}

mac := hmac.New(sha256.New, []byte(app.ApiSecret))
requestBody, err := ioutil.ReadAll(httpRequest.Body)
if err != nil {
return false, err
}

httpRequest.Body = ioutil.NopCloser(bytes.NewBuffer(requestBody))
if len(requestBody) == 0 {
return false, errors.New("request body is empty")
}

// Sha256 write doesn't actually return an error
mac.Write(requestBody)

computedHMAC := mac.Sum(nil)
HMACSame := hmac.Equal(decodedReceivedHMAC, computedHMAC)
if !HMACSame {
return HMACSame, fmt.Errorf("expected hash %x does not equal %x", computedHMAC, decodedReceivedHMAC)
}

return HMACSame, nil
}
134 changes: 124 additions & 10 deletions oauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import (
"net/url"
"testing"

"encoding/base64"
"errors"
"fmt"
"gopkg.in/jarcoal/httpmock.v1"
"net/http"
)

func TestAppAuthorizeUrl(t *testing.T) {
Expand Down Expand Up @@ -77,18 +81,128 @@ func TestVerifyWebhookRequest(t *testing.T) {
setup()
defer teardown()

hmac := "hMTq0K2x7oyOjoBwGYeTj5oxfnaVYXzbanUG9aajpKI="
message := "my secret message"
testClient := NewClient(App{}, "", "")
req, err := testClient.NewRequest("GET", "", message, nil)
if err != nil {
t.Fatalf("Webhook.verify err = %v, expected true", err)
cases := []struct {
hmac string
message string
expected bool
}{
{"hMTq0K2x7oyOjoBwGYeTj5oxfnaVYXzbanUG9aajpKI=", "my secret message", true},
{"wronghash", "my secret message", false},
{"wronghash", "", false},
{"hMTq0K2x7oyOjoBwGYeTj5oxfnaVYXzbanUG9aajpKI=", "", false},
{"", "", false},
{"hMTq0K2x7oyOjoBwGYeTj5oxfnaVYXzbanUG9aajpKIthissignatureisnowwaytoolongohmanicantbelievehowlongthisis=", "my secret message", false},
}
req.Header.Add("X-Shopify-Hmac-Sha256", hmac)

isValid := app.VerifyWebhookRequest(req)
for _, c := range cases {

testClient := NewClient(App{}, "", "")
req, err := testClient.NewRequest("GET", "", c.message, nil)
if err != nil {
t.Fatalf("Webhook.verify err = %v, expected true", err)
}
if c.hmac != "" {
req.Header.Add("X-Shopify-Hmac-Sha256", c.hmac)
}

if !isValid {
t.Error("Webhook.verify could not verified message checksum")
isValid := app.VerifyWebhookRequest(req)

if isValid != c.expected {
t.Errorf("Webhook.verify was expecting %t got %t", c.expected, isValid)
}
}

}

func TestVerifyWebhookRequestVerbose(t *testing.T) {
setup()
defer teardown()

var (
shortHMAC = "YmxhaGJsYWgK"
invalidBase64 = "XXXXXaGVsbG8="
validHMACSignature = "hMTq0K2x7oyOjoBwGYeTj5oxfnaVYXzbanUG9aajpKI="
validHMACSignatureEmptyBody = "ZAZ6P4c14f6v798OCPYCodtdf9g8Z+GfthdfCgyhUYg="
longHMAC = "VGhpc2lzdGhlc29uZ3RoYXRuZXZlcmVuZHN5ZXNpdGdvZXNvbmFuZG9ubXlmcmllbmRzc29tZXBlb3BsZXN0YXJ0aW5nc2luZ2luZ2l0bm90a25vd2luZ3doYXRpdHdhc2FuZG5vd2NvbnRpbnVlc2luZ2luZ2l0Zm9yZXZlcmp1c3RiZWNhdXNlCg=="
req *http.Request
err error
)
shortHMACBytes, _ := base64.StdEncoding.DecodeString(shortHMAC)
validHMACBytes, _ := base64.StdEncoding.DecodeString(validHMACSignature)
longHMACBytes, _ := base64.StdEncoding.DecodeString(longHMAC)

cases := []struct {
hmac string
message string
expected bool
expectedError error
}{
{validHMACSignature, "my secret message", true, nil},
{invalidBase64, "my secret message", false, errors.New("illegal base64 data at input byte 12")},
{shortHMAC, "my secret message", false, fmt.Errorf("received HMAC is not of length 32, it is of length %d", len(shortHMACBytes))},
{longHMAC, "my secret message", false, fmt.Errorf("received HMAC is not of length 32, it is of length %d", len(longHMACBytes))},
{shortHMAC, "", false, fmt.Errorf("received HMAC is not of length 32, it is of length %d", len(shortHMACBytes))},
{validHMACSignature, "my invalid message", false, fmt.Errorf("expected hash %s does not equal %x", "ac3560a67dd9ed3f46cf1807856b78f27c9543b5ae98f5292d8eccf6252254f0", validHMACBytes)},
{"", "", false, fmt.Errorf("header %s not set", shopifyChecksumHeader)},
{validHMACSignatureEmptyBody, "", false, errors.New("request body is empty")},
}

for _, c := range cases {

testClient := NewClient(App{}, "", "")

// We actually want to test nil body's, not ""
if c.message == "" {
req, err = testClient.NewRequest("GET", "", nil, nil)
} else {
req, err = testClient.NewRequest("GET", "", c.message, nil)
}

if err != nil {
t.Fatalf("Webhook.verify err = %v, expected true", err)
}

// We actually want to test not sending the header, not empty headers
if c.hmac != "" {
req.Header.Add("X-Shopify-Hmac-Sha256", c.hmac)
}

isValid, err := app.VerifyWebhookRequestVerbose(req)
if err == nil && c.expectedError != nil {
t.Errorf("Expected error %s got nil", c.expectedError.Error())
}
if c.expectedError == nil && err != nil {
t.Errorf("Expected nil got error %s", err.Error())
}

if isValid != c.expected {
t.Errorf("Webhook.verify was expecting %t got %t", c.expected, isValid)
if err != nil {
t.Errorf("Error returned %s header passed is %s", err.Error(), c.hmac)
} else {
t.Errorf("Header passed is %s", c.hmac)
}
}

if c.expectedError != nil && err.Error() != c.expectedError.Error() {
t.Errorf("Expected error %s got error %s", c.expectedError.Error(), err.Error())
}
}

// Other error cases
oldSecret := app.ApiSecret
app.ApiSecret = ""
isValid, err := app.VerifyWebhookRequestVerbose(req)
if err == nil || isValid == true || err.Error() != errors.New("ApiSecret is empty").Error() {
t.Errorf("Expected error %s got nil or true", errors.New("ApiSecret is empty"))
}

app.ApiSecret = oldSecret

req.Body = errReader{}
isValid, err = app.VerifyWebhookRequestVerbose(req)
if err == nil || isValid == true || err.Error() != errors.New("test-error").Error() {
t.Errorf("Expected error %s got %s", errors.New("test-error"), err)
}

}
2 changes: 1 addition & 1 deletion product_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func TestProductListFilterByIds(t *testing.T) {
httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/products.json?ids=1%2C2%2C3",
httpmock.NewStringResponder(200, `{"products": [{"id":1},{"id":2},{"id":3}]}`))

listOptions := ListOptions{IDs: []int{1,2,3}}
listOptions := ListOptions{IDs: []int{1, 2, 3}}

products, err := client.Product.List(listOptions)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion variant.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ type Variant struct {
CompareAtPrice *decimal.Decimal `json:"compare_at_price,omitempty"`
FulfillmentService string `json:"fulfillment_service,omitempty"`
InventoryManagement string `json:"inventory_management,omitempty"`
InventoryItemId int `json:"inventory_item_id,omitempty"`
InventoryItemId int `json:"inventory_item_id,omitempty"`
Option1 string `json:"option1,omitempty"`
Option2 string `json:"option2,omitempty"`
Option3 string `json:"option3,omitempty"`
Expand Down
2 changes: 1 addition & 1 deletion variant_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func variantTests(t *testing.T, variant Variant) {
if variant.Title != expectedTitle {
t.Errorf("Variant.Title returned %+v, expected %+v", variant.Title, expectedTitle)
}

expectedInventoryItemId := 1
if variant.InventoryItemId != expectedInventoryItemId {
t.Errorf("Variant.InventoryItemId returned %+v, expected %+v", variant.InventoryItemId, expectedInventoryItemId)
Expand Down

0 comments on commit 12eded9

Please sign in to comment.