PageRenderTime 48ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/wp-content/plugins/woocommerce/includes/api/v1/class-wc-api-authentication.php

https://bitbucket.org/theshipswakecreative/psw
PHP | 357 lines | 147 code | 79 blank | 131 comment | 25 complexity | 442cd1ae09a872720960577a7a5f6d3b MD5 | raw file
Possible License(s): LGPL-3.0, Apache-2.0
  1. <?php
  2. /**
  3. * WooCommerce API Authentication Class
  4. *
  5. * @author WooThemes
  6. * @category API
  7. * @package WooCommerce/API
  8. * @since 2.1
  9. * @version 2.1
  10. */
  11. if ( ! defined( 'ABSPATH' ) ) {
  12. exit; // Exit if accessed directly
  13. }
  14. class WC_API_Authentication {
  15. /**
  16. * Setup class
  17. *
  18. * @since 2.1
  19. * @return WC_API_Authentication
  20. */
  21. public function __construct() {
  22. // to disable authentication, hook into this filter at a later priority and return a valid WP_User
  23. add_filter( 'woocommerce_api_check_authentication', array( $this, 'authenticate' ), 0 );
  24. }
  25. /**
  26. * Authenticate the request. The authentication method varies based on whether the request was made over SSL or not.
  27. *
  28. * @since 2.1
  29. * @param WP_User $user
  30. * @return null|WP_Error|WP_User
  31. */
  32. public function authenticate( $user ) {
  33. // allow access to the index by default
  34. if ( '/' === WC()->api->server->path )
  35. return new WP_User(0);
  36. try {
  37. if ( is_ssl() )
  38. $user = $this->perform_ssl_authentication();
  39. else
  40. $user = $this->perform_oauth_authentication();
  41. // check API key-specific permission
  42. $this->check_api_key_permissions( $user );
  43. } catch ( Exception $e ) {
  44. $user = new WP_Error( 'woocommerce_api_authentication_error', $e->getMessage(), array( 'status' => $e->getCode() ) );
  45. }
  46. return $user;
  47. }
  48. /**
  49. * SSL-encrypted requests are not subject to sniffing or man-in-the-middle
  50. * attacks, so the request can be authenticated by simply looking up the user
  51. * associated with the given consumer key and confirming the consumer secret
  52. * provided is valid
  53. *
  54. * @since 2.1
  55. * @return WP_User
  56. * @throws Exception
  57. */
  58. private function perform_ssl_authentication() {
  59. $params = WC()->api->server->params['GET'];
  60. // get consumer key
  61. if ( ! empty( $_SERVER['PHP_AUTH_USER'] ) ) {
  62. // should be in HTTP Auth header by default
  63. $consumer_key = $_SERVER['PHP_AUTH_USER'];
  64. } elseif ( ! empty( $params['consumer_key'] ) ) {
  65. // allow a query string parameter as a fallback
  66. $consumer_key = $params['consumer_key'];
  67. } else {
  68. throw new Exception( __( 'Consumer Key is missing', 'woocommerce' ), 404 );
  69. }
  70. // get consumer secret
  71. if ( ! empty( $_SERVER['PHP_AUTH_PW'] ) ) {
  72. // should be in HTTP Auth header by default
  73. $consumer_secret = $_SERVER['PHP_AUTH_PW'];
  74. } elseif ( ! empty( $params['consumer_secret'] ) ) {
  75. // allow a query string parameter as a fallback
  76. $consumer_secret = $params['consumer_secret'];
  77. } else {
  78. throw new Exception( __( 'Consumer Secret is missing', 'woocommerce' ), 404 );
  79. }
  80. $user = $this->get_user_by_consumer_key( $consumer_key );
  81. if ( ! $this->is_consumer_secret_valid( $user, $consumer_secret ) ) {
  82. throw new Exception( __( 'Consumer Secret is invalid', 'woocommerce' ), 401 );
  83. }
  84. return $user;
  85. }
  86. /**
  87. * Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests
  88. *
  89. * This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP
  90. *
  91. * This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions:
  92. *
  93. * 1) There is no token associated with request/responses, only consumer keys/secrets are used
  94. *
  95. * 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header,
  96. * This is because there is no cross-OS function within PHP to get the raw Authorization header
  97. *
  98. * @link http://tools.ietf.org/html/rfc5849 for the full spec
  99. * @since 2.1
  100. * @return WP_User
  101. * @throws Exception
  102. */
  103. private function perform_oauth_authentication() {
  104. $params = WC()->api->server->params['GET'];
  105. $param_names = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method' );
  106. // check for required OAuth parameters
  107. foreach ( $param_names as $param_name ) {
  108. if ( empty( $params[ $param_name ] ) )
  109. throw new Exception( sprintf( __( '%s parameter is missing', 'woocommerce' ), $param_name ), 404 );
  110. }
  111. // fetch WP user by consumer key
  112. $user = $this->get_user_by_consumer_key( $params['oauth_consumer_key'] );
  113. // perform OAuth validation
  114. $this->check_oauth_signature( $user, $params );
  115. $this->check_oauth_timestamp_and_nonce( $user, $params['oauth_timestamp'], $params['oauth_nonce'] );
  116. // authentication successful, return user
  117. return $user;
  118. }
  119. /**
  120. * Return the user for the given consumer key
  121. *
  122. * @since 2.1
  123. * @param string $consumer_key
  124. * @return WP_User
  125. * @throws Exception
  126. */
  127. private function get_user_by_consumer_key( $consumer_key ) {
  128. $user_query = new WP_User_Query(
  129. array(
  130. 'meta_key' => 'woocommerce_api_consumer_key',
  131. 'meta_value' => $consumer_key,
  132. )
  133. );
  134. $users = $user_query->get_results();
  135. if ( empty( $users[0] ) )
  136. throw new Exception( __( 'Consumer Key is invalid', 'woocommerce' ), 401 );
  137. return $users[0];
  138. }
  139. /**
  140. * Check if the consumer secret provided for the given user is valid
  141. *
  142. * @since 2.1
  143. * @param WP_User $user
  144. * @param string $consumer_secret
  145. * @return bool
  146. */
  147. private function is_consumer_secret_valid( WP_User $user, $consumer_secret ) {
  148. return $user->woocommerce_api_consumer_secret === $consumer_secret;
  149. }
  150. /**
  151. * Verify that the consumer-provided request signature matches our generated signature, this ensures the consumer
  152. * has a valid key/secret
  153. *
  154. * @param WP_User $user
  155. * @param array $params the request parameters
  156. * @throws Exception
  157. */
  158. private function check_oauth_signature( $user, $params ) {
  159. $http_method = strtoupper( WC()->api->server->method );
  160. $base_request_uri = rawurlencode( untrailingslashit( get_woocommerce_api_url( '' ) ) . WC()->api->server->path );
  161. // get the signature provided by the consumer and remove it from the parameters prior to checking the signature
  162. $consumer_signature = rawurldecode( $params['oauth_signature'] );
  163. unset( $params['oauth_signature'] );
  164. // remove filters and convert them from array to strings to void normalize issues
  165. if ( isset( $params['filter'] ) ) {
  166. $filters = $params['filter'];
  167. unset( $params['filter'] );
  168. foreach ( $filters as $filter => $filter_value ) {
  169. $params['filter[' . $filter . ']'] = $filter_value;
  170. }
  171. }
  172. // normalize parameter key/values
  173. $params = $this->normalize_parameters( $params );
  174. // sort parameters
  175. if ( ! uksort( $params, 'strcmp' ) ) {
  176. throw new Exception( __( 'Invalid Signature - failed to sort parameters', 'woocommerce' ), 401 );
  177. }
  178. // form query string
  179. $query_params = array();
  180. foreach ( $params as $param_key => $param_value ) {
  181. $query_params[] = $param_key . '%3D' . $param_value; // join with equals sign
  182. }
  183. $query_string = implode( '%26', $query_params ); // join with ampersand
  184. $string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string;
  185. if ( $params['oauth_signature_method'] !== 'HMAC-SHA1' && $params['oauth_signature_method'] !== 'HMAC-SHA256' ) {
  186. throw new Exception( __( 'Invalid Signature - signature method is invalid', 'woocommerce' ), 401 );
  187. }
  188. $hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) );
  189. $signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $user->woocommerce_api_consumer_secret, true ) );
  190. if ( $signature !== $consumer_signature ) {
  191. throw new Exception( __( 'Invalid Signature - provided signature does not match', 'woocommerce' ), 401 );
  192. }
  193. }
  194. /**
  195. * Normalize each parameter by assuming each parameter may have already been
  196. * encoded, so attempt to decode, and then re-encode according to RFC 3986
  197. *
  198. * Note both the key and value is normalized so a filter param like:
  199. *
  200. * 'filter[period]' => 'week'
  201. *
  202. * is encoded to:
  203. *
  204. * 'filter%5Bperiod%5D' => 'week'
  205. *
  206. * This conforms to the OAuth 1.0a spec which indicates the entire query string
  207. * should be URL encoded
  208. *
  209. * @since 2.1
  210. * @see rawurlencode()
  211. * @param array $parameters un-normalized pararmeters
  212. * @return array normalized parameters
  213. */
  214. private function normalize_parameters( $parameters ) {
  215. $normalized_parameters = array();
  216. foreach ( $parameters as $key => $value ) {
  217. // percent symbols (%) must be double-encoded
  218. $key = str_replace( '%', '%25', rawurlencode( rawurldecode( $key ) ) );
  219. $value = str_replace( '%', '%25', rawurlencode( rawurldecode( $value ) ) );
  220. $normalized_parameters[ $key ] = $value;
  221. }
  222. return $normalized_parameters;
  223. }
  224. /**
  225. * Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where
  226. * an attacker could attempt to re-send an intercepted request at a later time.
  227. *
  228. * - A timestamp is valid if it is within 15 minutes of now
  229. * - A nonce is valid if it has not been used within the last 15 minutes
  230. *
  231. * @param WP_User $user
  232. * @param int $timestamp the unix timestamp for when the request was made
  233. * @param string $nonce a unique (for the given user) 32 alphanumeric string, consumer-generated
  234. * @throws Exception
  235. */
  236. private function check_oauth_timestamp_and_nonce( $user, $timestamp, $nonce ) {
  237. $valid_window = 15 * 60; // 15 minute window
  238. if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) )
  239. throw new Exception( __( 'Invalid timestamp', 'woocommerce' ) );
  240. $used_nonces = $user->woocommerce_api_nonces;
  241. if ( empty( $used_nonces ) )
  242. $used_nonces = array();
  243. if ( in_array( $nonce, $used_nonces ) )
  244. throw new Exception( __( 'Invalid nonce - nonce has already been used', 'woocommerce' ), 401 );
  245. $used_nonces[ $timestamp ] = $nonce;
  246. // remove expired nonces
  247. foreach( $used_nonces as $nonce_timestamp => $nonce ) {
  248. if ( $nonce_timestamp < ( time() - $valid_window ) )
  249. unset( $used_nonces[ $nonce_timestamp ] );
  250. }
  251. update_user_meta( $user->ID, 'woocommerce_api_nonces', $used_nonces );
  252. }
  253. /**
  254. * Check that the API keys provided have the proper key-specific permissions to either read or write API resources
  255. *
  256. * @param WP_User $user
  257. * @throws Exception if the permission check fails
  258. */
  259. public function check_api_key_permissions( $user ) {
  260. $key_permissions = $user->woocommerce_api_key_permissions;
  261. switch ( WC()->api->server->method ) {
  262. case 'HEAD':
  263. case 'GET':
  264. if ( 'read' !== $key_permissions && 'read_write' !== $key_permissions ) {
  265. throw new Exception( __( 'The API key provided does not have read permissions', 'woocommerce' ), 401 );
  266. }
  267. break;
  268. case 'POST':
  269. case 'PUT':
  270. case 'PATCH':
  271. case 'DELETE':
  272. if ( 'write' !== $key_permissions && 'read_write' !== $key_permissions ) {
  273. throw new Exception( __( 'The API key provided does not have write permissions', 'woocommerce' ), 401 );
  274. }
  275. break;
  276. }
  277. }
  278. }