Oauth.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://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. (http://cakefoundation.org)
  10. * @link http://cakephp.org CakePHP(tm) Project
  11. * @since 3.0.0
  12. * @license http://www.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->url(), 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. uksort($args, 'strcmp');
  268. $pairs = [];
  269. foreach ($args as $k => $val) {
  270. if (is_array($val)) {
  271. sort($val, SORT_STRING);
  272. foreach ($val as $nestedVal) {
  273. $pairs[] = "$k=$nestedVal";
  274. }
  275. } else {
  276. $pairs[] = "$k=$val";
  277. }
  278. }
  279. return implode('&', $pairs);
  280. }
  281. /**
  282. * Builds the Oauth Authorization header value.
  283. *
  284. * @param array $data The oauth_* values to build
  285. * @return string
  286. */
  287. protected function _buildAuth($data)
  288. {
  289. $out = 'OAuth ';
  290. $params = [];
  291. foreach ($data as $key => $value) {
  292. $params[] = $key . '="' . $this->_encode($value) . '"';
  293. }
  294. $out .= implode(',', $params);
  295. return $out;
  296. }
  297. /**
  298. * URL Encodes a value based on rules of rfc3986
  299. *
  300. * @param string $value Value to encode.
  301. * @return string
  302. */
  303. protected function _encode($value)
  304. {
  305. return str_replace(
  306. '+',
  307. ' ',
  308. str_replace('%7E', '~', rawurlencode($value))
  309. );
  310. }
  311. }
  312. // @deprecated Add backwards compat alias.
  313. class_alias('Cake\Http\Client\Auth\Oauth', 'Cake\Network\Http\Auth\Oauth');