Front-end user registration and login in WordPress

Handling logins on the front end is not something you should take lightly. It’s an hackish solution, we have to override some of the WordPress native functions and the API may change at any time.

A complete solution should include:

  • New user registration
  • Existing user log in
  • Lost password recovery
  • User profile page
  • Password change
  • Log out

The goal of this project is:

  • Hook to native functions as much as possible
  • When you can’t use native functions, hook to WP native events/actions at the very last moment
  • Let WP do all the validation
  • Keep the custom code small and simple
  • Let admins access the native back-end

Please note that this article is meant for developers who want to understand how WordPress works (in regards of user account management), it’s not a drop-in code you can copy/paste on your site. I suggest to novice WP users to find some ready to use plugins, but if you like to know what happens under the hood, this is probably a nice place to start.

The full code can be found on github.

Prerequisites

The first thing you need to do is to create a login page and possibly a user page (they can actually be the same, but let’s create the two of them for the sake of clearness).

Personally I created a /login and a /user pages, I’ve also added a page-login.php and a page-user.php template files in the theme directory where all the HTML code resides.

In the page-login.php file we are going to add four forms: registration, login, password recovery, password reset.

For the login form we can use the native wp_login_form function.

<h1>Login</h1>

<?php wp_login_form(); ?>

The registration form needs a little more attention. As far as I know there’s no native function we can use, so we have to create it from scratch:

<h1>Registration</h1>

<form name="registerform" action="<?php echo site_url('wp-login.php?action=register', 'login_post') ?>" method="post">
    <p>
        <label for="user_login">Username</label>
        <input type="text" name="user_login" value="">
    </p>
    <p>
        <label for="user_email">E-mail</label>
        <input type="text" name="user_email" id="user_email" value="">
    </p>
    <p style="display:none">
        <label for="confirm_email">Please leave this field empty</label>
        <input type="text" name="confirm_email" id="confirm_email" value="">
    </p>

    <p id="reg_passmail">A password will be e-mailed to you.</p>

    <input type="hidden" name="redirect_to" value="/login/?action=register&success=1" />
    <p class="submit"><input type="submit" name="wp-submit" id="wp-submit" value="Register" />></p>
</form>

Since we use one page for registration and login I set the redirect_to parameter to /login/?action=register&success=1. We can later check on $_GET['action'] to know which action we are currently working on. Feel free to create one page per action if you fancy.

You’ll also notice a confirm_email field that is actually hidden inside a display:none element. That’s just a honey pot for spam bots. It helps mitigate fake logins (but it does not eliminate the problem, further actions –not covered in this article– have to be taken).

Then the password recovery. We don’t have a native helper, so we have to create our own.

<h1>Password recovery</h1>

<form name="lostpasswordform" action="<?php echo site_url('wp-login.php?action=lostpassword', 'login_post') ?>" method="post">
    <p>
        <label for="user_login">Username or E-mail:</label>
        <input type="text" name="user_login" id="user_login" value="">
    </p>

    <input type="hidden" name="redirect_to" value="/login/?action=forgot&success=1">
    <p class="submit"><input type="submit" name="wp-submit" id="wp-submit" value="Get New Password" /></p>
</form>

Lastly the password reset, where the user can actually change the forgotten password.

<h1 class="entry-title">Reset password</h1>

<form name="resetpasswordform" action="<?php echo site_url('wp-login.php?action=resetpass', 'login_post') ?>" method="post">
    <p class="form-password">
        <label for="pass1">New Password</label>
        <input class="text-input" name="pass1" type="password" id="pass1">
    </p>
    <p class="form-password">
        <label for="pass2">Confirm Password</label>
        <input class="text-input" name="pass2" type="password" id="pass2">
    </p>

    <input type="hidden" name="redirect_to" value="/login/?action=resetpass&success=1">
    <p class="submit"><input type="submit" name="wp-submit" id="wp-submit" value="Get New Password" /></p>
</form>

Put all the four forms in the page-login.php template, you may want to create tabs for them, or selectively display them based on the action parameter. Up to you.

Okay, now to the fun part.

There’s not much PHP code involved but what we have is very sensible. I personally put everything into a login.php file that is required from theme functions.php, but probably a custom plugin would be a smarter choice.

Redirect wp-admin calls to the front-end

Every action to the user session or account passes through to the wp-login.php page. We have to check what action is actually performed and redirect the user accordingly. This is done by attaching a custom function to the login_init WP hook.

