PHP Secure, Advanced Login System

PHP

Read {count} times since 2020

We all get new ideas every day. With the idea, we build more and more sites. Mostly, the sites would have a login system, right ? So, when we create new projects (sites) every time, we will have to create login systems for each one. I do the same thing and I’m tired of creating the same thing over and over again. So, I decided to create a login system that can be integrated to any PHP powered sites.

Latest Version

The latest version is 1.0.1 released on February 11, 2018. See changelog.

How To Upgrade

logSys can now be installed with composer :

composer require francium/logsys

NOTE – If you’re upgrading from versions less than 0.6, your users won’t be able to login with their password. They would have to reset it to a new one.

If you’re upgrading to 0.7 from 0.6, your users can login normally like before.

logSys Admin

logSys has an administrator app (Graphic Application) for managing users, statistics and more… See this post.

Features

I’m introducing you to the PHP class logSys. This class will help you to set up a login System on any PHP site. logSys have the following features :

  • Supports MySQLSQLite and PostgreSQL

    logSys uses PDO for database connection

  • Lightweight

    Almost no dependenices – logSys does not require additional dependencies

  • Login & Registering

  • “Remember Me”

  • 2 Step Verification

    SMS or E-Mail

  • Secure

    Uses password_hash() for hashing passwords

    Protection from Brute Force attacks by disabling login attempts for fixed time after 5 failed login attempts

    Device Management – Log Out from a device remotely

  • “Forgot Password ?”

  • Custom fields for storing users’ details

  • Easily get and update user details

  • Auto redirection based on the login status of user

  • Extra functions such as

    E-mail validation and random string generator

    Show time since user joined

Yes, it’s BIG ! The source code is more than 700 lines long and you customize it so much. Here’s a full guide on how to use logSys.

Requirements

  • PHP 5.5 or later

    If you want to use it in an older PHP version, get the password.php file from here and include it before loading LS.php file.

  • MySQL server or PostgreSQL server or SQLite

Download & Install

Use composer for installing logSys in your project.

composer require francium/logsys

Previously logSys could have been used with just a single class file. But as features got added, the dependencies also increased. Hence now it’s only possible to install logSys with composer.

Introduction

For this tutorial, we use the table as “users” (which is changable).

The user can either use E-Mail or username for logging in. An example database structure :

id | username | email | password | name | created | attempt

The columns “id”, “username” and “password” is absolutely required for the basic setup of logSys. Other columns are for optional features like “Login with E-Mail & Username”, “Time Since User Joined” and “Brute Force Protection”. You can also change the column names if you want.

You can easily set up the Database table by reading the “Database Table Setup” section of this tutorial.

Return values of logSys functions are booleans. So use === operator instead of ==.

In this post, we assume that you use “custom configuration” and not the other method to configure. Therefore there will be no including “LS.php” file, but “config.php”. Don’t know what I’m talking about ? Read this.

Make sure you read the next portion carefully and change the config values accordingly.

Configuration

You have to configure logSys first to continue. You have two options to change configurations. See about it here on the “configuration” section.

Here are some of the main options you can set in logSys :

basic

<td>
  Description
</td>

<td>
  Example
</td>
<td>
  Your site/company name. Used for including in E-Mails to be sent
</td>

<td>
  Subin&#8217;s Blog
</td>
<td>
  Used for "From" header in E-Mails sent
</td>

<td>
  [email protected]
</td>
Option
company
email

db

<td>
  Database to use : mysql or sqlite or postgresql
</td>

<td>
  mysql
</td>
<td>
  Absolute path to SQLite database file
</td>

<td>
  /home/simsu/mydb.sqlite
</td>
<td>
  The database Hostname.
</td>

<td>
  localhost
</td>
<td>
  The database Port
</td>

<td>
  3306
</td>
<td>
  The database username
</td>

<td>
  root
</td>
<td>
  The database password for the username
</td>

<td>
  mypassword
</td>
<td>
  The database name
</td>

<td>
  projects
</td>
<td>
  The database table to store users&#8217; data
</td>

<td>
  users
</td>
<td>
  The database table to store tokens used for "Forgot Password" etc.
</td>

<td>
  user_tokens
</td>
<td>
  Array of column names to be used
</td>

<td>
  See <a href="#columns">below</a>
