github.com/twilio/twilio-go@v1.20.1/client/request_validator.go (about) 1 package client 2 3 import ( 4 "crypto/hmac" 5 "crypto/sha1" 6 "crypto/sha256" 7 "crypto/subtle" 8 "encoding/base64" 9 "fmt" 10 urllib "net/url" 11 "sort" 12 "strings" 13 ) 14 15 // RequestValidator is used to verify the Twilio Signature included with Twilio requests to webhooks. 16 // This ensures the request is actually coming from Twilio and helps with securing your webhooks. 17 type RequestValidator struct { 18 signingKey []byte 19 } 20 21 // NewRequestValidator returns a new RequestValidator which uses the specified auth token when verifying 22 // Twilio signatures. 23 func NewRequestValidator(authToken string) RequestValidator { 24 return RequestValidator{ 25 signingKey: []byte(authToken), 26 } 27 } 28 29 // Validate can be used for Twilio Signatures sent with webhooks configured for GET calls. It returns true 30 // if the computed signature matches the expectedSignature. Params are a map of string to string containing 31 // all the query params Twilio added to the configured webhook URL. 32 func (rv *RequestValidator) Validate(url string, params map[string]string, expectedSignature string) bool { 33 //sort the keys of query params then concatenated key+value strings 34 var paramKeys []string 35 for k := range params { 36 paramKeys = append(paramKeys, k) 37 } 38 sort.Strings(paramKeys) 39 var paramSlc []string 40 for _, k := range paramKeys { 41 paramSlc = append(paramSlc, fmt.Sprintf("%s%s", k, params[k])) 42 } 43 44 // check signature of testURL with and without port, since sig generation on back-end is inconsistent 45 signatureWithPort := rv.getValidationSignature(addPort(url), paramSlc) 46 signatureWithoutPort := rv.getValidationSignature(removePort(url), paramSlc) 47 return compare(signatureWithPort, expectedSignature) || 48 compare(signatureWithoutPort, expectedSignature) 49 } 50 51 // ValidateBody can be used for Twilio Signatures sent with webhooks configured for POST calls. It returns true 52 // if the computed signature matches the expectedSignature. Body is the HTTP request body from the webhook call 53 // as a slice of bytes. 54 func (rv *RequestValidator) ValidateBody(url string, body []byte, expectedSignature string) bool { 55 parsed, err := urllib.Parse(url) 56 if err != nil { 57 return false 58 } 59 60 bodySHA256 := parsed.Query().Get("bodySHA256") 61 62 // For x-www-form-urlencoded Request body 63 if len(bodySHA256) == 0 { 64 parsedBody, err := urllib.ParseQuery(string(body)) 65 if err != nil { 66 return false 67 } 68 params := make(map[string]string) 69 for k, v := range parsedBody { 70 //validate with first value of each key 71 params[k] = v[0] 72 } 73 return rv.Validate(url, params, expectedSignature) 74 } 75 76 return rv.Validate(url, map[string]string{}, expectedSignature) && 77 rv.validateBody(body, bodySHA256) 78 } 79 80 func compare(x, y string) bool { 81 return subtle.ConstantTimeCompare([]byte(x), []byte(y)) == 1 82 } 83 84 func (rv *RequestValidator) validateBody(body []byte, expectedSHA string) bool { 85 hasher := sha256.New() 86 _, err := hasher.Write(body) 87 if err != nil { 88 return false 89 } 90 sum := hasher.Sum(nil) 91 return compare(fmt.Sprintf("%x", sum), expectedSHA) 92 } 93 94 func (rv *RequestValidator) getValidationSignature(url string, sortedConcatenatedParams []string) string { 95 for _, param := range sortedConcatenatedParams { 96 url += param 97 } 98 99 h := hmac.New(sha1.New, rv.signingKey) 100 _, err := h.Write([]byte(url)) 101 if err != nil { 102 return "" 103 } 104 sum := h.Sum(nil) 105 return base64.StdEncoding.EncodeToString(sum) 106 } 107 108 func addPort(url string) string { 109 parsed, err := urllib.Parse(url) 110 if err != nil { 111 return url 112 } 113 114 port := parsed.Port() 115 if len(port) != 0 { 116 return url // url already has port 117 } 118 119 if parsed.Scheme == "https" { 120 return updatePort(url, 443) 121 } 122 return updatePort(url, 80) 123 } 124 125 func updatePort(url string, newPort int) string { 126 parsed, err := urllib.Parse(url) 127 if err != nil { 128 return url 129 } 130 131 var newHost string 132 if len(parsed.Port()) == 0 { 133 // url didn't already have port, add it 134 newHost = fmt.Sprintf("%s:%d", parsed.Host, newPort) 135 } else { 136 // url already had port, grab just the host and add new port 137 oldHost := strings.Split(parsed.Host, ":")[0] 138 newHost = fmt.Sprintf("%s:%d", oldHost, newPort) 139 } 140 141 parsed.Host = newHost 142 return parsed.String() 143 } 144 145 func removePort(url string) string { 146 parsed, err := urllib.Parse(url) 147 if err != nil { 148 return url 149 } 150 151 if len(parsed.Port()) == 0 { 152 return url 153 } 154 155 newHost := strings.Split(parsed.Host, ":")[0] 156 parsed.Host = newHost 157 return parsed.String() 158 }