<?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($params['consumer_key']) && !empty($params['consumer_secret'])) {
$keys = $this->get_keys_by_consumer_key($params['consumer_key']);
if (!$this->is_consumer_secret_valid($keys['consumer_secret'], $params['consumer_secret'])) {
throw new Exception(__('Consumer secret is invalid.', 'woocommerce'), 401);
}
return $keys;
}
if (empty($_SERVER['PHP_AUTH_USER']) || empty($_SERVER['PHP_AUTH_PW'])) {
$this->exit_with_unauthorized_headers();
}
$keys = $this->get_keys_by_consumer_key($_SERVER['PHP_AUTH_USER']);
if (!$this->is_consumer_secret_valid($keys['consumer_secret'], $_SERVER['PHP_AUTH_PW'])) {
$this->exit_with_unauthorized_headers();
}
return $keys;
}
private function exit_with_unauthorized_headers()
{
$auth_message = __('WooCommerce API. Use a consumer key in the username field and a consumer secret in the password field.', 'woocommerce');
header('WWW-Authenticate: Basic realm="' . $auth_message . '"');
header('HTTP/1.0 401 Unauthorized');
throw new Exception(__('Consumer Secret is invalid.', 'woocommerce'), 401);
}
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);
$server_path = WC()->api->server->path;
if (isset($_SERVER['REDIRECT_URL']) && '/' === substr($_SERVER['REDIRECT_URL'], -1)) {
$server_path .= '/';
}
$base_request_uri = rawurlencode(untrailingslashit(get_woocommerce_api_url('')) . $server_path);
$consumer_signature = rawurldecode(str_replace(' ', '+', $params['oauth_signature']));
unset($params['oauth_signature']);
if (!uksort($params, 'strcmp')) {
throw new Exception(__('Invalid signature - failed to sort parameters.', 'woocommerce'), 401);
}
$params = $this->normalize_parameters($params);
$query_parameters = array();
foreach ($params as $param_key => $param_value) {
if (is_array($param_value)) {
foreach ($param_value as $param_key_inner => $param_value_inner) {
$query_parameters[] = $param_key . '%255B' . $param_key_inner . '%255D%3D' . $param_value_inner;
}
} else {
$query_parameters[] = $param_key . '%3D' . $param_value;
}
}
$query_string = implode('%26', $query_parameters);
$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']));
$secret = $keys['consumer_secret'] . '&';
$signature = base64_encode(hash_hmac($hash_algorithm, $string_to_sign, $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)
{
$keys = WC_API_Authentication::urlencode_rfc3986(array_keys($parameters));
$values = WC_API_Authentication::urlencode_rfc3986(array_values($parameters));
$parameters = array_combine($keys, $values);
return $parameters;
}
public static function urlencode_rfc3986($value)
{
if (is_array($value)) {
return array_map(array('WC_API_Authentication', 'urlencode_rfc3986'), $value);
} else {
return str_replace('%', '%25', rawurlencode(rawurldecode($value)));
}
}
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'));
}
}