PageRenderTime 52ms CodeModel.GetById 6ms RepoModel.GetById 1ms app.codeStats 0ms

/src/JsConnectJSONP.php

https://github.com/vanilla/jsConnectPHP
PHP | 244 lines | 138 code | 22 blank | 84 comment | 25 complexity | 55899eb9dbeb8ac7e68e909145bdf65f MD5 | raw file
  1. <?php
  2. /**
  3. * @author Todd Burry <todd@vanillaforums.com>
  4. * @copyright 2009-2020 Vanilla Forums Inc.
  5. * @license MIT
  6. */
  7. namespace Vanilla\JsConnect;
  8. /**
  9. * This class contains backwards compatible methods for the v2.x jsConnect protocol.
  10. */
  11. final class JsConnectJSONP {
  12. const VERSION = '2';
  13. const TIMEOUT = 24 * 60;
  14. const FIELD_MAP = [
  15. 'uniqueid' => JsConnect::FIELD_UNIQUE_ID,
  16. 'photourl' => JsConnect::FIELD_PHOTO,
  17. ];
  18. /**
  19. * Write the jsConnect string for single sign on.
  20. *
  21. * @param array $user An array containing information about the currently signed on user. If no user is signed in, this should be empty.
  22. * @param array $request An array of the $_GET request.
  23. * @param string $clientID The string client ID that you set up in the jsConnect settings page.
  24. * @param string $secret The string secret that you set up in the jsConnect settings page.
  25. * @param string|bool $secure Whether or not to check for security. This is one of these values.
  26. * - true: Check for security and sign the response with an md5 hash.
  27. * - false: Don't check for security, but sign the response with an md5 hash.
  28. * - string: Check for security and sign the response with the given hash algorithm. See hash_algos() for what your server can support.
  29. * - null: Don't check for security and don't sign the response.
  30. * @since 1.1b Added the ability to provide a hash algorithm to $secure.
  31. */
  32. public static function writeJsConnect($user, $request, $clientID, $secret, $secure = true): void {
  33. if (isset($request['jwt'])) {
  34. self::writeJWT($user, $request, $clientID, $secret);
  35. } else {
  36. self::writeJSONP($user, $request, $clientID, $secret, $secure);
  37. }
  38. }
  39. /**
  40. * This is a backwards compatible method to help migrate jsConnect to the KWT protocol.
  41. *
  42. * @param array $user
  43. * @param array $query
  44. * @param string $clientID
  45. * @param string $secret
  46. */
  47. protected static function writeJWT(array $user, array $query, string $clientID, string $secret): void {
  48. $jsc = new JsConnect();
  49. $jsc->setSigningCredentials($clientID, $secret);
  50. foreach ($user as $key => $value) {
  51. $lkey = strtolower($key);
  52. if (isset(self::FIELD_MAP[$lkey])) {
  53. $key = self::FIELD_MAP[$lkey];
  54. }
  55. $jsc->setUserField($key, $value);
  56. }
  57. $jsc->handleRequest($query);
  58. }
  59. /**
  60. * Write the JSONP (v2) protocol response.
  61. *
  62. * @param array $user
  63. * @param array $request
  64. * @param string $clientID
  65. * @param string $secret
  66. * @param bool|string|null $secure
  67. */
  68. protected static function writeJSONP(array $user, array $request, string $clientID, string $secret, $secure): void {
  69. $user = array_change_key_case($user);
  70. // Error checking.
  71. if ($secure) {
  72. // Check the client.
  73. if (!isset($request['v'])) {
  74. $error = array('error' => 'invalid_request', 'message' => 'Missing the v parameter.');
  75. } elseif ($request['v'] !== self::VERSION) {
  76. $error = array('error' => 'invalid_request', 'message' => "Unsupported version {$request['v']}.");
  77. } elseif (!isset($request['client_id'])) {
  78. $error = array('error' => 'invalid_request', 'message' => 'Missing the client_id parameter.');
  79. } elseif ($request['client_id'] != $clientID) {
  80. $error = array('error' => 'invalid_client', 'message' => "Unknown client {$request['client_id']}.");
  81. } elseif (!isset($request['timestamp']) && !isset($request['sig'])) {
  82. if (count($user) > 0) {
  83. // This isn't really an error, but we are just going to return public information when no signature is sent.
  84. $error = array('name' => (string)@$user['name'], 'photourl' => @$user['photourl'], 'signedin' => true);
  85. } else {
  86. $error = array('name' => '', 'photourl' => '');
  87. }
  88. } elseif (!isset($request['timestamp']) || !ctype_digit($request['timestamp'])) {
  89. $error = array('error' => 'invalid_request', 'message' => 'The timestamp parameter is missing or invalid.');
  90. } elseif (!isset($request['sig'])) {
  91. $error = array('error' => 'invalid_request', 'message' => 'Missing the sig parameter.');
  92. } elseif (abs($request['timestamp'] - self::timestamp()) > self::TIMEOUT) {
  93. // Make sure the timestamp hasn't timeout
  94. $error = array('error' => 'invalid_request', 'message' => 'The timestamp is invalid.');
  95. } elseif (!isset($request['nonce'])) {
  96. $error = array('error' => 'invalid_request', 'message' => 'Missing the nonce parameter.');
  97. } elseif (!isset($request['ip'])) {
  98. $error = array('error' => 'invalid_request', 'message' => 'Missing the ip parameter.');
  99. } else {
  100. $signature = self::hash($request['ip'] . $request['nonce'] . $request['timestamp'] . $secret, $secure);
  101. if ($signature != $request['sig']) {
  102. $error = array('error' => 'access_denied', 'message' => 'Signature invalid.');
  103. }
  104. }
  105. }
  106. if (isset($error)) {
  107. $result = $error;
  108. } elseif (count($user) > 0) {
  109. if ($secure === null) {
  110. $result = $user;
  111. } else {
  112. $user['ip'] = $request['ip'];
  113. $user['nonce'] = $request['nonce'];
  114. $result = self::signJsConnect($user, $clientID, $secret, $secure, true);
  115. /**
  116. * @psalm-suppress PossiblyInvalidArrayOffset
  117. */
  118. $result['v'] = self::VERSION;
  119. }
  120. } else {
  121. $result = ['name' => '', 'photourl' => ''];
  122. }
  123. $content = json_encode($result);
  124. if (isset($request['callback'])) {
  125. $content = "{$request['callback']}($content)";
  126. }
  127. if (!headers_sent()) {
  128. $contentType = self::contentType($request);
  129. header($contentType, true);
  130. }
  131. echo $content;
  132. }
  133. /**
  134. * Get the current timestamp.
  135. *
  136. * @return int
  137. */
  138. public static function timestamp() {
  139. return time();
  140. }
  141. /**
  142. * Return the hash of a string.
  143. *
  144. * @param string $string The string to hash.
  145. * @param string|bool $secure The hash algorithm to use. true means md5.
  146. * @return string
  147. */
  148. public static function hash($string, $secure = true) {
  149. if ($secure === true) {
  150. $secure = 'md5';
  151. }
  152. switch ($secure) {
  153. case 'sha1':
  154. return sha1($string);
  155. break;
  156. case 'md5':
  157. case false:
  158. return md5($string);
  159. default:
  160. return hash($secure, $string);
  161. }
  162. }
  163. /**
  164. * Sign a jsConnect array.
  165. *
  166. * @param array $data
  167. * @param string $clientID
  168. * @param string $secret
  169. * @param string|bool $hashType
  170. * @param bool $returnData
  171. *
  172. * @return array|string
  173. */
  174. public static function signJsConnect(array $data, string $clientID, string $secret, $hashType, bool $returnData = false) {
  175. $normalizedData = array_change_key_case($data);
  176. ksort($normalizedData);
  177. foreach ($normalizedData as $key => $value) {
  178. if ($value === null) {
  179. $normalizedData[$key] = '';
  180. }
  181. }
  182. // RFC1738 state that spaces are encoded as '+'.
  183. $stringifiedData = http_build_query($normalizedData, '', '&', PHP_QUERY_RFC1738);
  184. $signature = self::hash($stringifiedData . $secret, $hashType);
  185. if ($returnData) {
  186. $normalizedData['client_id'] = $clientID;
  187. $normalizedData['sig'] = $signature;
  188. return $normalizedData;
  189. } else {
  190. return $signature;
  191. }
  192. }
  193. /**
  194. * Based on a jsConnect request, determine the proper response content type.
  195. *
  196. * @param array $request
  197. * @return string
  198. */
  199. public static function contentType(array $request): string {
  200. $isJsonp = isset($request["callback"]);
  201. $contentType = $isJsonp ? "Content-Type: application/javascript; charset=utf-8" : "Content-Type: application/json; charset=utf-8";
  202. return $contentType;
  203. }
  204. /**
  205. * Generate an SSO string suitable for passing in the url for embedded SSO.
  206. *
  207. * @param array $user The user to sso.
  208. * @param string $clientID Your client ID.
  209. * @param string $secret Your secret.
  210. * @return string
  211. */
  212. public static function ssoString($user, $clientID, $secret) {
  213. if (!isset($user['client_id'])) {
  214. $user['client_id'] = $clientID;
  215. }
  216. $string = base64_encode(json_encode($user));
  217. $timestamp = time();
  218. $hash = hash_hmac('sha1', "$string $timestamp", $secret);
  219. $result = "$string $hash $timestamp hmacsha1";
  220. return $result;
  221. }
  222. }