function cubiq_login_init () {
    $action = isset($_REQUEST['action']) ? $_REQUEST['action'] : 'login';

    if ( isset( $_POST['wp-submit'] ) ) {
        $action = 'post-data';
    } else if ( isset( $_GET['reauth'] ) ) {
        $action = 'reauth';
    } else if ( isset($_GET['key']) ) {
        $action = 'resetpass-key';
    }

    // redirect to change password form
    if ( $action == 'rp' || $action == 'resetpass' ) {
        wp_redirect( home_url('/login/?action=resetpass') );
        exit;
    }

    // redirect from wrong key when resetting password
    if ( $action == 'lostpassword' && isset($_GET['error']) && ( $_GET['error'] == 'expiredkey' || $_GET['error'] == 'invalidkey' ) ) {
        wp_redirect( home_url( '/login/?action=forgot&failed=wrongkey' ) );
        exit;
    }

    if (
        $action == 'post-data'        ||            // don't mess with POST requests
        $action == 'reauth'           ||            // need to reauthorize
        $action == 'resetpass-key'    ||            // password recovery
        $action == 'logout'                         // user is logging out
    ) {
        return;
    }

    wp_redirect( home_url( '/login/' ) );
    exit;
}
add_action('login_init', 'cubiq_login_init');

I first try to understand what login action is being done and based on that decide if we need to redirect to our login page or not.

All POST data is untouched and left to WordPress. Logout is also WP responsibility.

There are other two actions that have to be handled natively: one is the reset password code verification, the other is the so called reauth. As far as I understand reauth is performed when an admin tries to access the admin area after the session has expired.

Every other call is redirected to yoursite/login/.

Redirect to login or user profile page

If you try to access the /user page but you are not logged in you are redirected to /login insterad. Similarly, if you are logged in there’s no reason to show the /login page and you are headed to the profile page. This is easily done thanks to the template_redirect action.

function cubiq_template_redirect () {
    if ( is_page( 'login' ) && is_user_logged_in() ) {
        wp_redirect( home_url( '/user/' ) );
        exit();
    }

    if ( is_page( 'user' ) && !is_user_logged_in() ) {
        wp_redirect( home_url( '/login/' ) );
        exit();
    }
}
add_action( 'template_redirect', 'cubiq_template_redirect' );

Let administrators pass

Here you can choose the wp-admin access rules. In my case I let everyone in the backend except for subscribers (so admins and editors access the standard WordPress admin console).

function cubiq_admin_init () {
    if ( current_user_can( 'subscriber' ) && !defined( 'DOING_AJAX' ) ) {
        wp_redirect( home_url('/user/') );
        exit;
    }
}
add_action( 'admin_init', 'cubiq_admin_init' );

Remember to keep the DOING_AJAX check, all ajax requests are performed in the backend.

Check the registration form

The new user registration is left to WordPress, all we need to do is to check on errors so we can show a friendly message to the user in the front-end. Probably the best event we can hook to is registration_errors.

function cubiq_registration_redirect ($errors, $sanitized_user_login, $user_email) {

    // don't lose your time with spammers, redirect them to a success page
    if ( !isset($_POST['confirm_email']) || $_POST['confirm_email'] !== '' ) {

        wp_redirect( home_url('/login/') . '?action=register&success=1' );
        exit;

    }

    if ( !empty( $errors->errors) ) {
        if ( isset( $errors->errors['username_exists'] ) ) {

            wp_redirect( home_url('/login/') . '?action=register&failed=username_exists' );

        } else if ( isset( $errors->errors['email_exists'] ) ) {

            wp_redirect( home_url('/login/') . '?action=register&failed=email_exists' );

        } else if ( isset( $errors->errors['empty_username'] ) || isset( $errors->errors['empty_email'] ) ) {

            wp_redirect( home_url('/login/') . '?action=register&failed=empty' );

        } else if ( !empty( $errors->errors ) ) {

            wp_redirect( home_url('/login/') . '?action=register&failed=generic' );

        }

        exit;
    }

    return $errors;

}
add_filter('registration_errors', 'cubiq_registration_redirect', 10, 3);

First we check the honey pot (it must be empty), then we redirect to the login page with a failed value you can read from the $_GET variable. If no error occurred, the redirect_to url from the registration form is used. Congratulations, you are signed up!

Note that you can also add custom rules to user registration. For example you may want user names to contain only letters, or ban certain IP or domains.

Check the login form

This is a piece of cake. Hook to login_redirect and check for errors.

function cubiq_login_redirect ($redirect_to, $url, $user) {

    if ( !isset($user->errors) ) {
        return $redirect_to;
    }

    wp_redirect( home_url('/login/') . '?action=login&failed=1');
    exit;

}
add_filter('login_redirect', 'cubiq_login_redirect', 10, 3);

Again, if we find some errors in the login form, we redirect to the front-end with the failed variable set to true.

Check the reset password form

This time the rather undocumented lostpassword_post action is used.

