PageRenderTime 53ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Twilio/Security/RequestValidator.php

http://github.com/twilio/twilio-php
PHP | 167 lines | 71 code | 23 blank | 73 comment | 7 complexity | 1aece7b307af6890fa1e2e3eefe18355 MD5 | raw file
Possible License(s): MIT
  1. <?php
  2. namespace Twilio\Security;
  3. use Twilio\Values;
  4. /**
  5. * RequestValidator is a helper to validate that a request to a web server was actually made from Twilio
  6. * EXAMPLE USAGE:
  7. * $validator = new RequestValidator('your auth token here');
  8. * $isFromTwilio = $validator->validate($_SERVER['HTTP_X_TWILIO_SIGNATURE'], 'https://your-example-url.com/api/route/', $_REQUEST);
  9. * $isFromTwilio // <- if this is true, the request did come from Twilio, if not, it didn't
  10. */
  11. class RequestValidator {
  12. /**
  13. * @access private
  14. * @var string The auth token to the Twilio Account
  15. */
  16. private $authToken;
  17. /**
  18. * constructor
  19. * @access public
  20. * @param string $authToken the auth token of the Twilio user's account
  21. * Sets the account auth token to be used by the rest of the class
  22. */
  23. public function __construct(string $authToken) {
  24. $this->authToken = $authToken;
  25. }
  26. /**
  27. * Creates the actual base64 encoded signature of the sha1 hash of the concatenated URL and your auth token
  28. *
  29. * @param string $url the full URL of the request URL you specify for your phone number or app, from the protocol (https...) through the end of the query string (everything after the ?)
  30. * @param array $data the Twilio parameters the request was made with
  31. * @return string
  32. */
  33. public function computeSignature(string $url, array $data = []): string {
  34. // sort the array by keys
  35. \ksort($data);
  36. // append them to the data string in order
  37. // with no delimiters
  38. foreach ($data as $key => $value) {
  39. $url .= $key . $value;
  40. }
  41. // sha1 then base64 the url to the auth token and return the base64-ed string
  42. return \base64_encode(\hash_hmac('sha1', $url, $this->authToken, true));
  43. }
  44. /**
  45. * Converts the raw binary output to a hexadecimal return
  46. *
  47. * @param string $data
  48. * @return string
  49. */
  50. public static function computeBodyHash(string $data = ''): string {
  51. return \bin2hex(\hash('sha256', $data, true));
  52. }
  53. /**
  54. * The only method the client should be running...takes the Twilio signature, their URL, and the Twilio params and validates the signature
  55. *
  56. * @param string $expectedSignature
  57. * @param string $url
  58. * @param array|string $data
  59. * @return bool
  60. */
  61. public function validate(string $expectedSignature, string $url, $data = []): bool {
  62. $parsedUrl = \parse_url($url);
  63. $urlWithPort = self::addPort($parsedUrl);
  64. $urlWithoutPort = self::removePort($parsedUrl);
  65. $validBodyHash = true; // May not receive body hash, so default succeed
  66. if (!\is_array($data)) {
  67. // handling if the data was passed through as a string instead of an array of params
  68. $queryString = \explode('?', $url);
  69. $queryString = $queryString[1];
  70. \parse_str($queryString, $params);
  71. $validBodyHash = self::compare(self::computeBodyHash($data), Values::array_get($params, 'bodySHA256'));
  72. $data = [];
  73. }
  74. /*
  75. * Check signature of the URL with and without port information
  76. * since sig generation on the back end is inconsistent.
  77. */
  78. $validSignatureWithPort = self::compare(
  79. $expectedSignature,
  80. $this->computeSignature($urlWithPort, $data)
  81. );
  82. $validSignatureWithoutPort = self::compare(
  83. $expectedSignature,
  84. $this->computeSignature($urlWithoutPort, $data)
  85. );
  86. return $validBodyHash && ($validSignatureWithPort || $validSignatureWithoutPort);
  87. }
  88. /**
  89. * Time insensitive compare, function's runtime is governed by the length
  90. * of the first argument, not the difference between the arguments.
  91. *
  92. * @param string $a First part of the comparison pair
  93. * @param string $b Second part of the comparison pair
  94. * @return bool True if $a === $b, false otherwise.
  95. */
  96. public static function compare(?string $a, ?string $b): bool {
  97. if ($a && $b) {
  98. return hash_equals($a, $b);
  99. }
  100. return false;
  101. }
  102. /**
  103. * Removes the port from the URL
  104. *
  105. * @param array $parsedUrl
  106. * @return string Full URL without the port number
  107. */
  108. private static function removePort(array $parsedUrl): string {
  109. unset($parsedUrl['port']);
  110. return self::unparse_url($parsedUrl);
  111. }
  112. /**
  113. * Adds the port to the URL
  114. *
  115. * @param array $parsedUrl
  116. * @return string Full URL with the port number
  117. */
  118. private static function addPort(array $parsedUrl): string {
  119. if (!isset($parsedUrl['port'])) {
  120. $port = ($parsedUrl['scheme'] === 'https') ? 443 : 80;
  121. $parsedUrl['port'] = $port;
  122. }
  123. return self::unparse_url($parsedUrl);
  124. }
  125. /**
  126. * Builds the URL from its parsed component pieces
  127. *
  128. * @param array $parsedUrl
  129. * @return string Full URL
  130. */
  131. static function unparse_url(array $parsedUrl): string {
  132. $parts = [];
  133. $parts['scheme'] = isset($parsedUrl['scheme']) ? $parsedUrl['scheme'] . '://' : '';
  134. $parts['user'] = $parsedUrl['user'] ?? '';
  135. $parts['pass'] = isset($parsedUrl['pass']) ? ':' . $parsedUrl['pass'] : '';
  136. $parts['pass'] = ($parts['user'] || $parts['pass']) ? $parts['pass'] . '@' : '';
  137. $parts['host'] = $parsedUrl['host'] ?? '';
  138. $parts['port'] = isset($parsedUrl['port']) ? ':' . $parsedUrl['port'] : '';
  139. $parts['path'] = $parsedUrl['path'] ?? '';
  140. $parts['query'] = isset($parsedUrl['query']) ? '?' . $parsedUrl['query'] : '';
  141. $parts['fragment'] = isset($parsedUrl['fragment']) ? '#' . $parsedUrl['fragment'] : '';
  142. return \implode('', $parts);
  143. }
  144. }