WC_API_Authentication

The WooCommerce WC API Authentication class.

Defined (3)

The class is defined in the following location(s).

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