PageRenderTime 74ms CodeModel.GetById 42ms RepoModel.GetById 1ms app.codeStats 0ms

/src/Http/Client/Auth/Oauth.php

https://github.com/rchavik/cakephp
PHP | 372 lines | 220 code | 34 blank | 118 comment | 23 complexity | 308d5cdcff001b507521309d14c16c6a MD5 | raw file
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * Redistributions of files must retain the above copyright notice.
  8. *
  9. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  10. * @link https://cakephp.org CakePHP(tm) Project
  11. * @since 3.0.0
  12. * @license https://opensource.org/licenses/mit-license.php MIT License
  13. */
  14. namespace Cake\Http\Client\Auth;
  15. use Cake\Core\Exception\Exception;
  16. use Cake\Http\Client\Request;
  17. use Cake\Utility\Security;
  18. use RuntimeException;
  19. /**
  20. * Oauth 1 authentication strategy for Cake\Http\Client
  21. *
  22. * This object does not handle getting Oauth access tokens from the service
  23. * provider. It only handles make client requests *after* you have obtained the Oauth
  24. * tokens.
  25. *
  26. * Generally not directly constructed, but instead used by Cake\Http\Client
  27. * when $options['auth']['type'] is 'oauth'
  28. */
  29. class Oauth
  30. {
  31. /**
  32. * Add headers for Oauth authorization.
  33. *
  34. * @param \Cake\Http\Client\Request $request The request object.
  35. * @param array $credentials Authentication credentials.
  36. * @return \Cake\Http\Client\Request The updated request.
  37. * @throws \Cake\Core\Exception\Exception On invalid signature types.
  38. */
  39. public function authentication(Request $request, array $credentials)
  40. {
  41. if (!isset($credentials['consumerKey'])) {
  42. return $request;
  43. }
  44. if (empty($credentials['method'])) {
  45. $credentials['method'] = 'hmac-sha1';
  46. }
  47. $credentials['method'] = strtoupper($credentials['method']);
  48. switch ($credentials['method']) {
  49. case 'HMAC-SHA1':
  50. $hasKeys = isset(
  51. $credentials['consumerSecret'],
  52. $credentials['token'],
  53. $credentials['tokenSecret']
  54. );
  55. if (!$hasKeys) {
  56. return $request;
  57. }
  58. $value = $this->_hmacSha1($request, $credentials);
  59. break;
  60. case 'RSA-SHA1':
  61. if (!isset($credentials['privateKey'])) {
  62. return $request;
  63. }
  64. $value = $this->_rsaSha1($request, $credentials);
  65. break;
  66. case 'PLAINTEXT':
  67. $hasKeys = isset(
  68. $credentials['consumerSecret'],
  69. $credentials['token'],
  70. $credentials['tokenSecret']
  71. );
  72. if (!$hasKeys) {
  73. return $request;
  74. }
  75. $value = $this->_plaintext($request, $credentials);
  76. break;
  77. default:
  78. throw new Exception(sprintf('Unknown Oauth signature method %s', $credentials['method']));
  79. }
  80. return $request->withHeader('Authorization', $value);
  81. }
  82. /**
  83. * Plaintext signing
  84. *
  85. * This method is **not** suitable for plain HTTP.
  86. * You should only ever use PLAINTEXT when dealing with SSL
  87. * services.
  88. *
  89. * @param \Cake\Http\Client\Request $request The request object.
  90. * @param array $credentials Authentication credentials.
  91. * @return string Authorization header.
  92. */
  93. protected function _plaintext($request, $credentials)
  94. {
  95. $values = [
  96. 'oauth_version' => '1.0',
  97. 'oauth_nonce' => uniqid(),
  98. 'oauth_timestamp' => time(),
  99. 'oauth_signature_method' => 'PLAINTEXT',
  100. 'oauth_token' => $credentials['token'],
  101. 'oauth_consumer_key' => $credentials['consumerKey'],
  102. ];
  103. if (isset($credentials['realm'])) {
  104. $values['oauth_realm'] = $credentials['realm'];
  105. }
  106. $key = [$credentials['consumerSecret'], $credentials['tokenSecret']];
  107. $key = implode('&', $key);
  108. $values['oauth_signature'] = $key;
  109. return $this->_buildAuth($values);
  110. }
  111. /**
  112. * Use HMAC-SHA1 signing.
  113. *
  114. * This method is suitable for plain HTTP or HTTPS.
  115. *
  116. * @param \Cake\Http\Client\Request $request The request object.
  117. * @param array $credentials Authentication credentials.
  118. * @return string
  119. */
  120. protected function _hmacSha1($request, $credentials)
  121. {
  122. $nonce = isset($credentials['nonce']) ? $credentials['nonce'] : uniqid();
  123. $timestamp = isset($credentials['timestamp']) ? $credentials['timestamp'] : time();
  124. $values = [
  125. 'oauth_version' => '1.0',
  126. 'oauth_nonce' => $nonce,
  127. 'oauth_timestamp' => $timestamp,
  128. 'oauth_signature_method' => 'HMAC-SHA1',
  129. 'oauth_token' => $credentials['token'],
  130. 'oauth_consumer_key' => $credentials['consumerKey'],
  131. ];
  132. $baseString = $this->baseString($request, $values);
  133. if (isset($credentials['realm'])) {
  134. $values['oauth_realm'] = $credentials['realm'];
  135. }
  136. $key = [$credentials['consumerSecret'], $credentials['tokenSecret']];
  137. $key = array_map([$this, '_encode'], $key);
  138. $key = implode('&', $key);
  139. $values['oauth_signature'] = base64_encode(
  140. hash_hmac('sha1', $baseString, $key, true)
  141. );
  142. return $this->_buildAuth($values);
  143. }
  144. /**
  145. * Use RSA-SHA1 signing.
  146. *
  147. * This method is suitable for plain HTTP or HTTPS.
  148. *
  149. * @param \Cake\Http\Client\Request $request The request object.
  150. * @param array $credentials Authentication credentials.
  151. * @return string
  152. *
  153. * @throws \RuntimeException
  154. */
  155. protected function _rsaSha1($request, $credentials)
  156. {
  157. if (!function_exists('openssl_pkey_get_private')) {
  158. throw new RuntimeException('RSA-SHA1 signature method requires the OpenSSL extension.');
  159. }
  160. $nonce = isset($credentials['nonce']) ? $credentials['nonce'] : bin2hex(Security::randomBytes(16));
  161. $timestamp = isset($credentials['timestamp']) ? $credentials['timestamp'] : time();
  162. $values = [
  163. 'oauth_version' => '1.0',
  164. 'oauth_nonce' => $nonce,
  165. 'oauth_timestamp' => $timestamp,
  166. 'oauth_signature_method' => 'RSA-SHA1',
  167. 'oauth_consumer_key' => $credentials['consumerKey'],
  168. ];
  169. if (isset($credentials['consumerSecret'])) {
  170. $values['oauth_consumer_secret'] = $credentials['consumerSecret'];
  171. }
  172. if (isset($credentials['token'])) {
  173. $values['oauth_token'] = $credentials['token'];
  174. }
  175. if (isset($credentials['tokenSecret'])) {
  176. $values['oauth_token_secret'] = $credentials['tokenSecret'];
  177. }
  178. $baseString = $this->baseString($request, $values);
  179. if (isset($credentials['realm'])) {
  180. $values['oauth_realm'] = $credentials['realm'];
  181. }
  182. if (is_resource($credentials['privateKey'])) {
  183. $resource = $credentials['privateKey'];
  184. $privateKey = stream_get_contents($resource);
  185. rewind($resource);
  186. $credentials['privateKey'] = $privateKey;
  187. }
  188. $credentials += [
  189. 'privateKeyPassphrase' => null,
  190. ];
  191. if (is_resource($credentials['privateKeyPassphrase'])) {
  192. $resource = $credentials['privateKeyPassphrase'];
  193. $passphrase = stream_get_line($resource, 0, PHP_EOL);
  194. rewind($resource);
  195. $credentials['privateKeyPassphrase'] = $passphrase;
  196. }
  197. $privateKey = openssl_pkey_get_private($credentials['privateKey'], $credentials['privateKeyPassphrase']);
  198. $signature = '';
  199. openssl_sign($baseString, $signature, $privateKey);
  200. openssl_free_key($privateKey);
  201. $values['oauth_signature'] = base64_encode($signature);
  202. return $this->_buildAuth($values);
  203. }
  204. /**
  205. * Generate the Oauth basestring
  206. *
  207. * - Querystring, request data and oauth_* parameters are combined.
  208. * - Values are sorted by name and then value.
  209. * - Request values are concatenated and urlencoded.
  210. * - The request URL (without querystring) is normalized.
  211. * - The HTTP method, URL and request parameters are concatenated and returned.
  212. *
  213. * @param \Cake\Http\Client\Request $request The request object.
  214. * @param array $oauthValues Oauth values.
  215. * @return string
  216. */
  217. public function baseString($request, $oauthValues)
  218. {
  219. $parts = [
  220. $request->getMethod(),
  221. $this->_normalizedUrl($request->getUri()),
  222. $this->_normalizedParams($request, $oauthValues),
  223. ];
  224. $parts = array_map([$this, '_encode'], $parts);
  225. return implode('&', $parts);
  226. }
  227. /**
  228. * Builds a normalized URL
  229. *
  230. * Section 9.1.2. of the Oauth spec
  231. *
  232. * @param \Psr\Http\Message\UriInterface $uri Uri object to build a normalized version of.
  233. * @return string Normalized URL
  234. */
  235. protected function _normalizedUrl($uri)
  236. {
  237. $out = $uri->getScheme() . '://';
  238. $out .= strtolower($uri->getHost());
  239. $out .= $uri->getPath();
  240. return $out;
  241. }
  242. /**
  243. * Sorts and normalizes request data and oauthValues
  244. *
  245. * Section 9.1.1 of Oauth spec.
  246. *
  247. * - URL encode keys + values.
  248. * - Sort keys & values by byte value.
  249. *
  250. * @param \Cake\Http\Client\Request $request The request object.
  251. * @param array $oauthValues Oauth values.
  252. * @return string sorted and normalized values
  253. */
  254. protected function _normalizedParams($request, $oauthValues)
  255. {
  256. $query = parse_url($request->getUri(), PHP_URL_QUERY);
  257. parse_str($query, $queryArgs);
  258. $post = [];
  259. $body = $request->body();
  260. if (is_string($body) && $request->getHeaderLine('content-type') === 'application/x-www-form-urlencoded') {
  261. parse_str($body, $post);
  262. }
  263. if (is_array($body)) {
  264. $post = $body;
  265. }
  266. $args = array_merge($queryArgs, $oauthValues, $post);
  267. $pairs = $this->_normalizeData($args);
  268. $data = [];
  269. foreach ($pairs as $pair) {
  270. $data[] = implode('=', $pair);
  271. }
  272. sort($data, SORT_STRING);
  273. return implode('&', $data);
  274. }
  275. /**
  276. * Recursively convert request data into the normalized form.
  277. *
  278. * @param array $args The arguments to normalize.
  279. * @param string $path The current path being converted.
  280. * @see https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
  281. * @return array
  282. */
  283. protected function _normalizeData($args, $path = '')
  284. {
  285. $data = [];
  286. foreach ($args as $key => $value) {
  287. if ($path) {
  288. // Fold string keys with [].
  289. // Numeric keys result in a=b&a=c. While this isn't
  290. // standard behavior in PHP, it is common in other platforms.
  291. if (!is_numeric($key)) {
  292. $key = "{$path}[{$key}]";
  293. } else {
  294. $key = $path;
  295. }
  296. }
  297. if (is_array($value)) {
  298. uksort($value, 'strcmp');
  299. $data = array_merge($data, $this->_normalizeData($value, $key));
  300. } else {
  301. $data[] = [$key, $value];
  302. }
  303. }
  304. return $data;
  305. }
  306. /**
  307. * Builds the Oauth Authorization header value.
  308. *
  309. * @param array $data The oauth_* values to build
  310. * @return string
  311. */
  312. protected function _buildAuth($data)
  313. {
  314. $out = 'OAuth ';
  315. $params = [];
  316. foreach ($data as $key => $value) {
  317. $params[] = $key . '="' . $this->_encode($value) . '"';
  318. }
  319. $out .= implode(',', $params);
  320. return $out;
  321. }
  322. /**
  323. * URL Encodes a value based on rules of rfc3986
  324. *
  325. * @param string $value Value to encode.
  326. * @return string
  327. */
  328. protected function _encode($value)
  329. {
  330. return str_replace(
  331. '+',
  332. ' ',
  333. str_replace('%7E', '~', rawurlencode($value))
  334. );
  335. }
  336. }
  337. // @deprecated Add backwards compat alias.
  338. class_alias('Cake\Http\Client\Auth\Oauth', 'Cake\Network\Http\Auth\Oauth');