PageRenderTime 53ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

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

https://gitlab.com/webkod3r/tripolis
PHP | 414 lines | 191 code | 68 blank | 155 comment | 33 complexity | b684134336102373ca646198f91d5a13 MD5 | raw file
  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. // if the $_GET parameters are present, use those first
  65. if ( ! empty( $params['consumer_key'] ) && ! empty( $params['consumer_secret'] ) ) {
  66. $keys = $this->get_keys_by_consumer_key( $params['consumer_key'] );
  67. if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $params['consumer_secret'] ) ) {
  68. throw new Exception( __( 'Consumer Secret is invalid', 'woocommerce' ), 401 );
  69. }
  70. return $keys;
  71. }
  72. // if the above is not present, we will do full basic auth
  73. if ( empty( $_SERVER['PHP_AUTH_USER'] ) || empty( $_SERVER['PHP_AUTH_PW'] ) ) {
  74. $this->exit_with_unauthorized_headers();
  75. }
  76. $keys = $this->get_keys_by_consumer_key( $_SERVER['PHP_AUTH_USER'] );
  77. if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $_SERVER['PHP_AUTH_PW'] ) ) {
  78. $this->exit_with_unauthorized_headers();
  79. }
  80. return $keys;
  81. }
  82. /**
  83. * If the consumer_key and consumer_secret $_GET parameters are NOT provided
  84. * and the Basic auth headers are either not present or the consumer secret does not match the consumer
  85. * key provided, then return the correct Basic headers and an error message.
  86. *
  87. * @since 2.4
  88. */
  89. private function exit_with_unauthorized_headers() {
  90. $auth_message = __( 'WooCommerce API. Use a consumer key in the username field and a consumer secret in the password field', 'woocommerce' );
  91. header( 'WWW-Authenticate: Basic realm="' . $auth_message . '"' );
  92. header( 'HTTP/1.0 401 Unauthorized' );
  93. throw new Exception( __( 'Consumer Secret is invalid', 'woocommerce' ), 401 );
  94. }
  95. /**
  96. * Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests
  97. *
  98. * This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP
  99. *
  100. * This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions:
  101. *
  102. * 1) There is no token associated with request/responses, only consumer keys/secrets are used
  103. *
  104. * 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header,
  105. * This is because there is no cross-OS function within PHP to get the raw Authorization header
  106. *
  107. * @link http://tools.ietf.org/html/rfc5849 for the full spec
  108. * @since 2.1
  109. * @return array
  110. * @throws Exception
  111. */
  112. private function perform_oauth_authentication() {
  113. $params = WC()->api->server->params['GET'];
  114. $param_names = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method' );
  115. // Check for required OAuth parameters
  116. foreach ( $param_names as $param_name ) {
  117. if ( empty( $params[ $param_name ] ) ) {
  118. throw new Exception( sprintf( __( '%s parameter is missing', 'woocommerce' ), $param_name ), 404 );
  119. }
  120. }
  121. // Fetch WP user by consumer key
  122. $keys = $this->get_keys_by_consumer_key( $params['oauth_consumer_key'] );
  123. // Perform OAuth validation
  124. $this->check_oauth_signature( $keys, $params );
  125. $this->check_oauth_timestamp_and_nonce( $keys, $params['oauth_timestamp'], $params['oauth_nonce'] );
  126. // Authentication successful, return user
  127. return $keys;
  128. }
  129. /**
  130. * Return the keys for the given consumer key
  131. *
  132. * @since 2.4.0
  133. * @param string $consumer_key
  134. * @return array
  135. * @throws Exception
  136. */
  137. private function get_keys_by_consumer_key( $consumer_key ) {
  138. global $wpdb;
  139. $consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) );
  140. $keys = $wpdb->get_row( $wpdb->prepare( "
  141. SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces
  142. FROM {$wpdb->prefix}woocommerce_api_keys
  143. WHERE consumer_key = '%s'
  144. ", $consumer_key ), ARRAY_A );
  145. if ( empty( $keys ) ) {
  146. throw new Exception( __( 'Consumer Key is invalid', 'woocommerce' ), 401 );
  147. }
  148. return $keys;
  149. }
  150. /**
  151. * Get user by ID
  152. *
  153. * @since 2.4.0
  154. * @param int $user_id
  155. * @return WC_User
  156. * @throws Exception
  157. */
  158. private function get_user_by_id( $user_id ) {
  159. $user = get_user_by( 'id', $user_id );
  160. if ( ! $user ) {
  161. throw new Exception( __( 'API user is invalid', 'woocommerce' ), 401 );
  162. }
  163. return $user;
  164. }
  165. /**
  166. * Check if the consumer secret provided for the given user is valid
  167. *
  168. * @since 2.1
  169. * @param string $keys_consumer_secret
  170. * @param string $consumer_secret
  171. * @return bool
  172. */
  173. private function is_consumer_secret_valid( $keys_consumer_secret, $consumer_secret ) {
  174. return hash_equals( $keys_consumer_secret, $consumer_secret );
  175. }
  176. /**
  177. * Verify that the consumer-provided request signature matches our generated signature, this ensures the consumer
  178. * has a valid key/secret
  179. *
  180. * @param array $keys
  181. * @param array $params the request parameters
  182. * @throws Exception
  183. */
  184. private function check_oauth_signature( $keys, $params ) {
  185. $http_method = strtoupper( WC()->api->server->method );
  186. $server_path = WC()->api->server->path;
  187. // if the requested URL has a trailingslash, make sure our base URL does as well
  188. if ( isset( $_SERVER['REDIRECT_URL'] ) && '/' === substr( $_SERVER['REDIRECT_URL'], -1 ) ) {
  189. $server_path .= '/';
  190. }
  191. $base_request_uri = rawurlencode( untrailingslashit( get_woocommerce_api_url( '' ) ) . $server_path );
  192. // Get the signature provided by the consumer and remove it from the parameters prior to checking the signature
  193. $consumer_signature = rawurldecode( $params['oauth_signature'] );
  194. unset( $params['oauth_signature'] );
  195. // Sort parameters
  196. if ( ! uksort( $params, 'strcmp' ) ) {
  197. throw new Exception( __( 'Invalid Signature - failed to sort parameters', 'woocommerce' ), 401 );
  198. }
  199. // Normalize parameter key/values
  200. $params = $this->normalize_parameters( $params );
  201. $query_parameters = array();
  202. foreach ( $params as $param_key => $param_value ) {
  203. if ( is_array( $param_value ) ) {
  204. foreach ( $param_value as $param_key_inner => $param_value_inner ) {
  205. $query_parameters[] = $param_key . '%255B' . $param_key_inner . '%255D%3D' . $param_value_inner;
  206. }
  207. } else {
  208. $query_parameters[] = $param_key . '%3D' . $param_value; // join with equals sign
  209. }
  210. }
  211. $query_string = implode( '%26', $query_parameters ); // join with ampersand
  212. $string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string;
  213. if ( $params['oauth_signature_method'] !== 'HMAC-SHA1' && $params['oauth_signature_method'] !== 'HMAC-SHA256' ) {
  214. throw new Exception( __( 'Invalid Signature - signature method is invalid', 'woocommerce' ), 401 );
  215. }
  216. $hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) );
  217. $secret = $keys['consumer_secret'] . '&';
  218. $signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $secret, true ) );
  219. if ( ! hash_equals( $signature, $consumer_signature ) ) {
  220. throw new Exception( __( 'Invalid Signature - provided signature does not match', 'woocommerce' ), 401 );
  221. }
  222. }
  223. /**
  224. * Normalize each parameter by assuming each parameter may have already been
  225. * encoded, so attempt to decode, and then re-encode according to RFC 3986
  226. *
  227. * Note both the key and value is normalized so a filter param like:
  228. *
  229. * 'filter[period]' => 'week'
  230. *
  231. * is encoded to:
  232. *
  233. * 'filter%5Bperiod%5D' => 'week'
  234. *
  235. * This conforms to the OAuth 1.0a spec which indicates the entire query string
  236. * should be URL encoded
  237. *
  238. * @since 2.1
  239. * @see rawurlencode()
  240. * @param array $parameters un-normalized pararmeters
  241. * @return array normalized parameters
  242. */
  243. private function normalize_parameters( $parameters ) {
  244. $keys = WC_API_Authentication::urlencode_rfc3986( array_keys( $parameters ) );
  245. $values = WC_API_Authentication::urlencode_rfc3986( array_values( $parameters ) );
  246. $parameters = array_combine( $keys, $values );
  247. return $parameters;
  248. }
  249. /**
  250. * Encodes a value according to RFC 3986. Supports multidimensional arrays.
  251. *
  252. * @since 2.4
  253. * @param string|array $value The value to encode
  254. * @return string|array Encoded values
  255. */
  256. public static function urlencode_rfc3986( $value ) {
  257. if ( is_array( $value ) ) {
  258. return array_map( array( 'WC_API_Authentication', 'urlencode_rfc3986' ), $value );
  259. } else {
  260. // Percent symbols (%) must be double-encoded
  261. return str_replace( '%', '%25', rawurlencode( rawurldecode( $value ) ) );
  262. }
  263. }
  264. /**
  265. * Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where
  266. * an attacker could attempt to re-send an intercepted request at a later time.
  267. *
  268. * - A timestamp is valid if it is within 15 minutes of now
  269. * - A nonce is valid if it has not been used within the last 15 minutes
  270. *
  271. * @param array $keys
  272. * @param int $timestamp the unix timestamp for when the request was made
  273. * @param string $nonce a unique (for the given user) 32 alphanumeric string, consumer-generated
  274. * @throws Exception
  275. */
  276. private function check_oauth_timestamp_and_nonce( $keys, $timestamp, $nonce ) {
  277. global $wpdb;
  278. $valid_window = 15 * 60; // 15 minute window
  279. if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) {
  280. throw new Exception( __( 'Invalid timestamp', 'woocommerce' ), 401 );
  281. }
  282. $used_nonces = maybe_unserialize( $keys['nonces'] );
  283. if ( empty( $used_nonces ) ) {
  284. $used_nonces = array();
  285. }
  286. if ( in_array( $nonce, $used_nonces ) ) {
  287. throw new Exception( __( 'Invalid nonce - nonce has already been used', 'woocommerce' ), 401 );
  288. }
  289. $used_nonces[ $timestamp ] = $nonce;
  290. // Remove expired nonces
  291. foreach ( $used_nonces as $nonce_timestamp => $nonce ) {
  292. if ( $nonce_timestamp < ( time() - $valid_window ) ) {
  293. unset( $used_nonces[ $nonce_timestamp ] );
  294. }
  295. }
  296. $used_nonces = maybe_serialize( $used_nonces );
  297. $wpdb->update(
  298. $wpdb->prefix . 'woocommerce_api_keys',
  299. array( 'nonces' => $used_nonces ),
  300. array( 'key_id' => $keys['key_id'] ),
  301. array( '%s' ),
  302. array( '%d' )
  303. );
  304. }
  305. /**
  306. * Check that the API keys provided have the proper key-specific permissions to either read or write API resources
  307. *
  308. * @param string $key_permissions
  309. * @throws Exception if the permission check fails
  310. */
  311. public function check_api_key_permissions( $key_permissions ) {
  312. switch ( WC()->api->server->method ) {
  313. case 'HEAD':
  314. case 'GET':
  315. if ( 'read' !== $key_permissions && 'read_write' !== $key_permissions ) {
  316. throw new Exception( __( 'The API key provided does not have read permissions', 'woocommerce' ), 401 );
  317. }
  318. break;
  319. case 'POST':
  320. case 'PUT':
  321. case 'PATCH':
  322. case 'DELETE':
  323. if ( 'write' !== $key_permissions && 'read_write' !== $key_permissions ) {
  324. throw new Exception( __( 'The API key provided does not have write permissions', 'woocommerce' ), 401 );
  325. }
  326. break;
  327. }
  328. }
  329. /**
  330. * Updated API Key last access datetime
  331. *
  332. * @since 2.4.0
  333. *
  334. * @param int $key_id
  335. */
  336. private function update_api_key_last_access( $key_id ) {
  337. global $wpdb;
  338. $wpdb->update(
  339. $wpdb->prefix . 'woocommerce_api_keys',
  340. array( 'last_access' => current_time( 'mysql' ) ),
  341. array( 'key_id' => $key_id ),
  342. array( '%s' ),
  343. array( '%d' )
  344. );
  345. }
  346. }