Search code examples
phpwordpresscookieswordpress-rest-apinonce

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.


Solution

  • 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:

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