3

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?)

5

If you are an advanced user, check if you forgot to pass the wp_rest nonce that makes the WP_User class available to the REST classes.

If you are an intermediate user who wants to know how to use the REST class, continue reading:

You should pass two nonces to the REST API class:

wp_rest: Special WordPress nonce. Without it, the WP_User object will not be available in your REST class. You can pass this from several ways, from $_GET to $_POST to headers. Check the source code for details.

your_custom_nonce: The custom nonce you set up on the front-end and validate when performing the action on the REST class.

<form>
    <input type="text" name="post_name">
    <input type="text" name="post_excerpt">
    <input type="hidden" name="rest_endpoint" value="<?= esc_url( rest_url( '/v1/my_post' ) ) ?>">
    <?= wp_nonce_field( 'custom_create_post_nonce', 'custom_create_post_nonce' ) ?>
    <a href="#">Submit</a>
</form>

<script>
    jQuery(document).on('click', 'form a', function () {
        create_post();
    });

    function create_post() {
        let post_name = jQuery('form').find('input[name="post_name"]').val();
        let post_excerpt = jQuery('form').find('input[name="post_excerpt"]').val();
        let nonce = jQuery('form').find('input[name="custom_create_post_nonce"]').val();
        let rest_endpoint = jQuery('form').find('input[name="rest_endpoint"]').val();

        let ajax_data = {
            'post_name': post_name,
            'post_excerpt': post_excerpt,
            'nonce': nonce,
        };
        jQuery.ajax({
            url: rest_endpoint,
            method: "POST",
            dataType: "json",
            timeout: 10000,
            data: ajax_data,
            beforeSend: function (xhr) {
                /**
                 * Without this, you can't use wp_verify_nonce(), current_user_can()
                 * etc on the REST class, because WP_User will be empty
                 */
                xhr.setRequestHeader('X-WP-Nonce', data.rest.nonce);
            }
        }).done(function (results) {
            alert('SUCCESS');
            console.log(results);
        }).fail(function (xhr) {
            alert('ERROR');
            console.log(xhr);
        });
    }
</script>

Then register the REST endpoint on the backend:

// hook at "rest_init" to register routes
add_action('rest_init', function() {
    (new Create_Post_REST)->register_routes();
});
class Create_Post_REST {
  public function register_routes() {
    register_rest_route('v1', '/my_post', [
        'methods' => WP_REST_Server::CREATABLE,
        'callback' => [$this, 'create_post'],
        'args' => [
            'post_title' => [
                'required' => true,
            ],
            'post_excerpt' => [
                'required' => true,
            ],
            'nonce' => [
                'validate_callback' => function($value) {
                    return wp_verify_nonce($value, 'custom_create_nonce');
                },
                'required' => true,
            ],
            'permission_callback' => function ( WP_REST_Request $request ) {
                return current_user_can( 'publish_posts' );
            },
        ]
    ]);
  }

  public function create_post(WP_REST_Request $request): WP_REST_Response {
    wp_insert_post([
      'post_title' => $request['post_title'],
      'post_excerpt' => $request['post_excerpt'],
    ]);

    return new WP_REST_Response('Success from REST!');
  }
}
  • 2
    It's important that you also add 'required' => true to the foo_nonce args otherwise you can still post to the endpoint without the nonce. – Dylan Jun 5 '19 at 3:04
  • Where does data.rest.nonce come from? I don't see that defined in your code. – drwatsoncode Jan 10 at 4:53
  • @drwatsoncode thanks, I'll update this answer in a few with that code. – Lucas Bustamante Jan 10 at 13:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.