Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for x-www-form-urlencoded requests to rv.ValidateBody #165

Merged
merged 5 commits into from
Jul 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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))
})
}