thomas rudy

How To Build A WordPress AJAX Login and Register Without A Plugin

Getting a WordPress login system to work with a plugin is easy enough, but in some instances you may want to do it yourself. This is especially true with how WordPress plugins can often add considerable bloat.

Currently, Google is prioritizing speed and usability more than ever, with what is called Core Web Vitals. Because of that, you may want to implement you own version of a login system for WordPress.

Here is a simple setup you can use to have a login and register component for your WordPress website.

For the entire system to work, we will need to add 5 separate files to your WordPress theme.

  1. Login.php — Here we will register our hooks, and setup simple logic in order to login or register a user.
  2. Validation.php — This file, is a PHP trait and is setup to help out with simple validation. You may want to take extra precautions for increased security, although this tutorial will not cover security.
  3. User.js — This file handles the client side logic for submitting the form as well as some simple UI aspects to highlight to the user the results of their form submission.
  4. user-login.php — This is a partial template that we will include within our theme files, and will contain the actual form markup.
  5. _user_login.scss — The SCSS markup to help style the component.
  6. functions.php — This file is likely already in your theme, although we will need to add to it.

Build Your Login Code

Location: {THEME_DIRECTORY}/app/Ajax/Account/Login.php

This file handle the bulk of the logic for authenticating in WordPress as well as implement the Validation Trait below.

<?php

namespace App\Ajax\Account;

use App\Traits\Validation;

class Login {

  use Validation;

    /*
	*  $action_array
	*
	*  Array used to set proper ajax actions to be used by corresponding methods
    *
    *
	*
	*  @type	Array
	*
	*  @param	N/A
	*  @return	N/A
	*/
    private $action_array = [
        "user_login",
        "user_register"
    ];

    /*
	*  __construct
	*
	*
	*
	*  @type	function
	*
	*  @param	N/A
	*  @return	N/A
	*/
    public function __construct(){
        foreach ($this->action_array as $action){
            add_action("wp_ajax_$action", [$this, "ajax_$action"]);
            add_action("wp_ajax_nopriv_$action", [$this, "ajax_$action"]);
        }
    }

    /*
	*  ajax_user_register
	*
	*  Registers user, and then logs them in
	*
	*  @type	function
	*
	*  @param	N/A
	*  @return	JSON
	*/
    public function ajax_user_register(){
        $form_data = json_decode(stripslashes($_POST['form']),true);
        header('Content-Type: application/json');

        // quickly check to see if username is valid for wordpress
        // die early if it is not
        $username = $this->username_invalid($form_data['username'], 'username-register');
        if($username != false){
            echo $username;
            die();
        }


        $username = $this->email_invalid($form_data['email'], 'email-register');
        if($username != false){
            echo $username;
            die();
        }


        $username = $this->email_dns_check($form_data['email'], 'email-register');
        if($username != false){
            echo $username;
            die();
        }


        $username = $this->password_invalid($form_data['password'], 'password-register');
        if($username != false){
            echo $username;
            die();
        }

        if ($form_data['password'] != $form_data['confirm_password']) {
            echo json_encode([
                "status" => "fail",
                "reason" => "Passwords don't match",
                "input"  => "confirm_password-register",

            ]);
            die();
        }

        $action = $this->register_user($form_data);
        echo $action;
        die();
    }


    /*
	*  ajax_user_login
	*
	*  Login user and quick test to make sure username doesnt exist
	*
	*  @type	function
	*
	*  @param	N/A
	*  @return	JSON
	*/
    public function ajax_user_login(){
        $form_data = json_decode(stripslashes($_POST['form']),true);
        header('Content-Type: application/json');

        $username = username_exists($form_data['username']);
        if($username === false){
            echo json_encode([
                "status" => "fail",
                "input"  => "login-username",
                "reason" => "User doesn't exist"
            ]);
            die();
        }
        $action = $this->log_in_user( $form_data);
        echo $action;
        die();


    }


