PageRenderTime 28ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/Lib/Network/Http/HttpSocketOauth.php

https://bitbucket.org/cakephp/cakepackages
PHP | 295 lines | 117 code | 32 blank | 146 comment | 24 complexity | a08a92a769a1571f4897a4f638160b66 MD5 | raw file
  1. <?php
  2. /**
  3. * Extension to CakePHP core HttpSocket class that overrides the request method
  4. * and intercepts requests whose $request['auth']['method'] param is 'OAuth'.
  5. *
  6. * The correct OAuth Authorization header is determined from the request params
  7. * and then set in the $request['header']['Authorization'] param of the request
  8. * array before passing it back to HttpSocket::request() to send the request and
  9. * parse the response.
  10. *
  11. * So to trigger OAuth, add $request['auth']['method'] = 'OAuth' to your
  12. * request. In addition, you'll need to add your consumer key in the
  13. * $request['auth']['oauth_consumer_key'] and your consumer secret in the
  14. * $request['auth']['oauth_consumer_secret'] param. These are given to you by
  15. * the OAuth provider. And once you have them, $request['auth']['oauth_token']
  16. * and $request['auth']['oauth_token_secret'] params. Your OAuth provider may
  17. * require you to send additional params too. Include them in the
  18. * $request['auth'] array and they'll be passed on in the Authorization header
  19. * and considered when signing the request.
  20. *
  21. * @author Neil Crookes <neil@neilcrookes.com>
  22. * @link http://www.neilcrookes.com
  23. * @copyright (c) 2010 Neil Crookes
  24. * @license MIT License - http://www.opensource.org/licenses/mit-license.php
  25. */
  26. App::uses('HttpSocket', 'Network/Http');
  27. class HttpSocketOauth extends HttpSocket {
  28. /**
  29. * Default OAuth parameters. These get merged into the $request['auth'] param.
  30. *
  31. * @var array
  32. */
  33. public $defaults = array(
  34. 'oauth_version' => '1.0',
  35. 'oauth_signature_method' => 'HMAC-SHA1',
  36. );
  37. /**
  38. * Overrides HttpSocket::request() to handle cases where
  39. * $request['auth']['method'] is 'OAuth'.
  40. *
  41. * @param array $request As required by HttpSocket::request(). NOTE ONLY
  42. * THE ARRAY TYPE OF REQUEST IS SUPPORTED
  43. * @return array
  44. */
  45. public function request($request = array()) {
  46. // If the request does not need OAuth Authorization header, let the parent
  47. // deal with it.
  48. if (!isset($request['auth']['method']) || $request['auth']['method'] != 'OAuth') {
  49. return parent::request($request);
  50. }
  51. // Generate the OAuth Authorization Header content for this request from the
  52. // request data and add it into the request's Authorization Header. Note, we
  53. // don't just add the header directly in the request variable and return the
  54. // whole thing from the authorizationHeader() method because in some cases
  55. // we may not want the authorization header content in the request's
  56. // authorization header, for example, OAuth Echo as used by Twitpic and
  57. // Twitter includes an Authorization Header as required by twitter's verify
  58. // credentials API in the X-Verify-Credentials-Authorization header.
  59. $request['header']['Authorization'] = $this->authorizationHeader($request);
  60. // Now the Authorization header is built, fire the request off to the parent
  61. // HttpSocket class request method that we intercepted earlier.
  62. return parent::request($request);
  63. }
  64. /**
  65. * Returns the OAuth Authorization Header string for a given request array.
  66. *
  67. * This method is called by request but can also be called directly, which is
  68. * useful if you need to get the OAuth Authorization Header string, such as
  69. * when integrating with a service that uses OAuth Echo (Authorization
  70. * Delegation) e.g. Twitpic. In this case you send a normal unauthenticated
  71. * request to the service e.g. Twitpic along with 2 extra headers:
  72. * - X-Auth-Service-Provider - effectively, this is the realm that identity
  73. * delegation should be sent to - in the case of Twitter, just set this to
  74. * https://api.twitter.com/1/account/verify_credentials.json;
  75. * - X-Verify-Credentials-Authorization - Consumer should create all the OAuth
  76. * parameters necessary so it could call
  77. * https://api.twitter.com/1/account/verify_credentials.json using OAuth in
  78. * the HTTP header (e.g. it should look like OAuth oauth_consumer_key="...",
  79. * oauth_token="...", oauth_signature_method="...", oauth_signature="...",
  80. * oauth_timestamp="...", oauth_nonce="...", oauth_version="...".
  81. *
  82. * @param array $request As required by HttpSocket::request(). NOTE ONLY
  83. * THE ARRAY TYPE OF REQUEST IS SUPPORTED
  84. * @return String
  85. */
  86. public function authorizationHeader($request) {
  87. $request['auth'] = array_merge($this->defaults, $request['auth']);
  88. // Nonce, or number used once is used to distinguish between different
  89. // requests to the OAuth provider
  90. if (!isset($request['auth']['oauth_nonce'])) {
  91. $request['auth']['oauth_nonce'] = md5(uniqid(rand(), true));
  92. }
  93. if (!isset($request['auth']['oauth_timestamp'])) {
  94. $request['auth']['oauth_timestamp'] = time();
  95. }
  96. // Now starts the process of signing the request. The signature is a hash of
  97. // a signature base string with the secret keys. The signature base string
  98. // is made up of the request http verb, the request uri and the request
  99. // params, and the secret keys are the consumer secret (for your
  100. // application) and the access token secret generated for the user by the
  101. // provider, e.g. twitter, when the user authorizes your app to access their
  102. // details.
  103. // Building the request uri, note we don't include the query string or
  104. // fragment. Standard ports must not be included but non standard ones must.
  105. $uriFormat = '%scheme://%host';
  106. if (isset($request['uri']['port']) && !in_array($request['uri']['port'], array(80, 443))) {
  107. $uriFormat .= ':' . $request['uri']['port'];
  108. }
  109. $uriFormat .= '/%path';
  110. $requestUrl = $this->_buildUri($request['uri'], $uriFormat);
  111. // OAuth reference states that the request params, i.e. oauth_ params, body
  112. // params and query string params need to be normalised, i.e. combined in a
  113. // single string, separated by '&' in the format name=value. But they also
  114. // need to be sorted by key, then by value. You can't just merge the auth,
  115. // body and query arrays together then do a ksort because there may be
  116. // parameters with the same name. Instead we've got to get them into an
  117. // array of array('name' => '<name>', 'value' => '<value>') elements, then
  118. // sort those elements.
  119. // Let's start with the auth params - however, we shouldn't include the auth
  120. // method (OAuth), and OAuth reference says not to include the realm or the
  121. // consumer or token secrets
  122. $requestParams = $this->assocToNumericNameValue(array_diff_key(
  123. $request['auth'],
  124. array_flip(array('realm', 'method', 'oauth_consumer_secret', 'oauth_token_secret'))
  125. ));
  126. // Next add the body params if there are any and the content type header is
  127. // not set, or it's application/x-www-form-urlencoded
  128. if (isset($request['body']) && (!isset($request['header']['Content-Type']) || stristr($request['header']['Content-Type'], 'application/x-www-form-urlencoded'))) {
  129. $requestParams = array_merge($requestParams, $this->assocToNumericNameValue($request['body']));
  130. }
  131. // Finally the query params
  132. if (isset($request['uri']['query'])) {
  133. $requestParams = array_merge($requestParams, $this->assocToNumericNameValue($request['uri']['query']));
  134. }
  135. // Now we can sort them by name then value
  136. usort($requestParams, array($this, 'sortByNameThenByValue'));
  137. // Now we concatenate them together in name=value pairs separated by &
  138. $normalisedRequestParams = '';
  139. foreach ($requestParams as $k => $requestParam) {
  140. if ($k) {
  141. $normalisedRequestParams .= '&';
  142. }
  143. $normalisedRequestParams .= $requestParam['name'] . '=' . $this->parameterEncode($requestParam['value']);
  144. }
  145. // The signature base string consists of the request method (uppercased) and
  146. // concatenated with the request URL and normalised request parameters
  147. // string, both encoded, and separated by &
  148. $signatureBaseString = strtoupper($request['method']) . '&'
  149. . $this->parameterEncode($requestUrl) . '&'
  150. . $this->parameterEncode($normalisedRequestParams);
  151. // The signature base string is hashed with a key which is the consumer
  152. // secret (assigned to your application by the provider) and the token
  153. // secret (also known as the access token secret, if you've got it yet),
  154. // both encoded and separated by an &
  155. $key = '';
  156. if (isset($request['auth']['oauth_consumer_secret'])) {
  157. $key .= $this->parameterEncode($request['auth']['oauth_consumer_secret']);
  158. }
  159. $key .= '&';
  160. if (isset($request['auth']['oauth_token_secret'])) {
  161. $key .= $this->parameterEncode($request['auth']['oauth_token_secret']);
  162. }
  163. // Finally construct the signature according to the value of the
  164. // oauth_signature_method auth param in the request array.
  165. switch ($request['auth']['oauth_signature_method']) {
  166. case 'HMAC-SHA1':
  167. $request['auth']['oauth_signature'] = base64_encode(hash_hmac('sha1', $signatureBaseString, $key, true));
  168. break;
  169. default:
  170. // @todo implement the other 2 hashing methods
  171. break;
  172. }
  173. // Finally, we have all the Authorization header parameters so we can build
  174. // the header string.
  175. $authorizationHeader = 'OAuth';
  176. // We don't want to include the realm, method or secrets though
  177. $authorizationHeaderParams = array_diff_key(
  178. $request['auth'],
  179. array_flip(array('method', 'oauth_consumer_secret', 'oauth_token_secret', 'realm'))
  180. );
  181. // Add the Authorization header params to the Authorization header string,
  182. // properly encoded.
  183. $first = true;
  184. if (isset($request['auth']['realm'])) {
  185. $authorizationHeader .= ' realm="' . $request['auth']['realm'] . '"';
  186. $first = false;
  187. }
  188. foreach ($authorizationHeaderParams as $name => $value) {
  189. if (!$first) {
  190. $authorizationHeader .= ',';
  191. } else {
  192. $authorizationHeader .= ' ';
  193. $first = false;
  194. }
  195. $authorizationHeader .= $this->authorizationHeaderParamEncode($name, $value);
  196. }
  197. return $authorizationHeader;
  198. }
  199. /**
  200. * Builds an Authorization header param string from the supplied name and
  201. * value. See below for example:
  202. *
  203. * @param string $name E.g. 'oauth_signature_method'
  204. * @param string $value E.g. 'HMAC-SHA1'
  205. * @return string E.g. 'oauth_signature_method="HMAC-SHA1"'
  206. */
  207. public function authorizationHeaderParamEncode($name, $value) {
  208. return $this->parameterEncode($name) . '="' . $this->parameterEncode($value) . '"';
  209. }
  210. /**
  211. * Converts an associative array of name => value pairs to a numerically
  212. * indexed array of array('name' => '<name>', 'value' => '<value>') elements.
  213. *
  214. * @param array $array Associative array
  215. * @return array
  216. */
  217. public function assocToNumericNameValue($array) {
  218. $return = array();
  219. foreach ($array as $name => $value) {
  220. $return[] = array(
  221. 'name' => $name,
  222. 'value' => $value,
  223. );
  224. }
  225. return $return;
  226. }
  227. /**
  228. * User defined function to lexically sort an array of
  229. * array('name' => '<name>', 'value' => '<value>') elements by the value of
  230. * the name key, and if they're the same, then by the value of the value key.
  231. *
  232. * @param array $a Array with key for 'name' and one for 'value'
  233. * @param array $b Array with key for 'name' and one for 'value'
  234. * @return integer 1, 0 or -1 depending on whether a greater than b, less than
  235. * or the same.
  236. */
  237. public function sortByNameThenByValue($a, $b) {
  238. if ($a['name'] == $b['name']) {
  239. if ($a['value'] == $b['value']) {
  240. return 0;
  241. }
  242. return ($a['value'] > $b['value']) ? 1 : -1;
  243. }
  244. return ($a['name'] > $b['name']) ? 1 : -1;
  245. }
  246. /**
  247. * Encodes paramters as per the OAuth spec by utf 8 encoding the param (if it
  248. * is not already utf 8 encoded) and then percent encoding it according to
  249. * RFC3986
  250. *
  251. * @param string $param
  252. * @return string
  253. */
  254. public function parameterEncode($param) {
  255. $encoding = mb_detect_encoding($param);
  256. if ($encoding != 'UTF-8') {
  257. $param = mb_convert_encoding($param, 'UTF-8', $encoding);
  258. }
  259. $param = rawurlencode($param);
  260. $param = str_replace('%7E', '~', $param);
  261. return $param;
  262. }
  263. }