Woo API – how to whitelist endpoints
CHALLENGE: restrict access to WooCommerce built-in endpoints
SOLUTION: use the ‘rest_authentication_errors’ and ‘rest_endpoints’ filters
Special access permissions are needed to access the WooCommerce API (a pair of keys: Consumer key and secret). We can also restrict access type (Read/Write). A person who knows the keys has access to all API routes. Sometimes, it is recommended to restrict access and whitelist only really needed endpoints.
WooCommerce routes
To find out which routes are currently available, we can use the /wp-json/wc/v3 API endpoint. The response will return all routes in JSON format. To have better visualization, we can use the ‘WP API SwaggerUI’ plugin that will show the /wc/v3 list using permalink: /rest-api/docs/ . Each route can have a different method: GET, POST, PUT, PATCH or DELETE.
How to block the WooCommerce API route?
There are 2 filters that can be used to blacklist / whitelist the Woo Route. The first filter, ‘rest_endpoints’, disables route definition. The route will be blocked and become invisible on the list. The second filter, ‘rest_authentication_errors’, makes it possible to block route on authentication. The route will be visible on the list, but blocked when trying to use it.
Another difference is that the ‘rest_authentication_errors’ filter is not used in internal API calls in PHP (new WP_REST_Request()). I imagine a setup where we use different endpoints internally by doing internal API calls, but blacklisting them for external usage. If that’s the case, then ‘rest_authentication_errors’ can be used.
Whitelist REST endpoints
We will prepare a PHP class that will give us full control over which endpoints are accessible for users with API keys. The configuration is done using private arrays written at the beginning of the class. The first step is to blacklist all routes that start with ‘/wc/v3’, so all routes are currently blocked.
$whitelisted_routes is a list of route names that should be whitelisted. Keep in mind that all routes that “start with” those strings will be accessible. If we need even more control, to whitelist only a particular method (exact match) we will be using $whitelisted_exact_match. We can define route name and method, and whitelist only a specific one.
Here is a PHP function for whitelisting WooCommerce API routes. With the current configuration all endpoints that start with /wc/v3/customers and /wc/v3/payment_gateways will be available. In addition, /wc/v3/orders [GET] /wc/v3/orders/{id} [GET] will also be available as exact match.
<?php
// functions.php
/**
* Whitelist WP Rest API Endpoints
*/
class CT_whitelist_api_endpoints{
// Please note that item /wp-json/route/ will also whitelist: /wp-json/route/.*
// block all woocommerce endpoints
private $blocked_routes = array(
'/wc/v3/'
);
// some exceptions (all routes that "starts with" will be whitelisted)
private $whitelisted_routes = array(
'/wc/v3/customers',
'/wc/v3/payment_gateways',
);
// whitelist exact match for endpoint name
private $whitelisted_exact_match = array(
array('route' => '/wc/v3/orders', 'method' => 'GET'),
array('route' => '/wc/v3/orders/(?P<id>[\d]+)', 'method' => 'GET'),
);
function __construct() {
// disable routes definition, internal api calls will be not working
add_filter( 'rest_endpoints', array($this,'ct_rest_endpoints') );
// alternative solution, endpoint will be blocked on authentication, all internal api calls still will be working
add_filter( 'rest_authentication_errors', array($this,'ct_rest_authentication_errors'));
}
/**
* @param $result
* @return bool|mixed|WP_Error
* Whitelist WP Rest API endpoints, block other endpoints
*/
public function ct_rest_authentication_errors($result)
{
// If a previous authentication check was applied,
// pass that result along without modification.
if (true === $result || is_wp_error($result)) {
return $result;
}
if (!empty($result)) {
return $result;
}
// full path (with parameters)
$current_route = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
// clean up
$current_route = str_replace('/wp-json','',$current_route);
// allow all by default
$route_allowed = true;
// now block some endpoints
foreach ($this->blocked_routes as $blocked_route) {
if (substr($current_route, 0, strlen($blocked_route)) === $blocked_route) {
$route_allowed = false;
break;
}
}
// exceptions
foreach ($this->whitelisted_routes as $whitelisted_route) {
if (substr($current_route, 0, strlen($whitelisted_route)) === $whitelisted_route) {
$route_allowed = true;
break;
}
}
// exception for: exact match
$current_route_base = strtok($current_route, '?');
$current_route_base = rtrim($current_route_base, '/');
$exact_match = array_keys(array_column($this->whitelisted_exact_match, 'route'), $current_route_base);
if(!empty($exact_match)){
$route_allowed = true;
}
if ($route_allowed) {
return $result;
};
return new WP_Error('rest_cannot_access', __('This WP endpoint is disabled', 'disable-json-api'), array('status' => rest_authorization_required_code()));
}
/**
* Disable some WP Rest API Routes Endpoints
* Add some exceptions
*/
public function ct_rest_endpoints($endpoints){
if(isset($endpoints['/wc/v3/coupons/batch'])){
// debugging this particular filter
// var_dump($endpoints['/wc/v3/coupons/batch']);
// exit;
};
$block_route = false;
foreach( $endpoints as $route => $endpoint ){
// block items
// debugging route names
// var_dump($route);
if( $this->striposarray( $route, $this->blocked_routes ) ){
$block_route = true;
}
// add exceptions
if( $this->striposarray( $route, $this->whitelisted_routes ) ){
$block_route = false;
}
// add exception for full match
$exact_match_array_keys = array_keys(array_column($this->whitelisted_exact_match, 'route'), $route);
if(!empty($exact_match_array_keys)){
// whitelist entire endpoint (all methods)
$block_route = false;
foreach($endpoint as $item_key => $item_elem) {
// block methods that are not whitelisted
if (isset($item_elem['methods'])) {
$block_method = true;
foreach($exact_match_array_keys as $exact_match_key ){
// tricky.. method can be: string(16) "POST, PUT, PATCH" or just "POST"
if (strpos($item_elem['methods'], $this->whitelisted_exact_match[$exact_match_key]['method']) !== false) {
$block_method = false;
break;
}
}
if($block_method){
unset($endpoints[ $route ][$item_key]);
}
}
}
}
if($block_route){
unset( $endpoints[ $route ] );
}
}
return $endpoints;
}
private function striposarray($haystack, $needle, $offset=0) {
if(!is_array($needle)) {
$needle = array($needle);
}
foreach($needle as $query) {
if(stripos($haystack, $query, $offset) !== false){
// stop on first true result
return true;
}
}
return false;
}
}
$ctWhitelisted = new CT_whitelist_api_endpoints();
WordPress API security
That’s it. The function will control access to our API. By default, all built-in WooCommerce routes are blacklisted and secured. We give access only to selected endpoints. Here is a screenshot from Swagger documentation listing a confirmation that our code is working correctly.
That’s it for this tutorial. Make sure to follow us for other useful tips and guidelines.