class-wc-rest-authentication.php
<?php
defined( 'ABSPATH' ) || exit;
class WC_REST_Authentication {
protected $error = null;
protected $user = null;
protected $auth_method = '';
public static function instance(): self {
static $instance;
if ( ! isset( $instance ) ) {
$instance = new self();
}
return $instance;
}
public function __construct() {
add_filter( 'determine_current_user', array( $this, 'authenticate' ), 15 );
add_filter( 'rest_authentication_errors', array( $this, 'authentication_fallback' ) );
add_filter( 'rest_authentication_errors', array( $this, 'check_authentication_error' ), 15 );
add_filter( 'rest_post_dispatch', array( $this, 'send_unauthorized_headers' ), 50 );
add_filter( 'rest_pre_dispatch', array( $this, 'check_user_permissions' ), 10, 3 );
}
protected function is_request_to_rest_api() {
if ( empty( $_SERVER['REQUEST_URI'] ) ) {
return false;
}
$rest_prefix = trailingslashit( rest_get_url_prefix() );
$request_uri = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
$woocommerce = ( false !== strpos( $request_uri, $rest_prefix . 'wc/' ) );
$third_party = ( false !== strpos( $request_uri, $rest_prefix . 'wc-' ) );
return apply_filters( 'woocommerce_rest_is_request_to_rest_api', $woocommerce || $third_party );
}
public function authenticate( $user_id ) {
if ( ! empty( $user_id ) || ! $this->is_request_to_rest_api() ) {
return $user_id;
}
if ( is_ssl() ) {
$user_id = $this->perform_basic_authentication();
}
if ( $user_id ) {
return $user_id;
}
return $this->perform_oauth_authentication();
}
public function authentication_fallback( $error ) {
if ( ! empty( $error ) ) {
return $error;
}
if ( empty( $this->error ) && empty( $this->auth_method ) && empty( $this->user ) && 0 === get_current_user_id() ) {
$user_id = $this->authenticate( false );
if ( $user_id ) {
wp_set_current_user( $user_id );
return true;
}
}
return $error;
}
public function check_authentication_error( $error ) {
if ( ! empty( $error ) ) {
return $error;
}
return $this->get_error();
}
protected function set_error( $error ) {
$this->user = null;
$this->error = $error;
}
protected function get_error() {
return $this->error;
}
private function perform_basic_authentication() {
$this->auth_method = 'basic_auth';
$consumer_key = '';
$consumer_secret = '';
if ( ! empty( $_GET['consumer_key'] ) && ! empty( $_GET['consumer_secret'] ) ) {
$consumer_key = $_GET['consumer_key'];
$consumer_secret = $_GET['consumer_secret'];
}
if ( ! $consumer_key && ! empty( $_SERVER['PHP_AUTH_USER'] ) && ! empty( $_SERVER['PHP_AUTH_PW'] ) ) {
$consumer_key = $_SERVER['PHP_AUTH_USER'];
$consumer_secret = $_SERVER['PHP_AUTH_PW'];
}
if ( ! $consumer_key || ! $consumer_secret ) {
return false;
}
$this->user = $this->get_user_data_by_consumer_key( $consumer_key );
if ( empty( $this->user ) ) {
return false;
}
if ( ! hash_equals( $this->user->consumer_secret, $consumer_secret ) ) {
$this->set_error( new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer secret is invalid.', 'woocommerce' ), array( 'status' => 401 ) ) );
return false;
}
return $this->user->user_id;
}
public function parse_header( $header ) {
if ( 'OAuth ' !== substr( $header, 0, 6 ) ) {
return array();
}
$params = array();
if ( preg_match_all( '/(oauth_[a-z_-]*)=(:?"([^"]*)"|([^,]*))/', $header, $matches ) ) {
foreach ( $matches[1] as $i => $h ) {
$params[ $h ] = urldecode( empty( $matches[3][ $i ] ) ? $matches[4][ $i ] : $matches[3][ $i ] );
}
if ( isset( $params['realm'] ) ) {
unset( $params['realm'] );
}
}
return $params;
}
public function get_authorization_header() {
if ( ! empty( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
return wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] );
}
if ( function_exists( 'getallheaders' ) ) {
$headers = getallheaders();
foreach ( $headers as $key => $value ) {
if ( 'authorization' === strtolower( $key ) ) {
return $value;
}
}
}
return '';
}
public function get_oauth_parameters() {
$params = array_merge( $_GET, $_POST );
$params = wp_unslash( $params );
$header = $this->get_authorization_header();
if ( ! empty( $header ) ) {
$header = trim( $header );
$header_params = $this->parse_header( $header );
if ( ! empty( $header_params ) ) {
$params = array_merge( $params, $header_params );
}
}
$param_names = array(
'oauth_consumer_key',
'oauth_timestamp',
'oauth_nonce',
'oauth_signature',
'oauth_signature_method',
);
$errors = array();
$have_one = false;
foreach ( $param_names as $param_name ) {
if ( empty( $params[ $param_name ] ) ) {
$errors[] = $param_name;
} else {
$have_one = true;
}
}
if ( ! $have_one ) {
return array();
}
if ( ! empty( $errors ) ) {
$message = sprintf(
_n( 'Missing OAuth parameter %s', 'Missing OAuth parameters %s', count( $errors ), 'woocommerce' ),
implode( ', ', $errors )
);
$this->set_error( new WP_Error( 'woocommerce_rest_authentication_missing_parameter', $message, array( 'status' => 401 ) ) );
return array();
}
return $params;
}
private function perform_oauth_authentication() {
$this->auth_method = 'oauth1';
$params = $this->get_oauth_parameters();
if ( empty( $params ) ) {
return false;
}
$this->user = $this->get_user_data_by_consumer_key( $params['oauth_consumer_key'] );
if ( empty( $this->user ) ) {
$this->set_error( new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer key is invalid.', 'woocommerce' ), array( 'status' => 401 ) ) );
return false;
}
$signature = $this->check_oauth_signature( $this->user, $params );
if ( is_wp_error( $signature ) ) {
$this->set_error( $signature );
return false;
}
$timestamp_and_nonce = $this->check_oauth_timestamp_and_nonce( $this->user, $params['oauth_timestamp'], $params['oauth_nonce'] );
if ( is_wp_error( $timestamp_and_nonce ) ) {
$this->set_error( $timestamp_and_nonce );
return false;
}
return $this->user->user_id;
}
private function check_oauth_signature( $user, $params ) {
$http_method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( $_SERVER['REQUEST_METHOD'] ) : '';
$request_path = isset( $_SERVER['REQUEST_URI'] ) ? wp_parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ) : '';
$wp_base = get_home_url( null, '/', 'relative' );
if ( substr( $request_path, 0, strlen( $wp_base ) ) === $wp_base ) {
$request_path = substr( $request_path, strlen( $wp_base ) );
}
$base_request_uri = rawurlencode( get_home_url( null, $request_path, is_ssl() ? 'https' : 'http' ) );
$consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) );
unset( $params['oauth_signature'] );
if ( ! uksort( $params, 'strcmp' ) ) {
return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid signature - failed to sort parameters.', 'woocommerce' ), array( 'status' => 401 ) );
}
$params = $this->normalize_parameters( $params );
$query_string = implode( '%26', $this->join_with_equals_sign( $params ) );
$string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string;
if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) {
return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid signature - signature method is invalid.', 'woocommerce' ), array( 'status' => 401 ) );
}
$hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) );
$secret = $user->consumer_secret . '&';
$signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $secret, true ) );
if ( ! hash_equals( $signature, $consumer_signature ) ) {
return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid signature - provided signature does not match.', 'woocommerce' ), array( 'status' => 401 ) );
}
return true;
}
private function join_with_equals_sign( $params, $query_params = array(), $key = '' ) {
foreach ( $params as $param_key => $param_value ) {
if ( $key ) {
$param_key = $key . '%5B' . $param_key . '%5D';
}
if ( is_array( $param_value ) ) {
$query_params = $this->join_with_equals_sign( $param_value, $query_params, $param_key );
} else {
$string = $param_key . '=' . $param_value;
$query_params[] = wc_rest_urlencode_rfc3986( $string );
}
}
return $query_params;
}
private function normalize_parameters( $parameters ) {
$keys = wc_rest_urlencode_rfc3986( array_keys( $parameters ) );
$values = wc_rest_urlencode_rfc3986( array_values( $parameters ) );
$parameters = array_combine( $keys, $values );
return $parameters;
}
private function check_oauth_timestamp_and_nonce( $user, $timestamp, $nonce ) {
global $wpdb;
$valid_window = 15 * 60;
if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) {
return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid timestamp.', 'woocommerce' ), array( 'status' => 401 ) );
}
$used_nonces = maybe_unserialize( $user->nonces );
if ( empty( $used_nonces ) ) {
$used_nonces = array();
}
if ( in_array( $nonce, $used_nonces, true ) ) {
return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), array( 'status' => 401 ) );
}
$used_nonces[ $timestamp ] = $nonce;
foreach ( $used_nonces as $nonce_timestamp => $nonce ) {
if ( $nonce_timestamp < ( time() - $valid_window ) ) {
unset( $used_nonces[ $nonce_timestamp ] );
}
}
$used_nonces = maybe_serialize( $used_nonces );
$wpdb->update(
$wpdb->prefix . 'woocommerce_api_keys',
array( 'nonces' => $used_nonces ),
array( 'key_id' => $user->key_id ),
array( '%s' ),
array( '%d' )
);
return true;
}
private function get_user_data_by_consumer_key( $consumer_key ) {
global $wpdb;
$consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) );
$user = $wpdb->get_row(
$wpdb->prepare(
"
SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces
FROM {$wpdb->prefix}woocommerce_api_keys
WHERE consumer_key = %s
",
$consumer_key
)
);
return $user;
}
private function check_permissions( $method ) {
$permissions = $this->user->permissions;
switch ( $method ) {
case 'HEAD':
case 'GET':
if ( 'read' !== $permissions && 'read_write' !== $permissions ) {
return new WP_Error( 'woocommerce_rest_authentication_error', __( 'The API key provided does not have read permissions.', 'woocommerce' ), array( 'status' => 401 ) );
}
break;
case 'POST':
case 'PUT':
case 'PATCH':
case 'DELETE':
if ( 'write' !== $permissions && 'read_write' !== $permissions ) {
return new WP_Error( 'woocommerce_rest_authentication_error', __( 'The API key provided does not have write permissions.', 'woocommerce' ), array( 'status' => 401 ) );
}
break;
case 'OPTIONS':
return true;
default:
return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Unknown request method.', 'woocommerce' ), array( 'status' => 401 ) );
}
return true;
}
private function update_last_access( $request ) {
global $wp;
global $wpdb;
$do_not_record = true;
if ( is_a( $wp, WP::class ) && is_a( $request, WP_REST_Request::class ) ) {
$actual_http_request = trim( $wp->request, '/' );
$api_request_in_progress = trim( $request->get_route(), '/' );
$rest_prefix = trailingslashit( rest_get_url_prefix() );
if ( str_starts_with( $actual_http_request, $rest_prefix ) ) {
$actual_http_request = substr( $actual_http_request, strlen( $rest_prefix ) );
}
$do_not_record = $actual_http_request !== $api_request_in_progress;
}
if ( apply_filters( 'woocommerce_disable_rest_api_access_log', $do_not_record, $this->user->key_id, $this->user->user_id ) ) {
return;
}
$wpdb->update(
$wpdb->prefix . 'woocommerce_api_keys',
array( 'last_access' => current_time( 'mysql' ) ),
array( 'key_id' => $this->user->key_id ),
array( '%s' ),
array( '%d' )
);
}
public function send_unauthorized_headers( $response ) {
if ( is_wp_error( $this->get_error() ) && 'basic_auth' === $this->auth_method ) {
$auth_message = __( 'WooCommerce API. Use a consumer key in the username field and a consumer secret in the password field.', 'woocommerce' );
$response->header( 'WWW-Authenticate', 'Basic realm="' . $auth_message . '"', true );
}
return $response;
}
public function check_user_permissions( $result, $server, $request ) {
if ( $this->user ) {
$allowed = $this->check_permissions( $request->get_method() );
if ( is_wp_error( $allowed ) ) {
return $allowed;
}
$this->update_last_access( $request );
}
return $result;
}
}
WC_REST_Authentication::instance();