How to add a lost password form and password reset form to a WordPress page

How to display a WordPress lost password form and password reset form on any page using a custom shortcode, enhancing both forms to submit via ajax to improve the user experience.

Create a lost password form

The lost password form is the first step in the password rest process and allows users to request a password reset email by entering there username or email address, the password reset email then sends a link that will direct the user to the password reset form.

The following code uses the core wordpress lost password form as a base, only adding “jcul-ajax-error-message” container to display the forms response, and “jcul-ajax-enabled” hidden input to trigger our custom ajax response.

function jcul_display_lost_password_form()
{
    /**
     * Filters the URL redirected to after submitting the lostpassword/retrievepassword form.
     *
     * @since 3.0.0
     *
     * @param string $lostpassword_redirect The redirect destination URL.
     */
    $redirect_to = apply_filters('lostpassword_redirect', '');

    $user_login = '';

    if (isset($_POST['user_login']) && is_string($_POST['user_login'])) {
        $user_login = wp_unslash($_POST['user_login']);
    }

    ob_start();
?>
    <form name="lostpasswordform" id="lostpasswordform" action="<?php echo esc_url(network_site_url('wp-login.php?action=lostpassword', 'login_post')); ?>" method="post">
        <div class="jcul-ajax-error-message"></div>

        <p>
            <label for="user_login"><?php _e('Username or Email Address'); ?></label>
            <input type="text" name="user_login" id="user_login" class="input" value="<?php echo esc_attr($user_login); ?>" size="20" autocapitalize="off" />
        </p>
        <?php

        /**
         * Fires inside the lostpassword form tags, before the hidden fields.
         *
         * @since 2.1.0
         */
        do_action('lostpassword_form');

        ?>
        <input type="hidden" name="redirect_to" value="<?php echo esc_attr($redirect_to); ?>" />
        <input type="hidden" name="jcul-ajax-enabled" value="1" />
        <p class="submit">
            <input type="submit" name="wp-submit" id="wp-submit" class="button button-primary button-large" value="<?php esc_attr_e('Get New Password'); ?>" />
        </p>
    </form>
<?php
    return ob_get_clean();
}

Create a password reset form

The password reset form is used in the password reset and user registration process as the form allows the user to set there own or generate a new password by using the password reset key and username to authenticate the request.

The display reset password function extends the core wordpress reset password form adding “jcul-ajax-error-message” container to display the forms response, and “jcul-ajax-enabled” hidden input to trigger our custom ajax response.