    private function log_in_user( $data){
        if(username_exists( $data['username'] ) || email_exists( $data['username'] )){
            // log the user in
            $credentials = array(
                'user_login'    => $data['username'],
                'user_password' => $data['password'],
                'remember'      => true
            );

            $user = get_user_by( 'login', $data['username'] );
            if ( $user && wp_check_password( $data['password'], $user->data->user_pass, $user->ID) ){

            } else {
                return json_encode([
                    "status" => "fail",
                    "reason" => "invalid password",
                    "input"  => "login-password",
                ]);
            }


            $user = wp_signon( $credentials, false );
            $userID = $user->ID;

            if($userID){
                wp_set_current_user( $userID, $data['username'] );
                wp_set_auth_cookie( $userID, true, true );
                return json_encode([
                    "status" => "success",
                    "reason" => "Success, logging you in",
                    "data"   => $data
                ]);
            } else {
                return json_encode([
                    "status" => "fail",
                    "reason" => "cant log in user",
                    "data"   => $user
                ]);
            }


        } else {

            return json_encode([
                "status" => "fail",
                "reason" => "user doesn't exist, please register",
                "data"   => $data
            ]);
        }
    }



    private function register_user( $form_data ){


        $user_data = array(
            'user_login'    => $form_data['username'],
            'user_email'    => $form_data['email'],
            'user_pass' => $form_data['password'],
        );

        $user = wp_insert_user( $user_data ) ;
        if($user){
            $credentials = array(
                'user_login'    => $form_data['username'],
                'user_password' => $form_data['password'],
                'remember'      => true
            );
            $user = wp_signon( $credentials, false );
            $userID = $user->ID;

            if($userID){
                wp_set_current_user( $userID, $form_data['username'] );
                wp_set_auth_cookie( $userID, true, false );
                return json_encode([
                    "status" => "success",
                    "reason" => "Success, logging you in",
                ]);
            } else {
                return json_encode([
                    "status" => "fail",
                    "reason" => "cant log in user",
                    "data"   => $user
                ]);
            }
        } else {
            return false;
        }
    }


}

Add Validation

Location: {THEME_DIRECTORY}/app/Traits/Validation.php

This file simply contains a PHP Trait to handle validation. It is imported into the Login.php code above.

<?php

namespace App\Traits;

/**
 *
 */
trait Validation
{

    /**
      *  username_invalid
      *
      *  Tests to see if username is an invalid use case
      *
      *  @type	function
      *
      *  @param	N/A
      *  @return	JSON | BOOLEAN
      */
    private function username_invalid($username, $input)
    {
        if ($username == '') {
            return json_encode([
              "status" => "fail",
              "input"  => $input,
              "reason" => "Username is blank"
          ]);
        } elseif (strlen($username) < 5) {
            return json_encode([
              "status" => "fail",
              "input"  => $input,
              "reason" => "Must be at least 5 characters"
          ]);
        } elseif (username_exists($username)) {
            return json_encode([
              "status" => "fail",
              "input"  => $input,
              "reason" => "Username not available"
          ]);
        } elseif (!preg_match('/^[a-zA-Z]+[a-zA-Z0-9._]+$/', $username)) {
            return json_encode([
              "status" => "fail",
              "input"  => $input,
              "reason" => "Number and letters only"
          ]);
        } else {
            return false;
        }
    }

    /**
    	*  Tests to see if username is an invalid use case
    	*  @type	function
    	*
    	*  @param	N/A
    	*  @return	JSON | BOOLEAN
    	*/
    private function password_invalid($password, $input)
    {
        if ($password == '') {
            return json_encode([
                "status" => "fail",
                "input"  => $input,
                "reason" => "Password is blank"
            ]);
        } elseif (strlen($password) < 8) {
            return json_encode([
                "status" => "fail",
                "input"  => $input,
                "reason" => "Must be at least 8 characters"
            ]);
        } elseif (!preg_match("#[0-9]+#", $password)) {
            return json_encode([
                "status" => "fail",
                "input"  => $input,
                "reason" => "Password needs at least one number"
            ]);
        } elseif (!preg_match("#[a-zA-Z]+#", $password)) {
            return json_encode([
                "status" => "fail",
                "input"  => $input,
                "reason" => "Password needs at least one letter"
            ]);
        } else {
            return false;
        }
    }

    /**
      *  Tests to see if email is an invalid
      *  @type	function
      *
      *  @param	  N/A
      *  @return	JSON | BOOLEAN
      */
    private function email_invalid($email, $input, $check_for_existing = true)
    {
        if ($email == '') {
            return json_encode([
              "status" => "fail",
              "input"  => $input,
              "reason" => "Email is blank"
          ]);
        } elseif (!is_email($email)) {
            return json_encode([
              "status" => "fail",
              "input"  => $input,
              "reason" => "Email is not valid"
          ]);
        } elseif ($check_for_existing && email_exists($email)) {
            return json_encode([
              "status" => "fail",
              "input"  => $input,
              "reason" => "Email in use"
          ]);
        } else {
            return false;
        }
    }


