PageRenderTime 40ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

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

https://bitbucket.org/bluespire/asha-wordpress
PHP | 410 lines | 181 code | 82 blank | 147 comment | 27 complexity | fdc561812efed2ee164ef4f368715183 MD5 | raw file
Possible License(s): LGPL-3.0, BitTorrent-1.0, BSD-3-Clause, MIT, GPL-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.0
  9. * @version 2.4.0
  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. }
  37. try {
  38. if ( is_ssl() ) {
  39. $keys = $this->perform_ssl_authentication();
  40. } else {
  41. $keys = $this->perform_oauth_authentication();
  42. }
  43. // Check API key-specific permission
  44. $this->check_api_key_permissions( $keys['permissions'] );
  45. $user = $this->get_user_by_id( $keys['user_id'] );
  46. $this->update_api_key_last_access( $keys['key_id'] );
  47. } catch ( Exception $e ) {
  48. $user = new WP_Error( 'woocommerce_api_authentication_error', $e->getMessage(), array( 'status' => $e->getCode() ) );
  49. }
  50. return $user;
  51. }
  52. /**
  53. * SSL-encrypted requests are not subject to sniffing or man-in-the-middle
  54. * attacks, so the request can be authenticated by simply looking up the user
  55. * associated with the given consumer key and confirming the consumer secret
  56. * provided is valid
  57. *
  58. * @since 2.1
  59. * @return array
  60. * @throws Exception
  61. */
  62. private function perform_ssl_authentication() {
  63. $params = WC()->api->server->params['GET'];
  64. // Get consumer key
  65. if ( ! empty( $_SERVER['PHP_AUTH_USER'] ) ) {
  66. // Should be in HTTP Auth header by default
  67. $consumer_key = $_SERVER['PHP_AUTH_USER'];
  68. } elseif ( ! empty( $params['consumer_key'] ) ) {
  69. // Allow a query string parameter as a fallback
  70. $consumer_key = $params['consumer_key'];
  71. } else {
  72. throw new Exception( __( 'Consumer key is missing.', 'woocommerce' ), 404 );
  73. }
  74. // Get consumer secret
  75. if ( ! empty( $_SERVER['PHP_AUTH_PW'] ) ) {
  76. // Should be in HTTP Auth header by default
  77. $consumer_secret = $_SERVER['PHP_AUTH_PW'];
  78. } elseif ( ! empty( $params['consumer_secret'] ) ) {
  79. // Allow a query string parameter as a fallback
  80. $consumer_secret = $params['consumer_secret'];
  81. } else {
  82. throw new Exception( __( 'Consumer secret is missing.', 'woocommerce' ), 404 );
  83. }
  84. $keys = $this->get_keys_by_consumer_key( $consumer_key );
  85. if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $consumer_secret ) ) {
  86. throw new Exception( __( 'Consumer secret is invalid.', 'woocommerce' ), 401 );
  87. }
  88. return $keys;
  89. }
  90. /**
  91. * Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests
  92. *
  93. * This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP
  94. *
  95. * This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions:
  96. *
  97. * 1) There is no token associated with request/responses, only consumer keys/secrets are used
  98. *
  99. * 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header,
  100. * This is because there is no cross-OS function within PHP to get the raw Authorization header
  101. *
  102. * @link http://tools.ietf.org/html/rfc5849 for the full spec
  103. * @since 2.1
  104. * @return array
  105. * @throws Exception
  106. */
  107. private function perform_oauth_authentication() {
  108. $params = WC()->api->server->params['GET'];
  109. $param_names = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method' );
  110. // Check for required OAuth parameters
  111. foreach ( $param_names as $param_name ) {
  112. if ( empty( $params[ $param_name ] ) ) {
  113. /* translators: %s: parameter name */
  114. throw new Exception( sprintf( __( '%s parameter is missing', 'woocommerce' ), $param_name ), 404 );
  115. }
  116. }
  117. // Fetch WP user by consumer key
  118. $keys = $this->get_keys_by_consumer_key( $params['oauth_consumer_key'] );
  119. // Perform OAuth validation
  120. $this->check_oauth_signature( $keys, $params );
  121. $this->check_oauth_timestamp_and_nonce( $keys, $params['oauth_timestamp'], $params['oauth_nonce'] );
  122. // Authentication successful, return user
  123. return $keys;
  124. }
  125. /**
  126. * Return the keys for the given consumer key
  127. *
  128. * @since 2.4.0
  129. * @param string $consumer_key
  130. * @return array
  131. * @throws Exception
  132. */
  133. private function get_keys_by_consumer_key( $consumer_key ) {
  134. global $wpdb;
  135. $consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) );
  136. $keys = $wpdb->get_row( $wpdb->prepare( "
  137. SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces
  138. FROM {$wpdb->prefix}woocommerce_api_keys
  139. WHERE consumer_key = '%s'
  140. ", $consumer_key ), ARRAY_A );
  141. if ( empty( $keys ) ) {
  142. throw new Exception( __( 'Consumer key is invalid.', 'woocommerce' ), 401 );
  143. }
  144. return $keys;
  145. }
  146. /**
  147. * Get user by ID
  148. *
  149. * @since 2.4.0
  150. * @param int $user_id
  151. * @return WC_User
  152. * @throws Exception
  153. */
  154. private function get_user_by_id( $user_id ) {
  155. $user = get_user_by( 'id', $user_id );
  156. if ( ! $user ) {
  157. throw new Exception( __( 'API user is invalid', 'woocommerce' ), 401 );
  158. }
  159. return $user;
  160. }
  161. /**
  162. * Check if the consumer secret provided for the given user is valid
  163. *
  164. * @since 2.1
  165. * @param string $keys_consumer_secret
  166. * @param string $consumer_secret
  167. * @return bool
  168. */
  169. private function is_consumer_secret_valid( $keys_consumer_secret, $consumer_secret ) {
  170. return hash_equals( $keys_consumer_secret, $consumer_secret );
  171. }
  172. /**
  173. * Verify that the consumer-provided request signature matches our generated signature, this ensures the consumer
  174. * has a valid key/secret
  175. *
  176. * @param array $keys
  177. * @param array $params the request parameters
  178. * @throws Exception
  179. */
  180. private function check_oauth_signature( $keys, $params ) {
  181. $http_method = strtoupper( WC()->api->server->method );
  182. $base_request_uri = rawurlencode( untrailingslashit( get_woocommerce_api_url( '' ) ) . WC()->api->server->path );
  183. // Get the signature provided by the consumer and remove it from the parameters prior to checking the signature
  184. $consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) );
  185. unset( $params['oauth_signature'] );
  186. // Remove filters and convert them from array to strings to void normalize issues
  187. if ( isset( $params['filter'] ) ) {
  188. $filters = $params['filter'];
  189. unset( $params['filter'] );
  190. foreach ( $filters as $filter => $filter_value ) {
  191. $params[ 'filter[' . $filter . ']' ] = $filter_value;
  192. }
  193. }
  194. // Normalize parameter key/values
  195. $params = $this->normalize_parameters( $params );
  196. // Sort parameters
  197. if ( ! uksort( $params, 'strcmp' ) ) {
  198. throw new Exception( __( 'Invalid signature - failed to sort parameters.', 'woocommerce' ), 401 );
  199. }
  200. // Form query string
  201. $query_params = array();
  202. foreach ( $params as $param_key => $param_value ) {
  203. $query_params[] = $param_key . '%3D' . $param_value; // join with equals sign
  204. }
  205. $query_string = implode( '%26', $query_params ); // join with ampersand
  206. $string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string;
  207. if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) {
  208. throw new Exception( __( 'Invalid signature - signature method is invalid.', 'woocommerce' ), 401 );
  209. }
  210. $hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) );
  211. $signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $keys['consumer_secret'], true ) );
  212. if ( ! hash_equals( $signature, $consumer_signature ) ) {
  213. throw new Exception( __( 'Invalid signature - provided signature does not match.', 'woocommerce' ), 401 );
  214. }
  215. }
  216. /**
  217. * Normalize each parameter by assuming each parameter may have already been
  218. * encoded, so attempt to decode, and then re-encode according to RFC 3986
  219. *
  220. * Note both the key and value is normalized so a filter param like:
  221. *
  222. * 'filter[period]' => 'week'
  223. *
  224. * is encoded to:
  225. *
  226. * 'filter%5Bperiod%5D' => 'week'
  227. *
  228. * This conforms to the OAuth 1.0a spec which indicates the entire query string
  229. * should be URL encoded
  230. *
  231. * @since 2.1
  232. * @see rawurlencode()
  233. * @param array $parameters un-normalized pararmeters
  234. * @return array normalized parameters
  235. */
  236. private function normalize_parameters( $parameters ) {
  237. $normalized_parameters = array();
  238. foreach ( $parameters as $key => $value ) {
  239. // Percent symbols (%) must be double-encoded
  240. $key = str_replace( '%', '%25', rawurlencode( rawurldecode( $key ) ) );
  241. $value = str_replace( '%', '%25', rawurlencode( rawurldecode( $value ) ) );
  242. $normalized_parameters[ $key ] = $value;
  243. }
  244. return $normalized_parameters;
  245. }
  246. /**
  247. * Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where
  248. * an attacker could attempt to re-send an intercepted request at a later time.
  249. *
  250. * - A timestamp is valid if it is within 15 minutes of now
  251. * - A nonce is valid if it has not been used within the last 15 minutes
  252. *
  253. * @param array $keys
  254. * @param int $timestamp the unix timestamp for when the request was made
  255. * @param string $nonce a unique (for the given user) 32 alphanumeric string, consumer-generated
  256. * @throws Exception
  257. */
  258. private function check_oauth_timestamp_and_nonce( $keys, $timestamp, $nonce ) {
  259. global $wpdb;
  260. $valid_window = 15 * 60; // 15 minute window
  261. if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) {
  262. throw new Exception( __( 'Invalid timestamp.', 'woocommerce' ) );
  263. }
  264. $used_nonces = maybe_unserialize( $keys['nonces'] );
  265. if ( empty( $used_nonces ) ) {
  266. $used_nonces = array();
  267. }
  268. if ( in_array( $nonce, $used_nonces ) ) {
  269. throw new Exception( __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), 401 );
  270. }
  271. $used_nonces[ $timestamp ] = $nonce;
  272. // Remove expired nonces
  273. foreach ( $used_nonces as $nonce_timestamp => $nonce ) {
  274. if ( $nonce_timestamp < ( time() - $valid_window ) ) {
  275. unset( $used_nonces[ $nonce_timestamp ] );
  276. }
  277. }
  278. $used_nonces = maybe_serialize( $used_nonces );
  279. $wpdb->update(
  280. $wpdb->prefix . 'woocommerce_api_keys',
  281. array( 'nonces' => $used_nonces ),
  282. array( 'key_id' => $keys['key_id'] ),
  283. array( '%s' ),
  284. array( '%d' )
  285. );
  286. }
  287. /**
  288. * Check that the API keys provided have the proper key-specific permissions to either read or write API resources
  289. *
  290. * @param string $key_permissions
  291. * @throws Exception if the permission check fails
  292. */
  293. public function check_api_key_permissions( $key_permissions ) {
  294. switch ( WC()->api->server->method ) {
  295. case 'HEAD':
  296. case 'GET':
  297. if ( 'read' !== $key_permissions && 'read_write' !== $key_permissions ) {
  298. throw new Exception( __( 'The API key provided does not have read permissions.', 'woocommerce' ), 401 );
  299. }
  300. break;
  301. case 'POST':
  302. case 'PUT':
  303. case 'PATCH':
  304. case 'DELETE':
  305. if ( 'write' !== $key_permissions && 'read_write' !== $key_permissions ) {
  306. throw new Exception( __( 'The API key provided does not have write permissions.', 'woocommerce' ), 401 );
  307. }
  308. break;
  309. }
  310. }
  311. /**
  312. * Updated API Key last access datetime
  313. *
  314. * @since 2.4.0
  315. *
  316. * @param int $key_id
  317. */
  318. private function update_api_key_last_access( $key_id ) {
  319. global $wpdb;
  320. $wpdb->update(
  321. $wpdb->prefix . 'woocommerce_api_keys',
  322. array( 'last_access' => current_time( 'mysql' ) ),
  323. array( 'key_id' => $key_id ),
  324. array( '%s' ),
  325. array( '%d' )
  326. );
  327. }
  328. }