function jcul_display_reset_password_form()
{
    wp_enqueue_script('utils');
    wp_enqueue_script('user-profile');

    $rp_login = null;
    $rp_key = null;

    $rp_cookie       = 'wp-resetpass-' . COOKIEHASH;
    if (isset($_COOKIE[$rp_cookie]) && 0 < strpos($_COOKIE[$rp_cookie], ':')) {
        list($rp_login, $rp_key) = explode(':', wp_unslash($_COOKIE[$rp_cookie]), 2);

        $user = check_password_reset_key($rp_key, $rp_login);

        if (isset($_POST['pass1']) && !hash_equals($rp_key, $_POST['rp_key'])) {
            $user = false;
        }
    } else {
        $user = false;
    }

    ob_start();
?>
    <form name="resetpassform" id="resetpassform" action="<?php echo esc_url(network_site_url('wp-login.php?action=resetpass', 'login_post')); ?>" method="post" autocomplete="off">
        <input type="hidden" id="user_login" value="<?php echo esc_attr($rp_login); ?>" autocomplete="off" />

        <div class="jcul-ajax-error-message"></div>

        <div class="user-pass1-wrap">
            <p>
                <label for="pass1"><?php _e('New password'); ?></label>
            </p>

            <div class="wp-pwd">
                <input type="password" data-reveal="1" data-pw="<?php echo esc_attr(wp_generate_password(16)); ?>" name="pass1" id="pass1" class="input password-input" size="24" value="" autocomplete="off" aria-describedby="pass-strength-result" />

                <button type="button" class="button button-secondary wp-hide-pw hide-if-no-js" data-toggle="0" aria-label="<?php esc_attr_e('Hide password'); ?>">
                    <span class="dashicons dashicons-hidden" aria-hidden="true"></span>
                </button>
                <div id="pass-strength-result" class="hide-if-no-js" aria-live="polite"><?php _e('Strength indicator'); ?></div>
            </div>
            <div class="pw-weak">
                <input type="checkbox" name="pw_weak" id="pw-weak" class="pw-checkbox" />
                <label for="pw-weak"><?php _e('Confirm use of weak password'); ?></label>
            </div>
        </div>

        <p class="user-pass2-wrap">
            <label for="pass2"><?php _e('Confirm new password'); ?></label>
            <input type="password" name="pass2" id="pass2" class="input" size="20" value="" autocomplete="off" />
        </p>

        <p class="description indicator-hint"><?php echo wp_get_password_hint(); ?></p>
        <br class="clear" />

        <?php

        /**
         * Fires following the 'Strength indicator' meter in the user password reset form.
         *
         * @since 3.9.0
         *
         * @param WP_User $user User object of the user whose password is being reset.
         */
        do_action('resetpass_form', $user);

        ?>
        <input type="hidden" name="rp_key" value="<?php echo esc_attr($rp_key); ?>" />
        <input type="hidden" name="jcul-ajax-enabled" value="1" />
        <p class="submit reset-pass-submit">
            <button type="button" class="button wp-generate-pw hide-if-no-js" aria-expanded="true"><?php _e('Generate Password'); ?></button>
            <input type="submit" name="wp-submit" id="wp-submit" class="button button-primary button-large" value="<?php esc_attr_e('Save Password'); ?>" />
        </p>
    </form>
<?php
    return ob_get_clean();
}

Create a lost password and password reset shortcode

The lost password form shortcode will display the reset password form if it receives a reset password request which is identified via action get variable equals ‘rp’, otherwise the lost password form is displayed.

function jcul_lost_password_shortcode($atts)
{
    if (isset($_GET['action']) && $_GET['action'] === 'rp') {
        return jcul_display_reset_password_form();
    }
    return jcul_display_lost_password_form();
}

add_shortcode('lost_password_form', 'jcul_lost_password_shortcode');

Add lost password form to a page

Displaying the lost password / password reset form on any page can be added by pasting the shortcode into the classic editor WYSIWYG, or by adding a shortcode block into the gutenberg editor and pasting it there.

[lost_password_form]

Adding the lost password / password reset form to any other part of the website can be achieved by using the do_shortcode function in php.

do_shortcode('[lost_password_form]');

with the lost password / password rest form shortcode added to a page, we need to keep track of the page id by defining as constant in php:

define('JCUL_LOST_PASSWORD_ID', 13);

With the lost password form id set, we can use this to capture the request before displaying the form and set a cookie to be used by the password reset form.

function jcul_set_resetpass_cookie() {

    if (is_admin() || !is_page(JCUL_LOST_PASSWORD_ID) || !isset($_GET['action'], $_GET['key'], $_GET['login']) || $_GET['action'] !== 'rp') {
        return;
    }

    list($rp_path_raw) = explode('?', wp_unslash($_SERVER['REQUEST_URI']));
    $rp_cookie       = 'wp-resetpass-' . COOKIEHASH;

    if (isset($_GET['key']) && isset($_GET['login'])) {
        $value = sprintf('%s:%s', wp_unslash($_GET['login']), wp_unslash($_GET['key']));
        setcookie($rp_cookie, $value, 0, "/", COOKIE_DOMAIN, is_ssl(), true);

        wp_safe_redirect(remove_query_arg(array('key', 'login')));
        exit;
    }
}

add_action('wp', 'jcul_set_resetpass_cookie');

Next we need to change the default wordpress password reset url to our new page by using the network_site_url and site_url filters:

function jcul_alter_password_reset_link($url)
{
    if (strpos($url, 'wp-login.php?action=rp') !== false) {
        $url = preg_replace('/(.*?)wp\-login\.php/', get_permalink(JCUL_LOST_PASSWORD_ID), $url);
    }
    return $url;
}