function cubiq_reset_password () {
    $user_data = '';

    if ( !empty( $_POST['user_login'] ) ) {
        if ( strpos( $_POST['user_login'], '@' ) ) {
            $user_data = get_user_by( 'email', trim($_POST['user_login']) );
        } else {
            $user_data = get_user_by( 'login', trim($_POST['user_login']) );
        }
    }

    if ( empty($user_data) ) {
        wp_redirect( home_url('/login/') . '?action=forgot&failed=1' );
        exit;
    }
}
add_action( 'lostpassword_post', 'cubiq_reset_password');

The first few lines make a quick check on user login name or email. Don’t blame me for that code, it comes directly from WordPress source code. Checking an email by the @ character is something I haven’t seen since 2004, but I don’t want to mess with WP code so when I have to override native functions I try to replicate exactly what WP is doing.

Finally we have to validate the new password after you received the reset code.

function cubiq_validate_password_reset ($errors, $user) {
    // passwords don't match
    if ( $errors->get_error_code() ) {
        wp_redirect( home_url('/login/?action=resetpass&failed=nomatch') );
        exit;
    }

    // wp-login already checked if the password is valid, so no further check is needed
    if ( !empty( $_POST['pass1'] ) ) {
        reset_password($user, $_POST['pass1']);

        wp_redirect( home_url('/login/?action=resetpass&success=1') );
        exit;
    }

    // redirect to change password form
    wp_redirect( home_url('/login/?action=resetpass') );
    exit;
}
add_action('validate_password_reset', 'cubiq_validate_password_reset', 10, 2);

validate_password_reset is another poorly documented action, we need to hook to it to properly redirect the user to the front-end page in case of error in the password reset process.

User page and change password

Once the user is registered and logged in we show the profile page. For this purpose I created a /user/ page and a page-user.php template for it.

With get_currentuserinfo you can get… well, the current user info :) that you can display in the profile page.

Last thing left to do is to let the user change her password. WordPress by default creates a random password for new users so it is very likely that they want to change it.

I wasn’t able to find proper hooks for this purpose, so the best thing I could think of was to embed the needed PHP directly into the page-user.php page.

At the very top (before the header inclusion) of the page add the following:

global $current_user;
get_currentuserinfo();

require_once( ABSPATH . WPINC . '/registration.php' );

if ( !empty($_POST) && !empty( $_POST['action'] ) && $_POST['action'] == 'update-user' ) {

    /* Update user password */
    if ( !empty($_POST['current_pass']) && !empty($_POST['pass1'] ) && !empty( $_POST['pass2'] ) ) {

        if ( !wp_check_password( $_POST['current_pass'], $current_user->user_pass, $current_user->ID) ) {
            $error = 'Your current password does not match. Please retry.';
        } elseif ( $_POST['pass1'] != $_POST['pass2'] ) {
            $error = 'The passwords do not match. Please retry.';
        } elseif ( strlen($_POST['pass1']) < 4 ) {
            $error = 'Password too short';
        } elseif ( false !== strpos( wp_unslash($_POST['pass1']), "\\" ) ) {
            $error = 'Password may not contain the character "\\" (backslash).';
        } else {
            $error = wp_update_user( array( 'ID' => $current_user->ID, 'user_pass' => esc_attr( $_POST['pass1'] ) ) );

            if ( !is_int($error) ) {
                $error = 'An error occurred while updating your profile. Please retry.';
            } else {
                $error = false;
            }
        }

        if ( empty($error) ) {
            do_action('edit_user_profile_update', $current_user->ID);
            wp_redirect( site_url('/user/') . '?success=1' );
            exit;
        }
    }
}

After some data validation we just perform a edit_user_profile_update action and redirect to a success page.

In the page body instead we simply add the change password form:

<form method="post" action="/user/">
    <p>
        <label for="current_pass">Current Password</label>
        <input class="text-input" name="current_pass" type="password">
    </p>

    <p>
        <label for="pass1">New Password</label>
        <input class="text-input" name="pass1" type="password">
    </p>

    <p>
        <label for="pass2">Confirm Password</label>
        <input class="text-input" name="pass2" type="password">
    </p>

<?php

// extra fields
do_action('edit_user_profile', $current_user);

?>
    <p class="form-submit">
        <input name="updateuser" type="submit" value="Update profile">
        <input name="action" type="hidden" value="update-user">
    </p>
</form>

That’s it! You now completed your full user account management system.

Final words

As I said, this is not a ready to use solution, but I hope it’s enough to get you started.

I’m closing comments for this post but I set up a github repository with all the required code. If you find bugs or have suggestions I highly encourage you to keep the discussion rolling on github.

One last note on security: it is probably a good idea to add a wp_nonce_field field to all the forms. If there’s interest we can keep improving the code on github.

Thanks for watching.

Advertisements

2 comments on “Front-end user registration and login in WordPress

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s