CreateIT
CreateIT
BLOG

Woo API – how to whitelist endpoints

WooCommerce

Woo API – how to whitelist endpoints

SHARE

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.

WooCommerce

That’s it for this tutorial. Make sure to follow us for other useful tips and guidelines.

Need help?

  • Looking for support from experienced programmers?

  • Need to fix a bug in the code?

  • Want to customize your webste/application?

ADD COMMENT

Your email address will not be published. Required fields are marked *

createIT Contact