add_filter('network_site_url', 'jcul_alter_password_reset_link');
add_filter('site_url', 'jcul_alter_password_reset_link');

Submit Lost password form via AJAX

Adding AJAX to the wordpress lost password form can be achieved by capturing the default form submission to wp-login.php and instead submitting it using jquery $.ajax method, displaying the response inline at the top of the form.

The button_text function stops the user submitting the registration form multiple time, by disabling the submit button and changing its text to alert the user while the form is processing.

(function($){

var button_text = function(el, text = 'Loading') {

  if (!el.data('text')) {
    el.data('text', el.val());
  }

  if (!text) {
    el.prop('disabled', false);
    el.val(el.data('text'));
  } else {
    el.prop('disabled', true);
    el.val(text);
  }
}

$('body').on('submit', '#lostpasswordform', function (e) {
  var $form = $(this);
  e.preventDefault();

  button_text($form.find('input[type="submit"]'));

  $.ajax({
    url: $form.attr('action'),
    type: 'POST',
    dataType: 'json',
    data: $form.serialize(),
    success: function (response) {
      if (response.status !== 'S') {
        $form
          .find('.jcul-ajax-error-message')
          .html('<p>An unknown error occured</p>');
        return;
      }

      if (response.data.lost_password !== 1) {
        $form
          .find('.jcul-ajax-error-message')
          .html('<p>' + response.data.errors.join('<br />') + '</p>');
        return;
      }

      $form
        .find('.jcul-ajax-error-message')
        .html('<p>A password reset link has been sent.</p>');
    },
    complete: function () {
      button_text($form.find('input[type="submit"]'), false);
    },
  });
});

})(jQuery);

Capture wordpress lost password form errors as an AJAX response

We use the lost_password hook to capture errors that have happened with the lost password form submission, checking first to see “jcul-ajax-enabled” flag has been sent, if so we output a json response with the lost_password flag set to 0 to record the failed attempt, and list the errors that occurred before ending the response.

function jcul_lost_password_json_error_response($errors){

    if (!isset($_POST['jcul-ajax-enabled'])) {
        return;
    }
    echo wp_json_encode([
        'status' => 'S',
        'data' => [
            'lost_password' => 0,
            'errors' => $errors->get_error_messages()
        ]
    ]);
    exit;
}

add_action('lost_password', 'jcul_lost_password_json_error_response');

Capture successful lost password form submissions as an AJAX response

Unlike capturing a failed lost password submission there was no hook or filter built for this task, in the end we use the wp_safe_redirect_fallback filter checking to see if “jcul-ajax-enabled” flag has been set along with the user_login post variable, and if so we can return a successful json response with the lost_password flag set to 1.

function jcul_lost_password_json_success_response($location){

    if (isset($_POST['jcul-ajax-enabled'], $_POST['user_login'], $_REQUEST['action']) && $_POST['jcul-ajax-enabled'] === "1" && $_REQUEST['action'] === 'lostpassword') {
        echo wp_json_encode([
            'status' => 'S',
            'data' => [
                'lost_password' => 1,
            ]
        ]);
        exit;
    }
    return $location;
}

add_filter('wp_safe_redirect_fallback', 'jcul_lost_password_json_success_response');

Submit Password Reset form via AJAX

Capturing our password reset form is done using jquerys $.ajax method where we capture the submit event targeting the #resetpassform element, preventing it submitting normally and instead send it via ajax, displaying any error s or success messages returned.