    /**
     *  Tests to see if email is an invalid
     *  @type	function
     *
     *  @param	  N/A
     *  @return	JSON | BOOLEAN
     */
    private function email_dns_check($email, $input){

            if (!filter_var(trim($_POST['email']), FILTER_VALIDATE_EMAIL)){

                if(checkdnsrr(array_pop(explode("@",$email)),"MX")){

                }else{
                    return json_encode([
                        "status" => "fail",
                        "input"  => $input,
                        "reason" => "Please don't do that. We know this is a fake email."
                    ]);
                }

            }else{

                return false;

            }
    }

}

Add The JavaScript Utilizing Ajax

Location: {THEME_DIRECTORY}/assets/scripts/User.js

You should note, I am using ES6. It’s not 100% necessary to do so, but it can certainly help with writing JavaScript in newer instances of WordPress. I often use the Sage WordPress theme for new WordPress projects which supports this right out of the box.

export default class User {

    constructor () {
        this.wp_object = window.wp_object;
        this.revealLogin = $('.js-login');
        this.revealRegister = $('.js-register');
        this.loginModal = $('#login-modal');
        this.loginForm = $('#login-form');
        this.loginFormMessage = $('#login-form-message');
        this.registerFormMessage = $('#register-form-message');
        this.registerModal = $('#register-modal');
        this.registerForm = $('#register-form');
        this.closeReveal = $('.js-close-login-modal');
        this.overlay = $('#utility-overlay');
        this.modal();
    }

    modal(){
        let self = this;
        self.revealLogin.on('click', function(e){
            self.registerModal.hide();
            self.loginModal.show();
            e.preventDefault();
        });

        self.revealRegister.on('click', function(e){
            self.loginModal.hide();
            self.registerModal.show();
            e.preventDefault();
        });

        self.revealLogin.on('click', function(e){
            self.registerModal.hide();
            self.loginModal.show();
            e.preventDefault();
        });

        self.revealRegister.on('click', function(e){
            self.loginModal.hide();
            self.registerModal.show();
            e.preventDefault();
        });

        self.closeReveal.on('click', function(e){
            self.loginModal.hide();
            self.registerModal.hide();
            $('.form-input').removeClass('fail');
            self.loginFormMessage.removeClass('fail success').html('');
            self.registerFormMessage.removeClass('fail success').html('');
            e.preventDefault();
        });

        self.loginForm.on('submit', function(e){
            let form = $(this).serializeObject();
            self.loginFormMessage.removeClass('fail success').html('');
            form = JSON.stringify(form);
            self.ajaxPromise(
                [self.loginSend(form)]
            );
            e.preventDefault();
        });

        self.registerForm.on('submit', function(e){
            let form = $(this).serializeObject();
            self.registerFormMessage.removeClass('fail success').html('');
            $('.form-input').removeClass('fail');
            form = JSON.stringify(form);
            console.log(form);
            self.ajaxPromise(
                [self.registerSend(form)]
            );
            e.preventDefault();
        });
    }

    loginSend(form){
        let self = this;
        return $.ajax({
            url: self.wp_object.ajaxurl,
            type: 'POST',
            data: {
                'action':'user_login',
                'form' : form,
            },
            success: function( response ) {
                if(response.status === 'fail'){
                    self.loginFormMessage.addClass('fail').html(response.reason);
                    $('.form-input').removeClass('fail');
                    $('.'+response.input).addClass('fail').focus();
                } else if (response.status === 'success'){
                    $('.success-hide').hide();
                    $('.modal-close').hide();
                    self.loginFormMessage.addClass('success').html(response.reason);
                    window.location = window.location.href;
                }
            },
        });
    }

    registerSend(form){
        let self = this;
        return $.ajax({
            url: self.wp_object.ajaxurl,
            type: 'POST',
            data: {
                'action':'user_register',
                'form' : form,
            },
            success: function( response ) {
                if(response.status === 'fail') {
                    self.registerFormMessage.addClass('fail').html(response.reason);
                    $('.'+response.input).addClass('fail').focus();
                } else if (response.status === 'success'){
                    $('.success-hide').hide();
                    $('.modal-close').hide();
                    self.registerFormMessage.addClass('success').html(response.reason);
                    window.location = window.location.href;
                }
            },
        });
    }


