Identical wp_rest nonce returned from rest_api

All we need is an easy explanation of the problem, so here it is.

TLDR: How can I force a new nonce to be returned, so that AJAX works without refreshing the page?

Following on from Nonces and Ajax request to REST API and verification I implemented the Nonce functionality, with callback permissions and the ‘intent’ of my routes are now secure.

I enqueue Axios, with wp_rest nonce as a local script setting.

    // Register custom variables for the AJAX script.
    wp_localize_script( 'axios', 'axiosScriptVars', [
        'root'  => esc_url_raw( rest_url() ),
        'nonce' => wp_create_nonce( 'wp_rest' ),
    ]);

I set my Ajax headers up with axiosScriptVars.nonce. I POST – this works. I return a NONCE from my REST_API endpoint’s response, and update my Ajax header ready for the next POST…

Example of my log in and log out route:

function ajax_logout() {
    try {
    wp_logout();
    $nonce = wp_create_nonce( 'wp_rest' );
    return array('loggedin'=>false, 'message'=>__('Logged out successfully'), 'name'=>false, 'email'=>false, 'nonce'=>$nonce );
    } catch (Exception $e) {
        echo 'Caught exception: ',  $e->getMessage(), "\n";
        die;
    }
}


function ajax_login(){
    $_POST = json_decode(file_get_contents("php://input"),true);

    $info = array();
    $info['user_login'] = !empty($_POST['username']) ? sanitize_text_field( $_POST['username'] ) : null;
    $info['user_password'] = !empty( $_POST['password'] ) ? sanitize_text_field( $_POST['password'] ) : null;
    $info['remember'] = true;
    
    $user = wp_signon( $info, true );
    $nonce = wp_create_nonce( 'wp_rest' );

    if ( is_wp_error($user) ){
        return array('loggedin'=>false, 'message'=>__('Wrong username or password.'), 'name'=>false, 'email'=>false, 'nonce'=>$nonce );
    } else {
        return array('loggedin'=>true, 'message'=>__('Login successful'), 'name'=>$user->data->display_name, 'email'=>$user->data->user_email, 'nonce'=>$nonce );
    }
}


add_action( 'rest_api_init', function () {
    register_rest_route( 'rw-user/v1', '/log-in', array(
        'methods' => 'POST',
        'callback' => 'ajax_login',
        'permission_callback' => '__return_true'
    ));
    register_rest_route('rw-user/v1','/log-out', array(
        'methods' => 'POST',
        "callback" => 'ajax_logout',
        "permission_callback" => function () {
            return current_user_can( 'read' );
        }
    ));
});

POSTING fails because the New NONCE is identical to the old.

So I thought WordPress must return a new nonce in the response header… I check and see the "x-wp-nonce" header – only it is also identical!

(I read something about not using wp_json_success in REST routes – as the rest api already turns the return of your function into a json response and sets the correct headers etc.)

When I hard refresh the page, I get a new nonce and my AJAX now works…

How can I force a new nonce to be returned, so that AJAX works without refreshing the page?

This post is kinda similar – Serving nonces through AJAX is not refreshing nonce, returning 403 error, but I can’t see anything in my code which is changing the user and invalidating the nonce…

Unless, the log in route – now logged in – changes the nonce, which doesn’t get returned?


Edit

I’ve refactored using Sally’s answer. All works (although I had to clear my browser cache as some weird stuff was happening).

    // https://wordpress.stackexchange.com/questions/377570/identical-wp-rest-nonce-returned-from-rest-api

/* Ajax REST cookie stuff */
// https://remonpel.nl/2018/06/wordpress-rest-api-nonce-sense/

add_action('set_logged_in_cookie', function($cookie_value){
    $_COOKIE[ LOGGED_IN_COOKIE ] = $cookie_value;
}, PHP_INT_MAX);

// on log-ou, clear the global $_COOKIE the same way wp_logout() does.
// todo: see if more cookies need this.
add_action('clear_auth_cookie', function(){
    $_COOKIE[ LOGGED_IN_COOKIE ] = ' ';
});

/* end Ajax REST cookie stuff */