(function($){

var button_text = function(el, text = 'Loading') {

  if (!el.data('text')) {
    el.data('text', el.val());
  }

  if (!text) {
    el.prop('disabled', false);
    el.val(el.data('text'));
  } else {
    el.prop('disabled', true);
    el.val(text);
  }
}

$('body').on('submit', '#resetpassform', function (e) {
  var $form = $(this);
  e.preventDefault();

  $error_msg = $form.find('.jcul-ajax-error-message');
  $error_msg.html('');
  $error_msg.removeClass('jcul-ajax-error-message--error');
  $error_msg.removeClass('jcul-ajax-error-message--success');
  button_text($form.find('input[type="submit"]'));

  $.ajax({
    url: $form.attr('action'),
    type: 'POST',
    dataType: 'json',
    data: $form.serialize(),
    success: function (response) {
      if (response.status !== 'S') {
        $error_msg.addClass('jcul-ajax-error-message--error');
        $error_msg.html('<p>An unknown error occured</p>');
        return;
      }

      if (response.data.reset_password !== 1) {
        $error_msg.addClass('jcul-ajax-error-message--error');
        $error_msg.html('<p>' + response.data.errors.join('<br />') + '</p>');
        return;
      }

      $error_msg.addClass('jcul-ajax-error-message--success');
      $error_msg.html('<p>Your password has been reset.</p>');
    },
    complete: function () {
      button_text($form.find('input[type="submit"]'), false);
    },
  });
});

})(jQuery);

Capture wordpress password reset form errors as an AJAX response

the password reset form returns errors in two ways, the first using the login_form_resetpass hook to capture invalid or expired reset password keys, each returning a json response with the reset_password flag as 0 along with an error message.

function jcul_reset_password_json_key_error_response($url){

    if (isset($_POST['jcul-ajax-enabled'])) {
        if (strpos($url, 'wp-login.php?action=lostpassword&error=expiredkey')) {

            $error = new \WP_Error('EXPIRED_KEY', 'Expired Key');
            echo wp_json_encode([
                'status' => 'S',
                'data' => [
                    'reset_password' => 0,
                    'errors' => $error->get_error_messages()
                ]
            ]);
            exit;
        } elseif (strpos($url, 'wp-login.php?action=lostpassword&error=invalidkey')) {
            $error = new \WP_Error('INVALID_KEY', 'Invalid Key');
            echo wp_json_encode([
                'status' => 'S',
                'data' => [
                    'reset_password' => 0,
                    'errors' => $error->get_error_messages()
                ]
            ]);
            exit;
        }
    }

    return $url;
}

function jcul_reset_password_install_hook(){
    add_filter('wp_redirect', 'jcul_reset_password_json_key_error_response');
}

add_action('login_form_resetpass', 'jcul_reset_password_install_hook');

The next error messages we capture are the triggered by the validate_password_reset hook, where we also return a json response containing the reset_password flag set to 0 and the current error message.

function jcul_reset_password_json_error_response($errors){

    if (!isset($_POST['jcul-ajax-enabled'])) {
        return;
    }

    if ($errors->has_errors()) {
        echo wp_json_encode([
            'status' => 'S',
            'data' => [
                'reset_password' => 0,
                'errors' => $errors->get_error_messages()
            ]
        ]);
        exit;
    }
}

add_action('validate_password_reset', 'jcul_reset_password_json_error_response', PHP_INT_MAX);

Capture successful reset password form submissions as an AJAX response

Using the after_password_reset hook we can check that the jcul-ajax-enabled flag has been set, if so we capture the request, expire the reset pass cookie and output a success json response with reset_password flag set to 1.

function jcul_reset_password_json_success_response(){

    if (!isset($_POST['jcul-ajax-enabled'])) {
        return;
    }

    $rp_cookie       = 'wp-resetpass-' . COOKIEHASH;

    setcookie($rp_cookie, ' ', time() - YEAR_IN_SECONDS, "/", COOKIE_DOMAIN, is_ssl(), true);

    echo wp_json_encode([
        'status' => 'S',
        'data' => [
            'reset_password' => 1,
        ]
    ]);
    exit;
}

add_action('after_password_reset', 'jcul_reset_password_json_success_response');

Conclusion

In this article we covered how to display the lost password form and password reset form on any page of a wordpress website using a custom shortcode, how to intercept the normal form submission and instead submit if via ajax to allow the form submission to happen inline.

Next we covered how to hook into wordpress and return invalid or successful submissions and display the returned json response as a message on the form.

Leave a Reply

Fields marked with an * are required to post a comment