    ajaxPromise(arr, uiMethod = null, el = null){
        var self = this;
        self.overlay.show();
        if(uiMethod !== null) {
            this[uiMethod](el);
        }
        $.when.apply($,arr)
            .done(function() {
                    self.overlay.hide();
                }
            ).fail(function(){
                self.overlay.hide();
                alert('Something went wrong...');
            }
        );
    }

}

Add The User Login Partial

Location: {THEME_DIRECTORY}/partials/user-login.php

This file simply is the markup for the form that will be rendered on your page. You will need to include it where you would like the form to be present. Note, I am using Font Awesome here for the close icon.

<div id="login-modal" class="login-modal">
    <div class="login-modal-inner">
        <div class="modal-close js-close-login-modal">close <i class="fal-js fal fa-times" aria-hidden="true"></i></div>
        <h3 class="success-hide">Login</h3>
        <form id="login-form">
            <span id="login-form-message"></span>
            <label class="success-hide" for="login-username">Username</label>
            <input class="success-hide login-username form-input" id="login-username" type="text" name="username" tabindex="2"/>
            <label tabindex="1" class="success-hide" id="for-password"><span>Password</span> <span class="right forgot-password"><a href="{{$site_url}}/wp-login.php?action=lostpassword">Forgot your password?</a></span></label>
            <input class="success-hide login-password form-input" id="login-password" type="password" name="password" tabindex="3">
            <input type="submit" name="submit" class="button wide success-hide" value="Log In"/>
        </form>
        <div class="register-text success-hide">
            <h4>New to Your Website?</h4>
        </div>
        <div class="register success-hide">
            <a class="button js-register">Register</a>
        </div>
    </div>
</div>
<div id="register-modal" class="login-modal">
    <div class="login-modal-inner">
        <div class="modal-close js-close-login-modal">close <i class="fa fa-times" aria-hidden="true"></i></div>
        <h3 class="success-hide">Register</h3>
        <form id="register-form">
            <span id="register-form-message"></span>
            <label class="success-hide"for="register-username">Username</label>
            <input class="success-hide username-register form-input" id="register-username" type="text" name="username"/>
            <label class="success-hide"for="register-email">Email</label>
            <input class="success-hide email-register form-input" id="register-email" type="text" name="email"/>
            <label class="success-hide"for="register-password"><span>Password</span> <span class="right forgot-password"></span></label>
            <input class="success-hide password-register form-input" id="register-password" type="password" name="password">
            <label class="success-hide"for="register-password-confirm"><span>Confirm Password</span> <span class="right forgot-password"></span></label>
            <input class="success-hide confirm_password-register form-input" id="register-password-confirm" type="password" name="confirm_password">
            <input type="submit" name="submit" class="button wide success-hide" value="Register"/>
        </form>
        <div class="register-text success-hide">
            <h4>Already have an account?</h4>
        </div>
        <div class="register success-hide">
            <a class="button js-login">Login</a>
        </div>
    </div>
</div>

<div id="utility-overlay">
    <div id="utility-overlay-inner">
        <div id="loading">
            <div class="load-3">
                <div class="line"></div>
                <div class="line"></div>
                <div class="line"></div>
            </div>
        </div>
    </div>
</div>

Add Styling To The Component

Location: {THEME_DIRECTORY}/assets/styles/user_login.scss

I am using SCSS for styling in this example. You do not need to, although it can be advantageous. Although in my example below, there are certain variables and mix-ins you would have to recreate, or just simply omit them.

.login-modal{
  position: fixed;
  background: rgba($black,.3);
  top:0;
  left:0;
  height:100%;
  width:100%;
  z-index: 999;
  display: none;

  @include breakpoint(medium down){
    background: rgba($black,.4);
  }


  &-inner{
    position: relative;
    @include grid-column(3,0);
    max-width: 360px;
    min-width: 360px;

    @include breakpoint(medium down){
      @include grid-column(6,0);

    }

    @include breakpoint(small down){
      @include grid-column(12,0);

    }
    margin: 0 auto;
    z-index: 99999;
    top: 50%;
    transform: translateY(-50%);
    float: none !important;
    padding: rem-calc(20) rem-calc(40) !important;
    border: 1px solid rgba(10, 10, 10, 0.1);
    box-shadow: 10px 9px 8px 0 rgba(10, 10, 10, 0.05);
    background: $white;

    @include breakpoint(medium down){
      top: rem-calc(25);
      transform: translateY(0%);
    }

    h3{
      text-align: center;
      font-size: rem-calc(18);
      letter-spacing: .1em;
      font-family: $header-font-family;
    }
    label{
    }
    input:not([type=button]):not([type=reset]):not([type=submit]):not([type=image]) {
      height:rem-calc(38);
      font-size: rem-calc(17);
      padding-left: rem-calc(15);
      margin-bottom: rem-calc(8);

    }
    .wide{
      @include button-style($primary-color, $primary-color, $white);
      width: 100%;
      margin-top: rem-calc(10);
      &:hover{
        transform: translateY(0px);
        box-shadow: 0 7px 14px rgba($black,.2), 0 3px 6px rgba(0,0,0,.1);
      }
    }
  }
}


