Skip to content

Commit

Permalink
feat: Add support for x-www-form-urlencoded requests to rv.ValidateBo…
Browse files Browse the repository at this point in the history
…dy (#165)

* Add support for x-www-form-urlencoded requests
  • Loading branch information
heyztb committed Jul 8, 2022
1 parent fca15bf commit c21e6c4
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 14 deletions.
37 changes: 28 additions & 9 deletions client/request_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,26 +40,45 @@ func (rv *RequestValidator) Validate(url string, params map[string]string, expec
// check signature of testURL with and without port, since sig generation on back-end is inconsistent
signatureWithPort := rv.getValidationSignature(addPort(url), paramSlc)
signatureWithoutPort := rv.getValidationSignature(removePort(url), paramSlc)

return compare(signatureWithPort, expectedSignature) ||
compare(signatureWithoutPort, expectedSignature)
}

// ValidateBody can be used for Twilio Signatures sent with webhooks configured for POST calls. It returns true
// if the computed signature matches the expectedSignature. Body is the HTTP request body from the webhook call
// as a slice of bytes.
// ValidateBody can be used to verify request signatures included with Twilio webhook requests configured to be sent as POST requests.
// url is the full URL you are receiving webhook requests at
// body is a byte slice of the body of the incoming webhook request
// expectedSignature is the value of the X-Twilio-Signature header
func (rv *RequestValidator) ValidateBody(url string, body []byte, expectedSignature string) bool {
parsed, err := urllib.Parse(url)
if err != nil {
return false
}

bodySHA256 := parsed.Query().Get("bodySHA256")
if len(bodySHA256) == 0 {
return false
// we can expect this query paramter on requests made with json bodies
if parsed.Query().Has("bodySHA256") {
bodySHA256 := parsed.Query().Get("bodySHA256")
if len(bodySHA256) == 0 {
return false
}
return rv.Validate(url, map[string]string{}, expectedSignature) &&
rv.validateBody(body, bodySHA256)
} else {
// however if that parameter is not present, we assume the request body is x-www-form-urlencoded, e.g "property=value&boolean=true" (quotes added for clarity)
parsedBody, err := urllib.ParseQuery(string(body))
if err != nil {
return false
}

// url.Values is a map[string][]string, therefore we need to create a new map to store the values we will pass to rv.Validate below
params := make(map[string]string)
for k, v := range parsedBody {
// we only care about the first value held by each key. all other values under a key will be ignored.
params[k] = v[0]
}

return rv.Validate(url, params, expectedSignature)
}

return rv.Validate(url, map[string]string{}, expectedSignature) &&
rv.validateBody(body, bodySHA256)
}

func compare(x, y string) bool {
Expand Down
16 changes: 11 additions & 5 deletions client/request_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ var (
"Caller": "+14158675309",
"From": "+14158675309",
}
body = []byte(`{"property": "value", "boolean": true}`)
jsonBody = []byte(`{"property": "value", "boolean": true}`)
formBody = []byte(`property=value&boolean=true`)
)

func TestRequestValidator_Validate(t *testing.T) {
Expand Down Expand Up @@ -64,19 +65,24 @@ func TestRequestValidator_Validate(t *testing.T) {
func TestRequestValidator_ValidateBody(t *testing.T) {
t.Parallel()

t.Run("returns true when validation succeeds", func(t *testing.T) {
t.Run("returns true when validation succeeds with json body", func(t *testing.T) {
theURL := testURL + "&bodySHA256=" + bodyHash
signatureWithBodyHash := "a9nBmqA0ju/hNViExpshrM61xv4="
assert.True(t, validator.ValidateBody(theURL, body, signatureWithBodyHash))
assert.True(t, validator.ValidateBody(theURL, jsonBody, signatureWithBodyHash))
})

t.Run("returns true when validation succeeds with form body", func(t *testing.T) {
expectedSignature := "NBdBDr/T/lgjI+tlgpXjKZQZs/k="
assert.True(t, validator.ValidateBody(testURL, formBody, expectedSignature))
})

t.Run("returns false when validation fails", func(t *testing.T) {
assert.False(t, validator.ValidateBody(testURL, body, signature))
assert.False(t, validator.ValidateBody(testURL, jsonBody, signature))
})

t.Run("returns true when there's no other parameters and the signature is right", func(t *testing.T) {
theURL := "https://mycompany.com/myapp.php?bodySHA256=" + bodyHash
signatureForURL := "y77kIzt2vzLz71DgmJGsen2scGs="
assert.True(t, validator.ValidateBody(theURL, body, signatureForURL))
assert.True(t, validator.ValidateBody(theURL, jsonBody, signatureForURL))
})
}

0 comments on commit c21e6c4

Please sign in to comment.