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  }