function ajax_logout() {
    wp_logout();
    wp_set_current_user(0);
    return [
        'loggedin'  =>  false,
        'message'   =>  __('Logged out successfully'),
        'name'      =>  false,
        'email'     =>  false,
    ];
}


function ajax_login($request){
    $user = wp_signon( array(
        'user_login'    => $request->get_param( 'username' ),
        'user_password' => $request->get_param( 'password' ),
        'remember'      => true,
    ));
      // In case of errors like wrong password, return the error object/data.
if ( is_wp_error( $user ) ) {
    $data = [
        'failed' => $user,
        'nonce'    =>   wp_create_nonce( 'wp_rest' ),
    ];
    return new WP_REST_Response($data, 200); // important if you want to display data client side
}
    wp_set_current_user( $user->ID ); // super important
    return [
        'loggedin' =>   is_user_logged_in(),
        'message'  =>   __('Login successful'), 
        'name'     =>   $user->data->display_name, 
        'email'    =>   $user->data->user_email,
        'nonce'    =>   wp_create_nonce( 'wp_rest' ),
    ];
}



function read_permissions_check() {
    // Restrict endpoint to only users who have the edit_posts capability.
    // This can be extended or whatever
    if ( !current_user_can( 'read' ) ) {
        return new WP_Error( 'rest_forbidden', esc_html__( 'You shall not pass!', 'my-text-domain' ), array( 'status' => 401 ) );
    }
    return true;
}


add_action( 'rest_api_init', function () {
    register_rest_route( 'rw-user/v1', '/log-in', array(
        'methods' => 'POST',
        'callback' => 'ajax_login',
        'permission_callback' => '__return_true',
    ));
    register_rest_route('rw-user/v1','/log-out', array(
        'methods' => 'POST',
        "callback" => 'ajax_logout',
        'permission_callback' => 'read_permissions_check',
    ));
});

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

Despite you already figured it out or found a solution, since this is a follow-up to the other question, I thought it’d be benefecial to post this answer, so hopefully it helps you. =)

WordPress uses the native setcookie() function in PHP for setting the authentication cookies, and $_COOKIE for reading the cookies values. But the thing is, PHP doesn’t immediately update the values in the $_COOKIE array and instead, it’ll only be updated once the page is reloaded.

So because WordPress nonces rely upon the current user and the authentication cookies, you’ll need to do these in order to get the correct nonce on the very same page/request โ€” i.e. without page reloads:

  1. Immediately update the $_COOKIE array once you’ve called wp_signon() (which calls wp_set_auth_cookie()). And you can do that using the set_logged_in_cookie hook.

  2. And then call wp_set_current_user() after logging in the user โ€” if you don’t call it, the generated nonce will not use the current user and functions like is_user_logged_in() and current_user_can() would also return false.

Working Example

function wpse_377570( $logged_in_cookie ) {
    $_COOKIE[ LOGGED_IN_COOKIE ] = $logged_in_cookie;
}
add_action( 'set_logged_in_cookie', 'wpse_377570' );

function ajax_login( $request ) {
    // Logs the user in and set the authentication cookies.
    $user = wp_signon( array(
        'user_login'    => $request->get_param( 'username' ),
        'user_password' => $request->get_param( 'password' ),
        'remember'      => true,
    ) );

    // In case of errors like wrong password, return the error object/data.
    if ( is_wp_error( $user ) ) {
        return $user;
    }

    // Now set the current user so that we get the correct nonce based on
    // that user.
    wp_set_current_user( $user->ID );

    return [
        'nonce'    => wp_create_nonce( 'wp_rest' ),
        'loggedin' => is_user_logged_in(),
        // ... your other data.
    ];
}

Additional Notes

  • REST API endpoint’s callback always receives the request object (a WP_REST_Request instance) as the first parameter, which among other functions, has get_param() for retrieving a parameter like a URL query string, a POST body data or a JSON payload, so you should make use of that and the other functions, just as I did in my example. ๐Ÿ™‚

Note: Use and implement method 1 because this method fully tested our system.
Thank you ๐Ÿ™‚

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply