Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Changing Password via AJAX with the WordPress REST API

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.

like image 608
Xander Avatar asked Jul 31 '17 12:07

Xander


2 Answers

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 );
        }
    });
}
like image 85
Sally CJ Avatar answered Oct 10 '22 16:10

Sally CJ


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.

like image 42
Ty Bailey Avatar answered Oct 10 '22 15:10

Ty Bailey