</td>
type
sqlite_path
host
port
username
password
name
table
token_table
columns

columns

You can change the column names used by logSys. Just pass the alternative column name as value to the array :

array(
    "id" => "user_id",
    "username" => "my_column_username",
    "password" => "col_password",
    "email" => "user_email",
    "attempt" => "brute_force_check"
)

The above code will make logSys use the column “user_id” instead of “id” and others likewise.

keys

<td>
  A secure key that is used to encrypt cookie values
</td>

<td>
  sadauj3n(#ff)#%$
</td>
<td>
  The common password salt (site salt) that is used to hash all the users&#8217; passwords.<br /> This should not be changed after a user is created
</td>

<td>
  vdsjn3md%##$*5
</td>
cookie
salt

Make sure the secure keys for both hashing passwords and cookies are different and secure. Don’t let anyone know them.

If you change the salt value later, users won’t be able to login with their existing passwords.

If you change the cookie value later, all cookies will become invalid and user sessions will be terminated which will make them login again.

features

<td>
  Should the class call session_start();
</td>

<td>
  true (boolean)
</td>
<td>
  Should the class allows logging in with both E-Mail & Username ?
</td>

<td>
  true (boolean)
</td>
<td>
  If value is provided to Fr\LS::login(), should user be automatically logged in even after the end of PHP session.
</td>

<td>
  true (boolean)
</td>
<td>
  Should Fr\LS::init() be ran automatically on the first logSys function called in any page ?
</td>

<td>
  false (boolean)
</td>
<td>
  Prevent Brute Forcing.<br /> By enabling this, logSys will deny login for the time mentioned in the config value "brute_force"->"time_limit" seconds after config value "brute_force"->"tries" incorrect login tries.
</td>

<td>
  true
</td>
start_session
email_login
remember_me
auto_init
block_brute_force

brute_force

<td>
  Number of login tries alloted to users
</td>

<td>
  5
</td>
<td>
  The time IN SECONDS for which block from login action should be done after incorrect login attempts. Use <a href="http://www.easysurf.cc/utime.htm#m60s" target="_blank" rel="noopener">this tool</a> for converting minutes to seconds. Default : 5 minutes
</td>

<td>
  300
</td>
<td>
  Maximum number of tokens the user can request.
</td>

<td>
  5
</td>
tries
time_limit
max_tokens

pages

<td>
  An array containing the relative pathname of pages that doesn&#8217;t require logging in like the index page of site
</td>

<td>
  array("/")
</td>
<td>
  An array containing the relative pathname of pages that can be accessed by logged in and logged out users
</td>

<td>
  array()
</td>
<td>
  The relative pathname of login page
</td>

<td>
  /login.php
</td>
<td>
  The relative pathname of home page that the class should redirect to after logging in
</td>

<td>
  /home.php
</td>
no_login
everyone
login_page
home_page

It’s not necessary to include the login page and home page in “no_login” array, because logSys automatically adds it into that array.

cookies

<td>
  The expire time of the 2 cookies created during login process. This value is used for making the time using strtotime() function. So, the value must be compatible with <a href="http://php.net/manual/en/function.strtotime.php" target="_blank" rel="noopener">strtotime()</a>
</td>

<td>
  +30 days
</td>
<td>
  The `path` value of cookies.
</td>

<td>
  /
</td>
<td>
  The `domain` value of cookies. See <a href="http://php.net/manual/en/function.setcookie.php" target="_blank" rel="noopener">setcookie()</a>
</td>

<td>
  subinsb.com
</td>
expire
path
domain

two_step_login

<td>
  Message to show before displaying "Enter Token" form.
</td>

<td>
  A token was sent to your mobile number as SMS. Please enter it below :
</td>
<td>
  Callback that sends the token to user
</td>

<td>
  function($LS, $userID, $token){}
</td>
<td>
  Table in database where users&#8217; device information are stored
</td>

<td>
  user_devices
</td>
<td>
  The length of token that is generated
</td>

<td>
  4
</td>
<td>
  Maximum number of incorrect tries for Two Step Login token the user can do
</td>

<td>
  3
</td>
<td>
  Whether the token generated should be numeric only
</td>

<td>
  true
</td>
<td>
  The expire duration of the device cookie.<br /> This value is used for making the time using strtotime() function. So, the value must be compatible with <a href="http://php.net/manual/en/function.strtotime.php" target="_blank" rel="noopener">strtotime()</a>
</td>

<td>
  +45 days
</td>
<td>
  Should logSys checks if device is valid, everytime logSys is initiated ie everytime a page loads. If you want to check only the first time a user loads a page, then set the value to TRUE, else FALSE
</td>

<td>
  true
</td>
instruction
send_callback
devices_table
token_length
token_tries
numeric
expire
first_check_only

debug

You can make logSys write log files to debug.

<td>
  Should debugging be enabled
</td>

<td>
  false
</td>
<td>
  The absolute path to log file
</td>

<td>
  /
</td>
enable
log_file

Make sure the log file is situated outside the public directory of server. Messages are appended to log files and not overwritten.

You can find a sample configuration file here.

Database Table Setup

MySQL

You can find the SQL code to create the table users here.

logSys remembers the table name through “db”->“table”. By default it’s set to “users”. You can add extra columns according to your choice. After all, you can ask many info from users.

Note : As you can see in the `users` table mentioned above as SQL, the username column has a limit of “10” characters. So, you won’t be able to register users with usernames more than 10 chars. So, increase the limit to allow more characters in username.

For storing the reset password tokens, you need to create an extra table called “user_tokens”. This table name can be changed with config -> db -> tokens_table option.

You can find the SQL code for creating it here.

The reset password token saving table should be called “resetTokens”. It is not changeable. If you really want to change it, find the SQL queries in the class file that uses the “resetTokens” table and replace it.

PostgreSQL

You can find the SQL code here to create the 3 tables.

SQLite

The SQL code for creating the 3 tables is different for SQL****ite. You can find it here.

Timezone

Timezone is an important factor in any system that uses time in both database and server. In a login system it’s extensively used. So don’t screw it up.

I recommend using UTC as the timezone for all your applications. logSys by default doesn’t set a timezone. You should do it in your servers. See these pages to learn how to set it :

Initialize

Redirection based on the login status is needed for a login system. You should call the $LS->init(); at the start of every page on your site to redirect according to the login status. You can do this automatically without calling the function manually by setting config -> features -> auto_init to boolean TRUE.

Here is an example :

<?php
require "config.php";
$LS->init();
?>
<html>

and continue with your HTML code. When a user who is not logged in visits a page that is not in the config -> pages -> no_login array, the user gets redirected to the login page mentioned in config -> pages -> login_page.

If the user is logged in and is on a page mentioned in the config -> pages -> no_login array, he/she will be redirected to the config -> pages -> home_page URI.

All users who is logged in and not logged in can see the pages mentioned in config -> pages -> everyone. They won’t be redirected in those pages.

Login Page

Now, we’ll set up a login page. All you have to do is make a form and call the $LS->login() function with details at starting of the page. Example :

<html>
 <head>
  <title>Log In</title>
 </head>
 <body>
  <div class="content">
   <form method="POST">
    <label>Username / E-Mail</label><br/>
    <input name="login" type="text"/><br/>
    <label>Password</label><br/>
    <input name="pass" type="password"/><br/>
    <label>
     <input type="checkbox" name="remember_me"/> Remember Me
    </label>
    <button name="act_login">Log In</button>
   </form>
  </div>
 </body>
</html>

Now, we process the submitted login data. Place this code at the top of the page before :

<?php
require "config.php";
$LS->init();
if(isset($_POST["action_login"])){
  $identification = $_POST['login'];
  $password = $_POST['password'];
  if($identification == "" || $password == ""){
    $msg = array("Error", "Username / Password Wrong !");
  }else{
    $login = $LS->login($identification, $password, isset($_POST['remember_me']));
    if($login === false){
      $msg = array("Error", "Username / Password Wrong !");
    }else if(is_array($login) && $login['status'] == "blocked"){
      $msg = array("Error", "Too many login attempts. You can attempt login after ". $login['minutes'] ." minutes (". $login['seconds'] ." seconds)");
    }
  }
}
?>

The syntax for using the $LS->login() function is this :

boolean|array Fr\LS::login($username, $password, $remember_me, $cookies)

The $username parameter can be either the E-Mail (if config -> features -> email_login config value is boolean TRUE) or the username of user.

The $remember_me parameter (default FALSE) should be set to boolean TRUE, if the user needs to be remembered even after the end of the PHP session that is the user is automatically logged in after he/she visits the page again. But, to enable this, the config value features -> remember_me must be set to boolean TRUE.

The $cookies parameter (default TRUE) makes the decision whether cookies should be created or not. This is useful, when you have to check if a username and password is correct without creating any cookies and redirects. This too needs a boolean value.

User will be redirected after logging in if $cookies is set to TRUE which is the default value.

You can also login by passing the password value as NULL (Thanks Adi Kedem). This is useful when you have to login a user without knowing the password like in OAuth login process :

$LS->login($username, null)

But there is a danger to this – a user can enter just by entering the username and no password. To avoid this issue, a checking whether the password is blank is done in login.php page :

$user = $_POST['login'];
$pass = $_POST['pass'];
if($user == "" || $pass == ""){
    $msg = array("Error", "Username / Password Is Blank !");
}

If brute force check is enabled and if the account is blocked from incorrect login attempts, then an array is returned. Here is how the array look like :

array(
   "status" => "blocked",
   "minutes" => 5
   "seconds" => 300
)

Both minutes and seconds will be shown. Note that the minutes value is rounded. So, if it’s actually 4.10 seconds, the minutes value will still be “5”.

If the user login is successful, a boolean TRUE is returned, otherwise a boolean FALSE unless the account is blocked for which an array is returned instead of a boolean value.

Register / Create Account

Now, we move forward to the register page. We use Fr\LS::register() function for creating accounts. Here is the syntax :

boolean Fr\LS::register($username, $password, $extraValues);

The $extraValues variable is an array containing keys and values that are inserted with the username and password. Suppose, you made an extra column named “name” that is used for storing the user’s name. Here is how you make the $extraValues array :

array("name" => $personName)

Note that email value is not passed directly to the register() function. You should include it with **$extraValues **array and the whole array becomes :

array(
    "email" => $email,
    "name" => $name
)

You create the HTML form and pass the values got from the form to this function and it will take care of everything else. Fr\LS::register() returns “exists” if the username is already taken or if an account with the email given exists. Otherwise, if everything is successful, it returns boolean TRUE.

Check If User Exists

There is an in-built function to check if there is an account with the username or email exist already. Here is the syntax :

boolean Fr\LS::userExists($username)

You can also pass e-mail as the value if config -> features -> email_login is set to TRUE.

Check If User ID Exists

Instead of username, you can check if a user ID exist using userIDExists() :

boolean Fr\LS::userIDExists($userID);

Example :

if ($LS->userIDExists(1)) {
    // User with ID '1' exists
}

Check If User is Logged In

You can check if user is logged in with the function Fr\LS::isLoggedIn() :

if($LS->isLoggedIn()){
  // User logged in
} else {
  // User not logged in
}

Log Out

You just need to call Fr\LS::logout() for clearing the browser cookies and PHP session which means the user is logged out :

$LS->logout();

You don’t have to do anything else.

Sending E-Mails

When any components of logSys needs to send emails, it calls the Fr\LS::sendMail() function with email address, subject and body in the corresponding order.

You can change the method used to send mails by adding a callback function to config -> basic -> email_callback. Example :

"email_callback" => function($LS, $email, $subject, $body){
  mail($email, $subject . " - My Company", $body);
}

Sometimes mail() function won’t work and email won’t be received by the users. In that case, try using an Email API like Mailgun. Then use that API in the config -> basic -> email_callback function.

Forgot/Reset Password

Normally, almost every user forgets their password. logSys have a special function that takes care of everything for you. Just call Fr\LS::forgotPassword() at the place where you want to display the Forgot Password form :

<?php
require "config.php";
?>
<html>
 <head></head>
 <body>
  <div class="content">
   <?php
   $LS->forgotPassword();
   ?>
  </div>
 </body>
</html>

You may call LS->init() in the above page if you are sensitive about logged in users accessing the page. This function returns different strings according to the status of the resetting password process. Here are they :

<td>
  Description
</td>
<td>
  Identity (email/username) is not provided
</td>
<td>
  No user with given identity was found
</td>
<td>
  The Reset Password form is currently shown.
</td>
<td>
  The token given for changing password is invalid/wrong.
</td>
<td>
  The change Password Form is currently shown.
</td>
<td>
  The fields of change password form were left blank.
</td>
<td>
  The new password field and retype password field doesn&#8217;t match
</td>
<td>
  An email was sent to the account holder&#8217;s email address
</td>
<td>
  The Password was changed/resetted successfully
</td>
Return String
identityNotProvided
userNotFound
resetPasswordForm
invalidToken
changePasswordForm
fieldsLeftBlank
passwordDontMatch
emailSent
passwordChanged

Change Password

Just like forgot password, all you have to do is call $LS->changePassword() function to display the form and do the tasks needed for changing the password. As like before, logSys will take care of everything here too.

Update : As of version 0.4, logSys doesn’t take care of everything. You have to make the form and pass the values to the Fr\LS::changePassword() function. Here is an example :

<?php
require "config.php";
$LS->init();
?>
<!DOCTYPE html>
<html>
  <head>
    <title>Change Password</title>
  </head>
  <body>
    <?php
    if(isset($_POST['change_password'])){
      if(isset($_POST['current_password']) && $_POST['current_password'] != "" && isset($_POST['new_password']) && $_POST['new_password'] != "" && isset($_POST['retype_password']) && $_POST['retype_password'] != "" && isset($_POST['current_password']) && $_POST['current_password'] != ""){

        $curpass = $_POST['current_password'];
        $new_password = $_POST['new_password'];
        $retype_password = $_POST['retype_password'];

        if($new_password !== $retype_password){
          echo "<p><h2>Passwords Doesn't match</h2><p>The passwords you entered didn't match. Try again.</p></p>";
        }else if($LS->login($LS->getUser("username"), "", false, false) == false){
          echo "<h2>Current Password Wrong!</h2><p>The password you entered for your account is wrong.</p>";
        }else{
          $change_password = $LS->changePassword($new_password);
          if($change_password === true){
            echo "<h2>Password Changed Successfully</h2>";
          }
        }
      }else{
        echo "<p><h2>Password Fields was blank</h2><p>Form fields were left blank</p></p>";
      }
    }
    ?>
    <form action="<?php echo Fr\LS::curPageURL();?>" method='POST'>
      <label>
        <p>Current Password</p>
        <input type='password' name='current_password' />
      </label>
      <label>
        <p>New Password</p>
        <input type='password' name='new_password' />
      </label>
      <label>
        <p>Retype New Password</p>
        <input type='password' name='retype_password' />
      </label>
      <button style="display: block;margin-top: 10px;" name='change_password' type='submit'>Change Password</button>
    </form>
  </body>
</html>

Here is the syntax of the function :

boolean Fr\LS::changePassword($new_password, $userID);

You may optionally mention the user ID. If not, the currently logged in user is used. This function returns boolean TRUE if the password was changed.

You will now have to check whether the current password is right or not with Fr\LS::login() before changing the password.

$LS->login("username", "current_password", false, false)

The 4th parameter should be set to FALSE, otherwise redirection will occur as like a normal login.

Get User Details/Info

As I said in the introduction, you can add more columns to the table. This means that you have to get values from every columns. For this, I added an extra function to get all the fields of a particular row. To get the fields of current user, all you have to do is call Fr\LS::getUser(). Syntax :

string|array Fr\LS::getUser("column_name", $userID);

Note that, we are using $userID which is the id field of the row. If you use the column name as “*”, you will get an array as the return value like this :

array(
  "id" => 1,
  "username" => "subins2000",
  "email" => "[email protected]",
  "password" => "asd4845ghnvbmvolfpsdpsa0ffkfoeww89d9d25f1f56",
  "password_salt" => "mv5r7(4565v"
)

More fields will be obtained once you add more columns to the table. If you need to get only a single field, you can use :

$LS->getUser("column_name");

Update User Details/Info

As a suggestion of adding this feature from Kevin Hamil, I have added a function to update the users’ details. Syntax :

boolean Fr\LS::updateUser($values, $userID);

The variable $values is an array containing the information about updation of values. If you need to update the “name” field to “Vishal”, you can make the array like this :

$values = array(
    "name" => "Vishal"
);

And the $userID variable contains the user’s ID. By default, the value of it is the currently logged in user. Here is an example of updating the current user’s information :

$LS->updateUser(array(
    "name"  => "Subin",
    "birth" => "20/01/2000"
));

Two Step Login

Two Step Login is a new feature added to logSys in version 0.5. Here is how it works :

  1. User logs in with his/her username-password
  2. If the device the user used to login is not authorized, then a form is shown asking the user to enter a code
This code should be sent by your login system. You can choose the medium for sending : E-Mail/SMS

You should implement a callback that will send the token.
  1. The form also has a “Remember Device” option if enabled wouldn’t show the form again upon further logins in the future.
If the user gets the token right (note that it cannot be brute forced), then login is finished

If not, then the user would have to login again starting from username-password form

First of all enable Two Step Login by setting the value of config -> features -> two_step_login to TRUE.

Here’s a skeleton of the process :

try {
    if (isset($_POST['login']) && isset($_POST['password'])) {
        /**
         * Try login
         */
        $LS->twoStepLogin($_POST['login'], $_POST['password'], isset($_POST['remember_me']));
    } else {
        /**
         * Handle Two Step Login
         */
        $LS->twoStepLogin();
    }
} catch (Fr\LS\TwoStepLogin $TSL) {
    if ($TSL->getStatus() === 'login_fail') {
        // Username/password wrong
    } elseif ($TSL->getStatus() === 'blocked') {
        $blockInfo = $TSL->getBlockInfo();
        // Account blocked
    } elseif ($TSL->getStatus() === 'enter_token_form' || $TSL->getStatus() === 'invalid_token') {
        // Wrong token
        $hideLoginForm = true;
    } elseif ($TSL->getStatus() === 'login_success') {
        // login success. If auto init is enabled, redirection will be automatically done
    } elseif ($TSL->isError()) {
        // Some other error
    }
}

if (!isset($hideLoginForm)) {
    // Show login form here
}

See a complete example here.

The login form is placed under an if condition so that it is not displayed when the “Enter Token” form of Two step Login is shown. Or you can separate the login form page and the Two Step Login process page.

Calling twoStepLogin() with no parameters will make it automatically handle the Two Step Login part of the login process. The username and password parameter should be passed only once when the user submits the login form. This is what you see in the try{} block.

The parameters are the same as login():

$LS->twoStepLogin($_POST['login'], $_POST['password'], isset($_POST['remember_me']));

Exception

Exceptions are thrown by twoStepLogin()for handling the Two Step Login process. There are two type of exceptions here :

  • Two Step Login process exception
  • Two Step Login error exception

Note that the first exception in the above is actually good and not an error. You can test whether it’s an error using isError().

isError()

Returns whether the exception is about an error. Example :

catch (Fr\LS\TwoStepLogin as $TSL) {
    var_dump($TSL->isError()); // A boolean value
}

getOption()

For some statuses, there will be additional values associated with it. This can be obtained with this function. Example :

When the “Enter Token” form should be displayed, there are 3 values available. These are obtained like this :

$TSL->getOption('uid'); // User ID
$TSL->getOption('remember_me'); // Whether user checked 'Remember Me'
$TSL->getOption('tries_left'); // Number of tries left for entering token

getStatus()

Returns the status code of exception. This includes both error status and the Two Step Login process status. Example :

catch (Fr\LS\TwoStepLogin as $TSL) {
    var_dump($TSL->getStatus()); // String
}

The following table shows the different status values returned by Fr\LS\TwoStepLogin->getStatus() :

Type Value What It Means
process enter_token_form Show the enter token form. Read the section after this table.
process login_success The Two Step Login token was correct and the user can be logged in.

This exception can only be caught if 4th parameter to twoStepLogin() is FALSE i.e. no cookies should be set.

Because if cookies are set the user is redirected to the home page.

error login_fail The username or password was incorrect. The Two Step Login process cannot be started.
error blocked The user is blocked. Read [this](#getblockstatus).
error invalid_token The Two Step Login token submitted was incorrect.
error invalid_csrf_token CSRF Security token failed. [Read this](#csrf-security).
enter_token_form

This status is returned to show the Two Step Login “Enter Token” form. This form should be made by you with the only condition that the token input field should have the name two_step_login_token:

<input type='text' name='two_step_login_token' />

This is how the form should look :

<form action="<?php echo Fr\LS::curPageURL(); ?>" method="POST">
    <p>A token was sent to your E-Mail address. Paste the token in the box below :</p>
    <input type="text" name="two_step_login_token" /><br/>
    <span>Remember this device ?</span>
    <input type="checkbox" name="two_step_login_remember_device" /><br/>
    <input type="hidden" name="two_step_login_uid" value="<?php echo $TSL->getOption('uid'); ?>" />
    <?php
    echo $LS->csrf('i');
    if ($TSL->getOption('remember_me')) {
    ?>
        <input type="hidden" name="two_step_login_remember_me" />
    <?php
    }
    ?>
    <button>Verify</button>
    <a onclick="window.location.reload();" href="#">Resend Token</a>
</form>

NOTE that the input fields’ name attribute in your form SHOULD BE THE SAME AS SHOWN ABOVE.

Here are the additional input fields used :

  • two_step_login_remember_device
    Should the device be remembered. If it’s remembered, when the user logs in again later, Two Step Login process is skipped and user is logged in if he enters the correct username and password.
  • two_step_login_remember_me
    If the user had chosen “Remember Me” checkbox in the previous step (log in with username/password), then this field must be present to remember the user.
    Whether the user had chosen “Remember Me” checkbox in the previous step is determined by $TSL->getOption('remember_me')
  • two_step_login_uid
    Stores the user’s ID who is now attempting to log in. If this value is tampered with, then the login process fails.
blocked

When the user is blocked, the exception status will be ‘blocked’. You can get the information about the block using getBlockInfo(). The data returned will be same as in the normal login process.

Once again, I request you to see the complete example here to understand better.

Database

Now we must create a table in database that will store the devices of users :

MySQL

See this

PostgreSQL

See this

SQLite

See this

You can change the table name if you want to, but you must mention the new name in config -> two_step_login -> devices_table.

Configure

We still haven’t added the callback that will send the token. Add a callback function in config -> two_step_login -> send_callback :

'send_callback' => function($LS, $userID, $token){
    // Send Token as email
    $email = Fr\LS::getUser("email", $userID);
    $subject = 'Verify Yourself - 2 Step Verification';
    $body = '<p>Someone tried to login to your account. If it was you, then use the following token to complete logging in : <blockquote>'. $token .'</blockquote>If it was <b>not you</b>, then ignore this email and please consider to change your account\'s password.</p>';
    mail($email, $subject, $body);
}

You can use any mechanism to send the token. If you have access to SMS API, then use it because it is much more secure than E-Mail.

token_length

You can also change the token’s length. Just change the integer value of config -> two_step_login -> token_length.

token_tries

You can set a limit on how many incorrect tries the user can make while entering the Two Step Login token. For this, change the integer value of config -> two_step_login -> token_tries.

numeric

By default, the randomly generated token will have alphanumeric characters. In case you’re using SMS mechanism, it will be simpler for user to type numericals. You can make logSys generate numeric tokens for Two Step Login by setting the value of config -> two_step_login -> numeric to TRUE.

expiry

If the user chooses the “Remember Device” option, then a cookie is created in that browser that recognises the authorised device. You can set the validity of this cookie by changing the value of config -> two_step_login -> expiry. The value is used in strtotime() function, so enter values that are valid for the function.

first_check_only

If a user did “Remember Device” and visit a page, logSys will check if the device cookie matches the device ID stored in database. If it does not, then the user is logged out. This is done so, because if the original user revokes a device, the session on that device must be logged out wherever it is. But, checking this every time a page loads can decrease performance. To avoid this, you can set the value of config -> two_step_login -> first_check_only to TRUE.

getDeviceID()

The ID of the device currently logged in by the user. Actually it returns the device cookie value.

This only works if the user is logged in.

CSRF Security

To protect against CSRF, a token system is implemented in logSys. For handling CSRF security csrf() function is used.

Get Token As String

echo $LS->csrf('s');

Check If CSRF Token Is Correct

When a form is submitted it is necessary to check if CSRF token is correct. For this simply call csrf() with no parameters :

if ($LS->csrf()){
    // All good
}

Get CSRF Token As An Input Field

echo $LS->csrf('i');

This returns something like this :

<input type='hidden' name='csrf_token' value='w9cvK' />

Extra Functions/Tools

Along with the main functions in logSys, some extra tools or functions are included.

Time Since User Joined

If you would like to display to the user how much time he has been a member of the site, you have to do the following : Create a column named “created” in your users database table and add the created value in registration :

$LS->register($username, $password, array(
  "created" => date("Y-m-d H:i:s")
));

Now, you can use the built in joinedSince() function of logSys to display the time since joined :

echo $LS->joinedSince();

Some example outputs :

10 Seconds
2 Minutes
4 Hours
25 days
7 Weeks
15 Months

Check if email is valid

Use Fr\LS::validEmail() function for checking if an email address is valid or not. Usage :

Fr\LS::validEmail("[email protected]")

Current Page URL

Get the full URL of the current page. Usage :

echo Fr\LS::curPageURL()

Generate Random String

As seen on http://subinsb.com/php-generate-random-string, Generates a unique string. Usage :

Fr\LS::randStr(20)

Current Page Pathname

Get the path name of the current page. Usage :

echo Fr\LS::curPage()

Some sample outputs :

/
/myfolder/mysubfolder/mypage.php

Redirect With HTTP Status

Redirects with the HTTP status such as 301, 302. Usage :

Fr\LS::redirect("http://subinsb.com", 302)

That’s all the extra tools.

Common Problems

I should have made this section long time ago. Here are some of the most common problems and the solution to them :

Fatal error: Call to a member function prepare() on a non-object

This error happens because logSys couldn’t connect to the database. Either your server doesn’t have PHP PDO Extension or the database credentials given in config -> db is not correct.

So, install PDO extension and check if the database credentials given is correct.

Redirect Loop / Can’t Access Pages

This is the most common problem and the solution is simple. Why this error happened is because that the relative path names put in the config -> pages -> no_login array is wrong or the config -> pages->login_page is wrong or pages -> home_page has an invalid value. Here are some valid path names :

/
/index.php
/mypage/myfile.php
/login.php
/home.php

But, these path names are wrong :

index.php
http://mysite.com/mypage/myfile.php
//mysite.com/login.php
mysite.com/home.php

An easy way to find out the relative pathname of a page is to output $_SERVER[‘REQUEST_URI’] in that page.

session_start() – headers already sent

This is a common problem seen from the 0.4 version. It is because that the session is not started [session_start()] before the content is outputted.

logSys will start the session if config -> features -> start_session is set to TRUE. If you enable this, you must construct the logSys object before any output is made like this :

<?php
$LS = new Fr\LS;
?>
<html>

Cookies Not Created

When the cookies are not created, user is not logged in after submitting the form. He/she won’t be redirected to the home page and will still see the login page.

This is probably because of faults in the configuration. Check the values of config -> cookies array. Try removing the domain value or path value. Try messing with the values of it.

Also, try keeping the values of domain and path blank. It might work.

Cannot modify header information

You may have to enable Output Buffering to solve this problem. Or you should move all your logSys and redirection function calls at the top of the page before any output is made.

User Roles

Setting User Roles is a feature asked by many. I have plans to implement it, but it will take time. If you want to do it manually, this is what I recommend :

  • Add a column named “role” containing user access level in DB table
  • In the page where you display stuff on your site, add checking if the user has the level to access the page or display information in it. Example :
if($LS->getUser("role") == "admin"){
  // Show Admin stuff
}else if($LS->getUser("role") == "editor"){
  // Show Editor Stuff
}else if($LS->getUser("role") == "contributor"){
  // Show Contributor Stuff
}

assuming that the user is logged in.

Security

Chet has said that including sensitive credentials in a PHP file is not secure. I’m agreeing with him. But, a possible way to make it secure is not to include malicious/untrusted scripts in your server. See this post to see about how an attacker gets your configuration credentials such as database’s.

file_get_contents() is a function that can be exploited by an attacker to retrieve sensitive information from a server. Here is a list of the exploitable PHP functions.

If you’re going to report a problem, tell the version of logSys using, explain the problem clearly and put in some example codes.

This tutorial is completed. I will update logSys in the future if I can. After all, I’m a kid who is in 11th grade – a grade where studies should be taken seriously. Good Luck and I hope you found what you are looking for.