1

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',
    ));
});
2
  • Found this blog post remonpel.nl/2018/06/wordpress-rest-api-nonce-sense - this looks very much like the problem I am having. My PHP isn't solid enough to understand all of the code, but I get the gist of it. Will try this now :D – Rob Nov 3 '20 at 0:29
  • The blog post appears to have worked :D – Rob Nov 3 '20 at 2:50
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. :)
1
  • 1
    so I don't need to look at $POST - I can grab the data from an already parsed request object... I have an hour before the day job. Time for a quick refactor :D Thanks! – Rob Nov 3 '20 at 8:07

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.