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