Verify nonce in REST API?

I would like to understand the best practices regarding nonce validation in REST APIs.

I see a lot of people talking about wp_rest nonce for REST requests. But upon looking on WordPress core code, I saw that wp_rest is just a nonce to validate a logged in user status, if it's not present, it just runs the request as guest.

That said, should I submit two nonces upon sending a POST request to a REST API? One for authentication wp_rest and another for the action foo_action?

If so, how should I send wp_rest and foo_action nonce in JavaScript, and, in PHP, what's the correct place to validate those nonces? (I mean validate_callback for a arg? permission_callback?)

Topic nonce rest-api security customization Wordpress

Category Web


You should pass the special wp_rest nonce as part of the request. Without it, the global $current_user object will not be available in your REST class. You can pass this from several ways, from $_GET to $_POST to headers.

The action nonce is optional. If you add it, you can't use the REST endpoint from an external server, only from requests dispatched from within WordPress itself. The user can authenticate itself using Basic Auth, OAuth2, or JWT from an external server even without the wp_rest nonce, but if you add an action nonce as well, it won't work.

So the action nonce is optional. Add it if you want the endpoint to work locally only.

Example:

/**
* First step, registering, localizing and enqueueing the JavaScript
*/
wp_register_script( 'main-js', get_template_directory_uri() . '/js/main.js', [ 'jquery' ] );
wp_localize_script( 'main-js', 'data', [
    'rest' => [
        'endpoints' => [
            'my_endpoint'       => esc_url_raw( rest_url( 'my_plugin/v1/my_endpoint' ) ),
        ],
        'timeout'   => (int) apply_filters( "my_plugin_rest_timeout", 60 ),
        'nonce'     => wp_create_nonce( 'wp_rest' ),
        //'action_nonce'     => wp_create_nonce( 'action_nonce' ), 
    ],
] );
wp_enqueue_script( 'main-js' );

/**
* Second step, the request on the JavaScript file
*/
jQuery(document).on('click', '#some_element', function () {
    let ajax_data = {
        'some_value': jQuery( ".some_value" ).val(),
        //'action_nonce': data.rest.action_nonce
    };

    jQuery.ajax({
        url: data.rest.endpoints.my_endpoint,
        method: "GET",
        dataType: "json",
        timeout: data.rest.timeout,
        data: ajax_data,
        beforeSend: function (xhr) {
            xhr.setRequestHeader('X-WP-Nonce', data.rest.nonce);
        }
    }).done(function (results) {
        console.log(results);
        alert("Success!");
    }).fail(function (xhr) {
        console.log(results);
        alert("Error!");
    });
});

/**
* Third step, the REST endpoint itself
*/
class My_Endpoint {
        public function registerRoutes() {
        register_rest_route( 'my_plugin', 'v1/my_endpoint', [
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => [ $this, 'get_something' ],
            'args'                => [
                'some_value'     => [
                    'required' => true,
                ],
            ],
            'permission_callback' => function ( WP_REST_Request $request ) {
                return true;
            },
        ] );
    }

    /**
    *   @return WP_REST_Response
    */
    private function get_something( WP_REST_Request $request ) {
        //if ( ! wp_verify_nonce( $request['nonce'], 'action_nonce' ) ) {
        //  return false;
        //}

        $some_value        = $request['some_value'];

        if ( strlen( $some_value ) < 5 ) {
            return new WP_REST_Response( 'Sorry, Some Value must be at least 5 characters long.', 400 );
        }

        // Since we are passing the "X-WP-Nonce" header, this will work:
        $user = wp_get_current_user();

        if ( $user instanceof WP_User ) {
            return new WP_REST_Response( 'Sorry, could not get the name.', 400 );
        } else {
            return new WP_REST_Response( 'Your username name is: ' . $user->display_name, 200 );
        }
    }
}

Building on what @lucas-bustamante wrote (which helped me a ton!), once you have the X-WP-Nonce header setup in your custom routes you can do the following:

    register_rest_route('v1', '/my_post', [
        'methods' => WP_REST_Server::CREATABLE,
        'callback' => [$this, 'create_post'],
        'args' => [
            'post_title' => [
                'required' => true,
            ],
            'post_excerpt' => [
                'required' => true,
            ]
        ],
        'permission_callback' => function ( ) {
            return current_user_can( 'publish_posts' );
        },
    ]);

Note that the permission_callback is on the root level not under args (documented here) and I've removed the additional nonce check from args since checking the permission alone will fail if the nonce is invalid or not supplied (I've tested this extensively and can confirm I get an error when no nonce is supplied or it's invalid).

About

Geeks Mental is a community that publishes articles and tutorials about Web, Android, Data Science, new techniques and Linux security.