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',
));
});