WC_REST_Authentication

The WooCommerce WC REST Authentication class.

Defined (1)

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

/includes/api/class-wc-rest-authentication.php  
  1. class WC_REST_Authentication { 
  2.  
  3. /** 
  4. * Initialize authentication actions. 
  5. */ 
  6. public function __construct() { 
  7. add_filter( 'determine_current_user', array( $this, 'authenticate' ), 15 ); 
  8. add_filter( 'rest_authentication_errors', array( $this, 'check_authentication_error' ) ); 
  9. add_filter( 'rest_post_dispatch', array( $this, 'send_unauthorized_headers' ), 50 ); 
  10.  
  11. /** 
  12. * Check if is request to our REST API. 
  13. * @return bool 
  14. */ 
  15. protected function is_request_to_rest_api() { 
  16. if ( empty( $_SERVER['REQUEST_URI'] ) ) { 
  17. return false; 
  18.  
  19. $rest_prefix = trailingslashit( rest_get_url_prefix() ); 
  20.  
  21. // Check if our endpoint. 
  22. $woocommerce = ( false !== strpos( $_SERVER['REQUEST_URI'], $rest_prefix . 'wc/' ) ); 
  23.  
  24. // Allow third party plugins use our authentication methods. 
  25. $third_party = ( false !== strpos( $_SERVER['REQUEST_URI'], $rest_prefix . 'wc-' ) ); 
  26.  
  27. return apply_filters( 'woocommerce_rest_is_request_to_rest_api', $woocommerce || $third_party ); 
  28.  
  29. /** 
  30. * Authenticate user. 
  31. * @param int|false $user_id User ID if one has been determined, false otherwise. 
  32. * @return int|false 
  33. */ 
  34. public function authenticate( $user_id ) { 
  35. // Do not authenticate twice and check if is a request to our endpoint in the WP REST API. 
  36. if ( ! empty( $user_id ) || ! $this->is_request_to_rest_api() ) { 
  37. return $user_id; 
  38.  
  39. if ( is_ssl() ) { 
  40. return $this->perform_basic_authentication(); 
  41. } else { 
  42. return $this->perform_oauth_authentication(); 
  43.  
  44. /** 
  45. * Check for authentication error. 
  46. * @param WP_Error|null|bool $error 
  47. * @return WP_Error|null|bool 
  48. */ 
  49. public function check_authentication_error( $error ) { 
  50. global $wc_rest_authentication_error; 
  51.  
  52. // Passthrough other errors. 
  53. if ( ! empty( $error ) ) { 
  54. return $error; 
  55.  
  56. return $wc_rest_authentication_error; 
  57.  
  58. /** 
  59. * Basic Authentication. 
  60. * SSL-encrypted requests are not subject to sniffing or man-in-the-middle 
  61. * attacks, so the request can be authenticated by simply looking up the user 
  62. * associated with the given consumer key and confirming the consumer secret 
  63. * provided is valid. 
  64. * @return int|bool 
  65. */ 
  66. private function perform_basic_authentication() { 
  67. global $wc_rest_authentication_error; 
  68.  
  69. $consumer_key = ''; 
  70. $consumer_secret = ''; 
  71.  
  72. // If the $_GET parameters are present, use those first. 
  73. if ( ! empty( $_GET['consumer_key'] ) && ! empty( $_GET['consumer_secret'] ) ) { 
  74. $consumer_key = $_GET['consumer_key']; 
  75. $consumer_secret = $_GET['consumer_secret']; 
  76.  
  77. // If the above is not present, we will do full basic auth. 
  78. if ( ! $consumer_key && ! empty( $_SERVER['PHP_AUTH_USER'] ) && ! empty( $_SERVER['PHP_AUTH_PW'] ) ) { 
  79. $consumer_key = $_SERVER['PHP_AUTH_USER']; 
  80. $consumer_secret = $_SERVER['PHP_AUTH_PW']; 
  81.  
  82. // Stop if don't have any key. 
  83. if ( ! $consumer_key || ! $consumer_secret ) { 
  84. return false; 
  85.  
  86. // Get user data. 
  87. $user = $this->get_user_data_by_consumer_key( $consumer_key ); 
  88. if ( empty( $user ) ) { 
  89. return false; 
  90.  
  91. // Validate user secret. 
  92. if ( ! hash_equals( $user->consumer_secret, $consumer_secret ) ) { 
  93. $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer secret is invalid.', 'woocommerce' ), array( 'status' => 401 ) ); 
  94.  
  95. return false; 
  96.  
  97. // Check API Key permissions. 
  98. if ( ! $this->check_permissions( $user->permissions ) ) { 
  99. return false; 
  100.  
  101. // Update last access. 
  102. $this->update_last_access( $user->key_id ); 
  103.  
  104. return $user->user_id; 
  105.  
  106. /** 
  107. * Parse the Authorization header into parameters. 
  108. * @since 3.0.0 
  109. * @param string $header Authorization header value (not including "Authorization: " prefix). 
  110. * @return array Map of parameter values. 
  111. */ 
  112. public function parse_header( $header ) { 
  113. if ( 'OAuth ' !== substr( $header, 0, 6 ) ) { 
  114. return array(); 
  115.  
  116. // From OAuth PHP library, used under MIT license. 
  117. $params = array(); 
  118. if ( preg_match_all( '/(oauth_[a-z_-]*)=(:?"([^"]*)"|([^, ]*))/', $header, $matches ) ) { 
  119. foreach ( $matches[1] as $i => $h ) { 
  120. $params[ $h ] = urldecode( empty( $matches[3][ $i ] ) ? $matches[4][ $i ] : $matches[3][ $i ] ); 
  121. if ( isset( $params['realm'] ) ) { 
  122. unset( $params['realm'] ); 
  123.  
  124. return $params; 
  125.  
  126. /** 
  127. * Get the authorization header. 
  128. * On certain systems and configurations, the Authorization header will be 
  129. * stripped out by the server or PHP. Typically this is then used to 
  130. * generate `PHP_AUTH_USER`/`PHP_AUTH_PASS` but not passed on. We use 
  131. * `getallheaders` here to try and grab it out instead. 
  132. * @since 3.0.0 
  133. * @return string Authorization header if set. 
  134. */ 
  135. public function get_authorization_header() { 
  136. if ( ! empty( $_SERVER['HTTP_AUTHORIZATION'] ) ) { 
  137. return wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ); 
  138.  
  139. if ( function_exists( 'getallheaders' ) ) { 
  140. $headers = getallheaders(); 
  141. // Check for the authoization header case-insensitively. 
  142. foreach ( $headers as $key => $value ) { 
  143. if ( 'authorization' === strtolower( $key ) ) { 
  144. return $value; 
  145.  
  146. return ''; 
  147.  
  148. /** 
  149. * Get oAuth parameters from $_GET, $_POST or request header. 
  150. * @since 3.0.0 
  151. * @return array|WP_Error 
  152. */ 
  153. public function get_oauth_parameters() { 
  154. global $wc_rest_authentication_error; 
  155.  
  156. $params = array_merge( $_GET, $_POST ); 
  157. $params = wp_unslash( $params ); 
  158. $header = $this->get_authorization_header(); 
  159.  
  160. if ( ! empty( $header ) ) { 
  161. // Trim leading spaces. 
  162. $header = trim( $header ); 
  163. $header_params = $this->parse_header( $header ); 
  164.  
  165. if ( ! empty( $header_params ) ) { 
  166. $params = array_merge( $params, $header_params ); 
  167.  
  168. $param_names = array( 
  169. 'oauth_consumer_key',  
  170. 'oauth_timestamp',  
  171. 'oauth_nonce',  
  172. 'oauth_signature',  
  173. 'oauth_signature_method',  
  174. ); 
  175.  
  176. $errors = array(); 
  177. $have_one = false; 
  178.  
  179. // Check for required OAuth parameters. 
  180. foreach ( $param_names as $param_name ) { 
  181. if ( empty( $params[ $param_name ] ) ) { 
  182. $errors[] = $param_name; 
  183. } else { 
  184. $have_one = true; 
  185.  
  186. // All keys are missing, so we're probably not even trying to use OAuth. 
  187. if ( ! $have_one ) { 
  188. return array(); 
  189.  
  190. // If we have at least one supplied piece of data, and we have an error,  
  191. // then it's a failed authentication. 
  192. if ( ! empty( $errors ) ) { 
  193. $message = sprintf( 
  194. _n( 'Missing OAuth parameter %s', 'Missing OAuth parameters %s', count( $errors ), 'woocommerce' ),  
  195. implode( ', ', $errors ) 
  196. ); 
  197.  
  198. $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_missing_parameter', $message, array( 'status' => 401 ) ); 
  199.  
  200. return array(); 
  201.  
  202. return $params; 
  203.  
  204. /** 
  205. * Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests. 
  206. * This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP. 
  207. * This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions: 
  208. * 1) There is no token associated with request/responses, only consumer keys/secrets are used. 
  209. * 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header,  
  210. * This is because there is no cross-OS function within PHP to get the raw Authorization header. 
  211. * @link http://tools.ietf.org/html/rfc5849 for the full spec. 
  212. * @return int|bool 
  213. */ 
  214. private function perform_oauth_authentication() { 
  215. global $wc_rest_authentication_error; 
  216.  
  217. $params = $this->get_oauth_parameters(); 
  218. if ( empty( $params ) ) { 
  219. return false; 
  220.  
  221. // Fetch WP user by consumer key. 
  222. $user = $this->get_user_data_by_consumer_key( $params['oauth_consumer_key'] ); 
  223.  
  224. if ( empty( $user ) ) { 
  225. $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer key is invalid.', 'woocommerce' ), array( 'status' => 401 ) ); 
  226.  
  227. return false; 
  228.  
  229. // Perform OAuth validation. 
  230. $wc_rest_authentication_error = $this->check_oauth_signature( $user, $params ); 
  231. if ( is_wp_error( $wc_rest_authentication_error ) ) { 
  232. return false; 
  233.  
  234. $wc_rest_authentication_error = $this->check_oauth_timestamp_and_nonce( $user, $params['oauth_timestamp'], $params['oauth_nonce'] ); 
  235. if ( is_wp_error( $wc_rest_authentication_error ) ) { 
  236. return false; 
  237.  
  238. // Check API Key permissions. 
  239. if ( ! $this->check_permissions( $user->permissions ) ) { 
  240. return false; 
  241.  
  242. // Update last access. 
  243. $this->update_last_access( $user->key_id ); 
  244.  
  245. return $user->user_id; 
  246.  
  247. /** 
  248. * Verify that the consumer-provided request signature matches our generated signature,  
  249. * this ensures the consumer has a valid key/secret. 
  250. * @param stdClass $user 
  251. * @param array $params The request parameters. 
  252. * @return null|WP_Error 
  253. */ 
  254. private function check_oauth_signature( $user, $params ) { 
  255. $http_method = strtoupper( $_SERVER['REQUEST_METHOD'] ); 
  256. $request_path = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ); 
  257. $wp_base = get_home_url( null, '/', 'relative' ); 
  258. if ( substr( $request_path, 0, strlen( $wp_base ) ) === $wp_base ) { 
  259. $request_path = substr( $request_path, strlen( $wp_base ) ); 
  260. $base_request_uri = rawurlencode( get_home_url( null, $request_path ) ); 
  261.  
  262. // Get the signature provided by the consumer and remove it from the parameters prior to checking the signature. 
  263. $consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) ); 
  264. unset( $params['oauth_signature'] ); 
  265.  
  266. // Sort parameters. 
  267. if ( ! uksort( $params, 'strcmp' ) ) { 
  268. return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid signature - failed to sort parameters.', 'woocommerce' ), array( 'status' => 401 ) ); 
  269.  
  270. // Normalize parameter key/values. 
  271. $params = $this->normalize_parameters( $params ); 
  272. $query_parameters = array(); 
  273. foreach ( $params as $param_key => $param_value ) { 
  274. if ( is_array( $param_value ) ) { 
  275. foreach ( $param_value as $param_key_inner => $param_value_inner ) { 
  276. $query_parameters[] = $param_key . '%255B' . $param_key_inner . '%255D%3D' . $param_value_inner; 
  277. } else { 
  278. $query_parameters[] = $param_key . '%3D' . $param_value; // Join with equals sign. 
  279. $query_string = implode( '%26', $query_parameters ); // Join with ampersand. 
  280. $string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string; 
  281.  
  282. if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) { 
  283. return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid signature - signature method is invalid.', 'woocommerce' ), array( 'status' => 401 ) ); 
  284.  
  285. $hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) ); 
  286. $secret = $user->consumer_secret . '&'; 
  287. $signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $secret, true ) ); 
  288.  
  289. if ( ! hash_equals( $signature, $consumer_signature ) ) { 
  290. return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid signature - provided signature does not match.', 'woocommerce' ), array( 'status' => 401 ) ); 
  291.  
  292. return true; 
  293.  
  294. /** 
  295. * Normalize each parameter by assuming each parameter may have already been 
  296. * encoded, so attempt to decode, and then re-encode according to RFC 3986. 
  297. * Note both the key and value is normalized so a filter param like: 
  298. * 'filter[period]' => 'week' 
  299. * is encoded to: 
  300. * 'filter%5Bperiod%5D' => 'week' 
  301. * This conforms to the OAuth 1.0a spec which indicates the entire query string 
  302. * should be URL encoded. 
  303. * @see rawurlencode() 
  304. * @param array $parameters Un-normalized pararmeters. 
  305. * @return array Normalized parameters. 
  306. */ 
  307. private function normalize_parameters( $parameters ) { 
  308. $keys = wc_rest_urlencode_rfc3986( array_keys( $parameters ) ); 
  309. $values = wc_rest_urlencode_rfc3986( array_values( $parameters ) ); 
  310. $parameters = array_combine( $keys, $values ); 
  311.  
  312. return $parameters; 
  313.  
  314. /** 
  315. * Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where 
  316. * an attacker could attempt to re-send an intercepted request at a later time. 
  317. * - A timestamp is valid if it is within 15 minutes of now. 
  318. * - A nonce is valid if it has not been used within the last 15 minutes. 
  319. * @param stdClass $user 
  320. * @param int $timestamp the unix timestamp for when the request was made 
  321. * @param string $nonce a unique (for the given user) 32 alphanumeric string, consumer-generated 
  322. * @return bool|WP_Error 
  323. */ 
  324. private function check_oauth_timestamp_and_nonce( $user, $timestamp, $nonce ) { 
  325. global $wpdb; 
  326.  
  327. $valid_window = 15 * 60; // 15 minute window. 
  328.  
  329. if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) { 
  330. return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid timestamp.', 'woocommerce' ), array( 'status' => 401 ) ); 
  331.  
  332. $used_nonces = maybe_unserialize( $user->nonces ); 
  333.  
  334. if ( empty( $used_nonces ) ) { 
  335. $used_nonces = array(); 
  336.  
  337. if ( in_array( $nonce, $used_nonces ) ) { 
  338. return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), array( 'status' => 401 ) ); 
  339.  
  340. $used_nonces[ $timestamp ] = $nonce; 
  341.  
  342. // Remove expired nonces. 
  343. foreach ( $used_nonces as $nonce_timestamp => $nonce ) { 
  344. if ( $nonce_timestamp < ( time() - $valid_window ) ) { 
  345. unset( $used_nonces[ $nonce_timestamp ] ); 
  346.  
  347. $used_nonces = maybe_serialize( $used_nonces ); 
  348.  
  349. $wpdb->update( 
  350. $wpdb->prefix . 'woocommerce_api_keys',  
  351. array( 'nonces' => $used_nonces ),  
  352. array( 'key_id' => $user->key_id ),  
  353. array( '%s' ),  
  354. array( '%d' ) 
  355. ); 
  356.  
  357. return true; 
  358.  
  359. /** 
  360. * Return the user data for the given consumer_key. 
  361. * @param string $consumer_key 
  362. * @return array 
  363. */ 
  364. private function get_user_data_by_consumer_key( $consumer_key ) { 
  365. global $wpdb; 
  366.  
  367. $consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) ); 
  368. $user = $wpdb->get_row( $wpdb->prepare( " 
  369. SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces 
  370. FROM {$wpdb->prefix}woocommerce_api_keys 
  371. WHERE consumer_key = %s 
  372. ", $consumer_key ) ); 
  373.  
  374. return $user; 
  375.  
  376. /** 
  377. * Check that the API keys provided have the proper key-specific permissions to either read or write API resources. 
  378. * @param string $permissions 
  379. * @return bool 
  380. */ 
  381. private function check_permissions( $permissions ) { 
  382. global $wc_rest_authentication_error; 
  383.  
  384. $valid = true; 
  385.  
  386. if ( ! isset( $_SERVER['REQUEST_METHOD'] ) ) { 
  387. return false; 
  388.  
  389. switch ( $_SERVER['REQUEST_METHOD'] ) { 
  390.  
  391. case 'HEAD' : 
  392. case 'GET' : 
  393. if ( 'read' !== $permissions && 'read_write' !== $permissions ) { 
  394. $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'The API key provided does not have read permissions.', 'woocommerce' ), array( 'status' => 401 ) ); 
  395. $valid = false; 
  396. break; 
  397.  
  398. case 'POST' : 
  399. case 'PUT' : 
  400. case 'PATCH' : 
  401. case 'DELETE' : 
  402. if ( 'write' !== $permissions && 'read_write' !== $permissions ) { 
  403. $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'The API key provided does not have write permissions.', 'woocommerce' ), array( 'status' => 401 ) ); 
  404. $valid = false; 
  405. break; 
  406.  
  407. return $valid; 
  408.  
  409. /** 
  410. * Updated API Key last access datetime. 
  411. * @param int $key_id 
  412. */ 
  413. private function update_last_access( $key_id ) { 
  414. global $wpdb; 
  415.  
  416. $wpdb->update( 
  417. $wpdb->prefix . 'woocommerce_api_keys',  
  418. array( 'last_access' => current_time( 'mysql' ) ),  
  419. array( 'key_id' => $key_id ),  
  420. array( '%s' ),  
  421. array( '%d' ) 
  422. ); 
  423.  
  424. /** 
  425. * If the consumer_key and consumer_secret $_GET parameters are NOT provided 
  426. * and the Basic auth headers are either not present or the consumer secret does not match the consumer 
  427. * key provided, then return the correct Basic headers and an error message. 
  428. * @param WP_REST_Response $response Current response being served. 
  429. * @return WP_REST_Response 
  430. */ 
  431. public function send_unauthorized_headers( $response ) { 
  432. global $wc_rest_authentication_error; 
  433.  
  434. if ( is_wp_error( $wc_rest_authentication_error ) && is_ssl() ) { 
  435. $auth_message = __( 'WooCommerce API. Use a consumer key in the username field and a consumer secret in the password field.', 'woocommerce' ); 
  436. $response->header( 'WWW-Authenticate', 'Basic realm="' . $auth_message . '"', true ); 
  437.  
  438. return $response;