.modal-close{
  position: absolute;
  top:5px;
  right:8px;
  cursor: pointer;
  color: $black;
  font-size: rem-calc(12);

  i{
    font-size: rem-calc(12);
  }
}

.register-text{
  text-align: center;
  font-style: italic;
  position: relative;
  top: 2px;
  padding-top: 1px;
  margin-top: rem-calc(4);
  margin-bottom: rem-calc(10);
  line-height: 0;
  h4{
    line-height: 1;
    z-index: 2;
    background-color: #fff;
    padding: 0 8px 0 7px;
    font-size: 14px;
    position: relative;
    display: inline-block;
  }
  &:after{
    content: " ";
    width: 100%;
    background-color: $dark-gray;
    display: block;
    height: 1px;
    position: absolute;
    top: 50%;
    margin-top: -3px;
    z-index: 1;
  }
}

.register{
  .button{
    background: linear-gradient(-180deg, #eee 0, #eee 100%);
    color: $body-font-color !important;
    width: 100%;
    &:hover{
      transform: translateY(0px);
      box-shadow: 0 7px 14px rgba(50,50,93,.2), 0 3px 6px rgba(0,0,0,.1);
    }
  }
}

.forgot-password{
  float: right;
  font-style: italic;
  a{
    font-size: rem-calc(12);
  }
}

#login-form-message, #register-form-message{
  &.fail{
    //background: rgba($alert-color,.5);
    //border: 2px solid $alert-color;
    color: $alert-color;
    font-family: $header-font-family;
    font-weight: bold;
    padding: rem-calc(5) rem-calc(10);
    display: block;
    position: relative;
    font-size: rem-calc(18);
    &:before{
      font-family: "Font Awesome 5 Duotone";
      font-weight: 900;
      position: absolute;
      content: "\F321";
      left: -7px;
      font-size: 26px;
      top: -1px;
    }
  }
  &.success{
    color: $success-color;
    font-family: $header-font-family;
    font-weight: bold;
    padding: rem-calc(5) rem-calc(10) rem-calc(5) rem-calc(27);
    display: block;
    position: relative;
    font-size: rem-calc(23);
    &:before{
      font-family: "Font Awesome 5 Duotone";
      font-weight: 900;
      position: absolute;
      content: "\f058";
      left:0;
      top:rem-calc(6);
    }
  }
}

.form-input{
  &.fail{
    border-color: $alert-color;
  }
}

Add To The Functions File

Location: {THEME_DIRECTORY}/functions.php

It’s possible to register this action in a more elegant way, but this will get the job done. What this does is allow us to have a number of global variables we may want to use later on. For instance the ajaxurl that is used in the User.js file.

add_action ( 'wp_footer', 'my_js_variables' );
function my_js_variables(){

    global $post;
    if ( current_user_can( 'manage_options' ) ) {
        $admin = 1;
    } else {
        $admin = 0;
    }

    ?>
    <script type="text/javascript">

        var wp_object =

        <?php

        echo json_encode(
            array(
                'ajaxurl' => admin_url( 'admin-ajax.php' ),
                'post_id' => $post->ID,
                'post_permalink' => get_permalink($post->ID),
                'user_logged_in' => is_user_logged_in(),
                'ajax_nonce' => wp_create_nonce('the_nonce'),
                'can_edit'  => $admin,
                'avatar' => get_avatar( get_current_user_id() ),
            )
        );
        ?>
    </script>

    <?php
}

There are certainly ways to improve this code, or have expand it to work with third party APIs like Facebook or Linkedin. However, this code is functional and will give you a light footprint WordPress AJAX login system.

Let me know what you think in the comments.

Leave a Reply

Your email address will not be published. Required fields are marked *