I am building a password change form using the WordPress REST API. The user enters a new password which is then submitted via AJAX to a custom endpoint which does this:
$userID = get_current_user_id();
wp_set_password($password, $userID);
//Log user in and update auth cookie
wp_set_current_user($userID);
wp_set_auth_cookie($userID);
//Set the cookie immediately
$_COOKIE[AUTH_COOKIE] = wp_generate_auth_cookie($userID, 2 * DAY_IN_SECONDS);
//Return fresh nonce
return new WP_Rest_Response(array(
'nonce' => wp_create_nonce('wp_rest')
));
The endpoint should automatically log the user in and return a new nonce so that they do not have to login again with their new password.
The problem is that the nonce returned is exactly the same and is invalid. I can only get the new nonce after a page refresh. It seems that some $_COOKIE
or $_SESSION
variables that WordPress relies on to generate nonces are not being updated until the page refresh.
Thanks for your help.
It seems that some $_COOKIE or $_SESSION variables that WordPress relies on to generate nonces are not being updated until the page refresh.
Yes, and it is LOGGED_IN_COOKIE
, which defaults to:
'wordpress_logged_in_' . COOKIEHASH
Take a look at:
$token = wp_get_session_token();
in wp_create_nonce()
$cookie = wp_parse_auth_cookie( '', 'logged_in' );
in wp_get_session_token()
$cookie_name = LOGGED_IN_COOKIE;
in wp_parse_auth_cookie()
So you'd replace this code: (and I wouldn't use wp_generate_auth_cookie()
for this purpose; and instead, use what's already been generated via wp_set_auth_cookie()
)
//Set the cookie immediately
$_COOKIE[AUTH_COOKIE] = wp_generate_auth_cookie($userID, 2 * DAY_IN_SECONDS);
..with this one:
$_set_cookies = true; // for the closures
// Set the (secure) auth cookie immediately. We need only the first and last
// arguments; hence I renamed the other three, namely `$a`, `$b`, and `$c`.
add_action( 'set_auth_cookie', function( $auth_cookie, $a, $b, $c, $scheme ) use( $_set_cookies ){
if ( $_set_cookies ) {
$_COOKIE[ 'secure_auth' === $scheme ? SECURE_AUTH_COOKIE : AUTH_COOKIE ] = $auth_cookie;
}
}, 10, 5 );
// Set the logged-in cookie immediately. `wp_create_nonce()` relies upon this
// cookie; hence, we must also set it.
add_action( 'set_logged_in_cookie', function( $logged_in_cookie ) use( $_set_cookies ){
if ( $_set_cookies ) {
$_COOKIE[ LOGGED_IN_COOKIE ] = $logged_in_cookie;
}
} );
// Set cookies.
wp_set_auth_cookie($userID);
$_set_cookies = false;
Working Example (tested on WordPress 4.9.5)
PHP / WP REST API
function myplugin__change_password( $password, $userID ) {
//$userID = get_current_user_id();
wp_set_password($password, $userID);
// Log user in.
wp_set_current_user($userID);
$_set_cookies = true; // for the closures
// Set the (secure) auth cookie immediately. We need only the first and last
// arguments; hence I renamed the other three, namely `$a`, `$b`, and `$c`.
add_action( 'set_auth_cookie', function( $auth_cookie, $a, $b, $c, $scheme ) use( $_set_cookies ){
if ( $_set_cookies ) {
$_COOKIE[ 'secure_auth' === $scheme ? SECURE_AUTH_COOKIE : AUTH_COOKIE ] = $auth_cookie;
}
}, 10, 5 );
// Set the logged-in cookie immediately. `wp_create_nonce()` relies upon this
// cookie; hence, we must also set it.
add_action( 'set_logged_in_cookie', function( $logged_in_cookie ) use( $_set_cookies ){
if ( $_set_cookies ) {
$_COOKIE[ LOGGED_IN_COOKIE ] = $logged_in_cookie;
}
} );
// Set cookies.
wp_set_auth_cookie($userID);
$_set_cookies = false;
//Return fresh nonce
return new WP_Rest_Response(array(
'nonce' => wp_create_nonce('wp_rest'),
'status' => 'password_changed',
));
}
function myplugin_change_password( WP_REST_Request $request ) {
$old_pwd = $request->get_param( 'old_pwd' );
$new_pwd = $request->get_param( 'new_pwd' );
$user = wp_get_current_user();
if ( ! wp_check_password( $old_pwd, $user->user_pass, $user->ID ) ) {
return new WP_Error( 'wrong_password', 'Old password incorrect' );
}
if ( $old_pwd !== $new_pwd ) {
return myplugin__change_password( $new_pwd, $user->ID );
}
return new WP_Rest_Response( [
'nonce' => wp_create_nonce( 'wp_rest' ),
'status' => 'passwords_equal',
], 200 );
}
add_action( 'rest_api_init', function(){
register_rest_route( 'myplugin/v1', '/change-password', [
'methods' => 'POST',
'callback' => 'myplugin_change_password',
'args' => [
'old_pwd' => [
'type' => 'string',
'validate_callback' => function( $param ) {
return ! empty( $param );
}
],
'new_pwd' => [
'type' => 'string',
'validate_callback' => function( $param ) {
return ! empty( $param );
}
],
],
'permission_callback' => function(){
// Only logged-in users.
return current_user_can( 'read' );
},
] );
} );
HTML / The form
<fieldset>
<legend>Change Password</legend>
<p>
To change your password, please enter your old or current password.
</p>
<label>
Old Password:
<input id="old_passwd">
</label>
<label>
New Password:
<input id="new_passwd">
</label>
<label>
Old nonce: (read-only)
<input id="old_nonce" value="<?= wp_create_nonce( 'wp_rest' ) ?>"
readonly disabled>
</label>
<label>
New nonce: (read-only)
<input id="new_nonce" readonly disabled>
</label>
<button id="go" onclick="change_passwd()">Change</button>
</fieldset>
<div id="rest-res"><!-- AJAX response goes here --></div>
jQuery / AJAX
function change_passwd() {
var apiurl = '/wp-json/myplugin/v1/change-password',
$ = jQuery;
$.post( apiurl, {
old_pwd: $( '#old_passwd' ).val(),
new_pwd: $( '#new_passwd' ).val(),
_wpnonce: $( '#new_nonce' ).val() || $( '#old_nonce' ).val()
}, function( res ){
$( '#new_nonce' ).val( res.nonce );
// Update the global nonce for scripts using the `wp-api` script.
if ( 'object' === typeof wpApiSettings ) {
wpApiSettings.nonce = res.nonce;
}
$( '#rest-res' ).html( '<b>Password changed successfully.</b>' );
}, 'json' ).fail( function( xhr ){
try {
var res = JSON.parse( xhr.responseText );
} catch ( err ) {
return;
}
if ( res.code ) {
$( '#rest-res' ).html( '<b>[ERROR]</b> ' + res.message );
}
});
}
You need to pass the nonce through the X-WP-Nonce
header with your AJAX request. Something like:
beforeSend: function ( xhr ) {
xhr.setRequestHeader( 'X-WP-Nonce', localizedScript.nonce );
}
Where localizedScript
is the script your nonce is localized to. See the codex for more info on localization. Note the localizedScript
method is NOT required.
Without a localizedSript
your AJAX could look similar to the following:
var _nonce = "<?php echo wp_create_nonce( 'wp_rest' ); ?>";
$.ajax({
type: 'POST',
url: url_path,
data: {},
dataType: 'json',
beforeSend: function ( xhr ) {
xhr.setRequestHeader( 'X-WP-Nonce', _nonce );
}
});
Obviously copying that exact script above wont get it done for you, but that's essentially how you need to pass the nonce successfully.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With