<?php
if (!defined('ABSPATH')) {
exit;
}
class WC_API_Authentication
{
public function __construct()
{
add_filter('woocommerce_api_check_authentication', array($this, 'authenticate'), 0);
}
public function authenticate($user)
{
if ('/' === WC()->api->server->path) {
return new WP_User(0);
}
try {
if (is_ssl()) {
$keys = $this->perform_ssl_authentication();
} else {
$keys = $this->perform_oauth_authentication();
}
$this->check_api_key_permissions($keys['permissions']);
$user = $this->get_user_by_id($keys['user_id']);
$this->update_api_key_last_access($keys['key_id']);
} catch (Exception $e) {
$user = new WP_Error('woocommerce_api_authentication_error', $e->getMessage(), array('status' => $e->getCode()));
}
return $user;
}
private function perform_ssl_authentication()
{
$params = WC()->api->server->params['GET'];
if (!empty($_SERVER['PHP_AUTH_USER'])) {
$consumer_key = $_SERVER['PHP_AUTH_USER'];
} elseif (!empty($params['consumer_key'])) {
$consumer_key = $params['consumer_key'];
} else {
throw new Exception(__('Consumer key is missing.', 'woocommerce'), 404);
}
if (!empty($_SERVER['PHP_AUTH_PW'])) {
$consumer_secret = $_SERVER['PHP_AUTH_PW'];
} elseif (!empty($params['consumer_secret'])) {
$consumer_secret = $params['consumer_secret'];
} else {
throw new Exception(__('Consumer secret is missing.', 'woocommerce'), 404);
}
$keys = $this->get_keys_by_consumer_key($consumer_key);
if (!$this->is_consumer_secret_valid($keys['consumer_secret'], $consumer_secret)) {
throw new Exception(__('Consumer secret is invalid.', 'woocommerce'), 401);
}
return $keys;
}
private function perform_oauth_authentication()
{
$params = WC()->api->server->params['GET'];
$param_names = array('oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method');
foreach ($param_names as $param_name) {
if (empty($params[$param_name])) {
throw new Exception(sprintf(__('%s parameter is missing', 'woocommerce'), $param_name), 404);
}
}
$keys = $this->get_keys_by_consumer_key($params['oauth_consumer_key']);
$this->check_oauth_signature($keys, $params);
$this->check_oauth_timestamp_and_nonce($keys, $params['oauth_timestamp'], $params['oauth_nonce']);
return $keys;
}
private function get_keys_by_consumer_key($consumer_key)
{
global $wpdb;
$consumer_key = wc_api_hash(sanitize_text_field($consumer_key));
$keys = $wpdb->get_row($wpdb->prepare("\n\t\t\tSELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces\n\t\t\tFROM {$wpdb->prefix}woocommerce_api_keys\n\t\t\tWHERE consumer_key = '%s'\n\t\t", $consumer_key), ARRAY_A);
if (empty($keys)) {
throw new Exception(__('Consumer key is invalid.', 'woocommerce'), 401);
}
return $keys;
}
private function get_user_by_id($user_id)
{
$user = get_user_by('id', $user_id);
if (!$user) {
throw new Exception(__('API user is invalid', 'woocommerce'), 401);
}
return $user;
}
private function is_consumer_secret_valid($keys_consumer_secret, $consumer_secret)
{
return hash_equals($keys_consumer_secret, $consumer_secret);
}
private function check_oauth_signature($keys, $params)
{
$http_method = strtoupper(WC()->api->server->method);
$base_request_uri = rawurlencode(untrailingslashit(get_woocommerce_api_url('')) . WC()->api->server->path);
$consumer_signature = rawurldecode(str_replace(' ', '+', $params['oauth_signature']));
unset($params['oauth_signature']);
if (isset($params['filter'])) {
$filters = $params['filter'];
unset($params['filter']);
foreach ($filters as $filter => $filter_value) {
$params['filter[' . $filter . ']'] = $filter_value;
}
}
$params = $this->normalize_parameters($params);
if (!uksort($params, 'strcmp')) {
throw new Exception(__('Invalid signature - failed to sort parameters.', 'woocommerce'), 401);
}
$query_params = array();
foreach ($params as $param_key => $param_value) {
$query_params[] = $param_key . '%3D' . $param_value;
}
$query_string = implode('%26', $query_params);
$string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string;
if ('HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method']) {
throw new Exception(__('Invalid signature - signature method is invalid.', 'woocommerce'), 401);
}
$hash_algorithm = strtolower(str_replace('HMAC-', '', $params['oauth_signature_method']));
$signature = base64_encode(hash_hmac($hash_algorithm, $string_to_sign, $keys['consumer_secret'], true));
if (!hash_equals($signature, $consumer_signature)) {
throw new Exception(__('Invalid signature - provided signature does not match.', 'woocommerce'), 401);
}
}
private function normalize_parameters($parameters)
{
$normalized_parameters = array();
foreach ($parameters as $key => $value) {
$key = str_replace('%', '%25', rawurlencode(rawurldecode($key)));
$value = str_replace('%', '%25', rawurlencode(rawurldecode($value)));
$normalized_parameters[$key] = $value;
}
return $normalized_parameters;
}
private function check_oauth_timestamp_and_nonce($keys, $timestamp, $nonce)
{
global $wpdb;
$valid_window = 15 * 60;
if ($timestamp < time() - $valid_window || $timestamp > time() + $valid_window) {
throw new Exception(__('Invalid timestamp.', 'woocommerce'), 401);
}
$used_nonces = maybe_unserialize($keys['nonces']);
if (empty($used_nonces)) {
$used_nonces = array();
}
if (in_array($nonce, $used_nonces)) {
throw new Exception(__('Invalid nonce - nonce has already been used.', 'woocommerce'), 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' => $keys['key_id']), array('%s'), array('%d'));
}
public function check_api_key_permissions($key_permissions)
{
switch (WC()->api->server->method) {
case 'HEAD':
case 'GET':
if ('read' !== $key_permissions && 'read_write' !== $key_permissions) {
throw new Exception(__('The API key provided does not have read permissions.', 'woocommerce'), 401);
}
break;
case 'POST':
case 'PUT':
case 'PATCH':
case 'DELETE':
if ('write' !== $key_permissions && 'read_write' !== $key_permissions) {
throw new Exception(__('The API key provided does not have write permissions.', 'woocommerce'), 401);
}
break;
}
}
private function update_api_key_last_access($key_id)
{
global $wpdb;
$wpdb->update($wpdb->prefix . 'woocommerce_api_keys', array('last_access' => current_time('mysql')), array('key_id' => $key_id), array('%s'), array('%d'));
}
}