DO YOU NEED TO GET STARTED WITH USER AUTHENTICATION WITH PHP?

 

Keep reading to learn how to implement a simple but fully functional PHP class for managing user authentication.

You will see:

  • how to authenticate users via username and password;
  • how to keep users authenticated using cookies;
  • how to properly store account passwords for keeping them safe even if the server is compromised;
  • how to have multiple account sessions (that means: let users log in from multiple browsers at the same time, for example from their PC and from their phone);
  • how to log out users closing just the current session or all the account’s open sessions;
  • bonus: how to set an account expiry date (very handy for temporary accounts!)

 

 

 

Authentication
Copyright: convisum/123RF

 

 

We are going to write a PHP class named User that will provide all these functionalities. We will also see how to add new accounts and how to edit and delete existing ones using static functions.

At the end of this post you will also find a link to download a complete example file in PHP format.

Let’s get started.

FIRST STEP: THE DATABASE

 

 

In order to do any kind of authentication, we first need a database where to store the accounts data. In this example we will use MySQL, but you can use any database you are familiar with.

Our User class will work with two database tables: the first is called accounts and the other one is called sessions

The accounts table is where all the accounts are stored, along with their data such as username and password. This table is composed of the following columns:

  • account_id: the unique account identifier (and also the table’s primary key);
  • account_name: the account’s username used for log in;
  • account_password: a secure hash of the account’s password;
  • account_enabled: a boolean field (i.e., it can be true or false) that we can use to disable an account without deleting it from the database;
  • account_expiry: a date field with an optional expiry date for the account.

 

Every account has its own record saved in this table. Depending on your needs, you may find useful to add some other fields, like the registration date or the role (admin, standard user…). It really depends on what your application needs to do.

Before adding too many columns, keep in mind that optional data (i.e., data that not every account necessarily has) should probably be put in a different table. We will see how to do this in detail in another post.

If you have any doubt about this database structure feel free to ask me any question in the comments at the end of this post.

To create this table on the fly, just click below to show the SQL code (note that the table has a unique key on the account_name column so to avoid duplicate usernames):

 

accounts table SQL code (click to show)
CREATE TABLE IF NOT EXISTS `accounts` (
`account_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`account_name` varchar(254) NOT NULL,
`account_password` varchar(254) NOT NULL,
`account_enabled` tinyint(1) unsigned NOT NULL DEFAULT '1',
`account_expiry` date NOT NULL DEFAULT '1999-01-01',
PRIMARY KEY (`account_id`),
UNIQUE KEY `account_name` (`account_name`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

 

 

Let’s move on to the other table: sessions.

 

This table is used to store the cookies that we send to users after they log in. When the same user will come back to our site, his browser will send the cookie back to our web application so that we can log in the user automatically.

You probably already know that PHP has a feature called Sessions that can be used to do just that, without the need to create any table or write any specific code. So why am I bothering using my own custom table instead?

The reason is that PHP Sessions, while very handy, have some limitations. For example, using PHP Sessions you wouldn’t be able to do some useful operations like “close all account’s browser sessions” or have a list of active sessions. If you are building a web applications, there is a good chance that using a custom table (like the session table in this example) will prove very useful in the future.

 

The session table has the following columns: 

  • session_id: the table primary key;
  • session_account_id: the account id (from the accounts table) the session refers to;
  • session_cookie: a hash of this session’s cookie;
  • session_start: the timestamp (date and time) of when the user has logged in; this is useful for keeping a session opened only for some time.

Again, here is the SQL code for creating the table:

sessions table SQL code (click to show)
CREATE TABLE IF NOT EXISTS `sessions` (
`session_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`session_account_id` int(10) unsigned NOT NULL,
`session_cookie` char(32) NOT NULL,
`session_start` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`session_id`),
UNIQUE KEY `session_cookie` (`session_cookie`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

 

Note that an account can have any number of open sessions. For example, if a user logs in from his computer and from his phone using the same username and password, then his account will have two open sessions and there will be two different records on the sessions table.

Now let’s start with the PHP code.

 

  

SECOND STEP: DATABASE CONNECTION

 

Database

 

 

Our User class will need to perform read and write operations on the database, so the first thing we need is a database connection. There are many ways to do that; here we will create a resource object using the PHP PDO class:

/* Database PDO connection. */
try
{
   $db = new PDO('mysql:host=localhost;dbname=test', 'myUser', 'myPasswd');
   $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
catch (PDOException $e)
{
   echo $e->getMessage();
}

 

Remember to change the host, dbname, username and password values with yours. You may also want to manage exceptions (connection errors) is some other way. If you need any advice just ask me in the comments.

Be sure to always keep your code safe from injection attacks. Read my SQL injection prevention guide to learn how to do it.

 

The $db variable we just defined is an instance of the PDO class that we will use to perform database operations. 

There are many ways to let our class use this variable; for example, if we define $db in the global scope then we can use it from inside any class function by declaring it global (or using the $GLOBALS special array). Another way is to pass the $db variable to the class constructor and store it as a class parameter, so that every class function will be able to use it with the $this operator.

Static class functions cannot use $this, so in that case we need to use global or pass the $db variable to the function itself as an argument.

 

In our example we will pass the $db object to the class constructor and to the static functions, but this doesn’t mean that this is the “right” way to do it. It really depends on your needs and how your web application is done. Again, if you need a different example feel free to ask.

 

Now it’s finally time to create the User class.

 

 

THIRD STEP: CREATING THE USER CLASS

 

 

Let’s begin with the class properties and constructor:

 

class User
{
   /* Cookie name used for cookie authentication */
   const cookie_name = 'auth_cookie';
   
   /* Cookie session's length in seconds (after that, the user must authenticate again with username and password) */
   const session_time = 604800; // 7 days
   
   /* Account Id (taken from the account_id column of the accounts table) */
   private $account_id;
   
   /* Account username */
   private $account_name;
   
   /* Boolean value set to TRUE if the authentication is successful */
   private $is_authenticated;
   
   /* Optional account expiry date */
   private $expiry_date;
   
   /* Cookie session id (session_id column from the sessions table) */
   private $session_id;
   
   /* Timestamp of the last login (stored in "Unix timestamp" format) */
   private $session_start_time;
   
   /* PDO object to use for database operations */
   private $db;
   
   /* Constructor; it takes the $db object as argument, passed by reference */
   public function __construct(&$db)
   {
      $this->account_id = NULL;
      $this->account_name = NULL;
      $this->is_authenticated = FALSE;
      $this->expiry_date = NULL;
      $this->session_id = NULL;
      $this->session_start_time = NULL;
      $this->db = $db;
   }
}

 

The first two class properties are constants, because their value doesn’t need to change. The other properties are related to the user that logs in.

The constructor initialize all the user-related properties to NULL, since no user has logged in yet. These parameters will be properly set by the login functions when a user logs in.

The only property available from the start is $db (that, as we saw before, is used for database operations).

 

Now we will see how to authenticate users and how to manage cookies.

 

 

FOURTH STEP: AUTHENTICATION

 

Login

 

 

Username and password authentication is done by the class function called login. This function takes the username and password as arguments and checks whether they are valid. If so, it also sets the user-related class properties.

/* Username and password authentication */
public function login($name, $password)
{
   /* Check the strings' length */
   if ((mb_strlen($name) < 3) || (mb_strlen($name) > 24))
   {
      return TRUE;
   }
   
   if ((mb_strlen($password) < 3) || (mb_strlen($password) > 24))
   {
      return TRUE;
   }
   
   try
   {
      /* First we search for the username */
      $sql = 'SELECT * FROM accounts WHERE (account_name = ?) AND (account_enabled = 1) AND ((account_expiry > NOW()) OR (account_expiry < ?))';
      $st = $this->db->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
      $st->execute(array($name, '2000-01-01'));
      $res = $st->fetch(PDO::FETCH_ASSOC);
       
      /* If the username exists and is enabled, then we check the password */
      if (password_verify($password, $res['account_password']))
      {
         /* Log in ok, we retrieve the account data */
         $this->account_id = $res['account_id'];
         $this->account_name = $res['account_name'];
         $this->is_authenticated = TRUE;
         $this->expiry_date = $res['account_expiry'];
         $this->session_start_time = time();
          
         /* Now we create the cookie and send it to the user's browser */
         $this->create_session();
      }
   }
   catch (PDOException $e)
   {
      /* Exception (SQL error) */
      echo $e->getMessage();
      return FALSE;
   }
   
   /* If no exception occurs, return true */ 
   return TRUE;
}

 

The login function searches the accounts table for the given username. This is done by the first SQL query (on line 7). The query also checks that the account is enabled (the column account_enabled must be 1) and not expired.

 

Just a word about the expiry date. We want to be able to create temporary accounts that will be allowed to log in only until a particular date. This is done by storing the expiry date in the account_expiry field. However, we also want to create permanent accounts, so the expiry date must be optional.

Optional data like this should be stored in a separated table, but for the sake of simplicity I decided not to make another table in this tutorial. Instead, I just assume that if the expiry date is set before year 2000, than it means that the account is permanent.

(You could also set the expiry date column to NULL, but sometimes MySQL NULL values can be problematic so I prefer not to use them.)

 

If the search query returns a record, it means that the given username exists and is valid. Then, the function checks if the given password matches the one on the database.

It’s very important not to store the password in plain text anywhere, not even on the database, because if the server is compromised then all the accounts’ passwords could potentially be read and stolen. For this reason, the account_password field doesn’t contain the plain text password but a secure hash of it. There is no (easy) way to retrieve the password from its hash, so even if somebody steals the hash he cannot do anything with it.

PHP has two very handy functions to create a secure hash of a password and to verify a password from its hash. In the login function we use the password_verify function: it takes the plain text password and its hash as arguments and returns TRUE if they match.

(We will see how to create a password hash in the static function for adding new accounts).

 

If the user has logged in successfully, than the login function reads all the account data, and then sends a cookie to the client with the create_session() class function:

 

/* Sends a new authentication cookie to the client's browser and saves the cookie hash in the database */
private function create_session()
{
   try
   {
      /* Create a new cookie */
      $cookie = bin2hex(random_bytes(16));
    
      /* Saves the md5 hash of the new cookie in the database */
      $sql = 'INSERT INTO sessions (session_cookie, session_account_id, session_start) VALUES (?, ?, NOW())';
      $st = $this->db->prepare($sql);
      $st->execute(array(md5($cookie), $this->account_id));
    
      /* Reads the session ID of the new cookie session and stores it in the class parameter */
      $this->session_id = $this->db->lastInsertId();
   }
   catch (PDOException $e)
   {
      /* Exception (SQL error) */
      echo $e->getMessage();
      return FALSE;
   }
 
   /* Finally we actually send the cookie to the user and we save it in the $_COOKIE PHP superglobal */
   setcookie(self::cookie_name, $cookie, time() + self::session_time, '/');
   $_COOKIE[self::cookie_name] = $cookie;

   /* If no exception occurs, return true */ 
   return TRUE;
}

 

Every time a user logs in via username and password authentication, we send a cookie to his browser. As you can see at line 7, this cookie is actually a random string.

First, the function inserts the cookie in a new record of the sessions table, along with the account_id it refers to and the current date. Notice, at line 12, that the table record doesn’t contain the actual cookie, but its md5 hash. Why? Because this way, even if the database is compromised, the cookie would remain safe and the attacker wouldn’t be able to log in.

We are not using the password_hash and password_verify functions for the cookies because they don’t require that much security. Cookies are temporary anyway, and while is theoretically possible to crack a md5 hash, by the time the attacker has cracked the hash the cookie will already be expired. (Furthermore, cookies are not vulnerable to dictionary attacks making them even more difficult to decrypt).

 

Important: note how all the SQL code is properly protected against SQL injection attacks.

 

After saving the cookie in the database, the function sends it to the client’s browser using the setcookie PHP function. It uses the class constants cookie_name and session_time to set the cookie name and expiry time.

 

When the user comes back to our site, or visits another page, his browser will send the cookie to our web application. We can read this cookie and authenticate the user without asking again for username and password. This kind of authentication is done by another class function called cookie_login:

 

/* Cookie authentication */
public function cookie_login()
{
   try
   {
      if (array_key_exists(self::cookie_name, $_COOKIE))
      {
         /* First we check the cookie's length */
         if (mb_strlen($_COOKIE[self::cookie_name]) < 1) 
         { 
             return TRUE;
         }

         $auth_sql = 'SELECT *, UNIX_TIMESTAMP(session_start) AS session_start_ts FROM sessions, accounts WHERE (session_cookie = ?) AND (session_account_id = account_id) AND (account_enabled = 1) AND ((account_expiry > NOW()) OR (account_expiry < ?))';
         $cookie_md5 = md5($_COOKIE[self::cookie_name]);
         $auth_st = $this->db->prepare($auth_sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
         $auth_st->execute(array($cookie_md5, '2000-01-01'));
         
         if ($res = $auth_st->fetch(PDO::FETCH_ASSOC))
         {
            /* Log in successful */
			$this->account_id = $res['account_id'];
            $this->account_name = $res['account_name'];
            $this->is_authenticated = TRUE;
            $this->expiry_date = $res['account_expiry'];
            $this->session_id = $res['session_id'];
            $this->session_start_time = intval($res['session_start_ts'], 10);
         }
      }
   }
   catch (PDOException $e)
   {
      /* Exception (SQL error) */
      echo $e->getMessage();
      return FALSE;
   }
   
   /* If no exception occurs, return true */ 
   return TRUE;
}

 

This function searches for the cookie inside the superglobal $_COOKIE; if it’s there, it also checks the sessions table to see if a valid session with that cookie exists. In that case, the function performs the same validation checks and data retrieval the login function does.

  

 

Free PDF checklist: 10 Authentication Security mistakes you must avoid

 

 

 

FIFTH STEP: LOGOUT

 

Exit

 

 

 

Now let’s see how to log out (or disconnect) a user with the logout class function. This function takes one boolean argument: $close_all_sessions. If this argument is FALSE, then this function closes only the user’s current session, and the user will be logged out only from the browser he is currently using. If the argument is TRUE, then the function also closes every open session of the same account.

For example, a user could log in from his computer, then leave his home and connect again from his phone. If he then logs out from his phone, we can close his phone session only and leaving the computer session open (in this case, the $close_all_sessions argument would be FALSE), or we could close all the open sessions ($close_all_sessions set to TRUE).

 

Usually, when a user logs out he wants to close his current session only, but sometimes may be useful to close the other sessions too. For example, when a user changes his account’s password it could be a good idea to force him to login again from every browser.

Here is the logout function:

 

/* Logs out user and close his current session (and all other sessions if $close_all_sessions is TRUE) */
public function logout($close_all_sessions = FALSE)
{
   /* First we check if a cookie does exist */
   if (mb_strlen($_COOKIE[self::cookie_name]) < 1)
   {
      return TRUE;
   }
   
   try
   {
      /* First, we close the current session */
      $cookie_md5 = md5($_COOKIE[self::cookie_name]);
      $sql = 'DELETE FROM sessions WHERE (session_cookie = ?) AND (session_account_id = ?)';
      $st = $this->db->prepare($sql);
      $st->execute(array($cookie_md5, $this->account_id));
       
	  /* Do we need to close other sessions as well? */
      if ($close_all_sessions)
      {
         /* We close all account's sessions */
         $sql = 'DELETE FROM sessions WHERE (session_account_id = ?)';
         $st = $this->db->prepare($sql);
         $st->execute(array($this->account_id));
      }
   }
   catch (PDOException $e)
   {
      /* Exception (SQL error) */
      echo $e->getMessage();
      return FALSE;
   }
   
   /* Delete the cookie from user's browser */
   setcookie(self::cookie_name, '', 0, '/');
   $_COOKIE[self::cookie_name] = NULL;
   
   /* Clear user-related properties */
   $this->account_id = NULL;
   $this->account_name = NULL;
   $this->is_authenticated = FALSE;
   $this->expiry_date = NULL;
   $this->session_id = NULL;
   $this->session_start_time = NULL;
   
   /* If no exception occurs, return true */ 
   return TRUE;
}

 

 

 

SIXTH STEP: STATIC FUNCTIONS

 

 

Now we will see how to add, delete and edit accounts. Contrary to the class functions we have seen so far, the functions for adding, deleting and editing accounts are not related to user authentication, and can be used regardless of whether some user has logged in or not.

It is a good idea to make this functions static, since they don’t need to use any class property except for the $db object, which will be passed as function argument.

 

The function for adding a new account is called add_account:

/* Adds a new account */
public static function add_account($username, $password, &$db)
{
   /* First we check the strings' length */
   if ((mb_strlen($username) < 3) || (mb_strlen($username) > 24))
   {
      return TRUE;
   }
   
   if ((mb_strlen($password) < 3) || (mb_strlen($password) > 24))
   {
      return TRUE;
   }
   
   /* Password hash */
   $hash = password_hash($password, PASSWORD_DEFAULT);
    
   try
   {
      /* Add the new account on the database (it's a good idea to check first if the username already exists) */
      $sql = 'INSERT INTO accounts (account_name, account_password, account_enabled, account_expiry) VALUES (?, ?, ?, ?)';
      $st = $db->prepare($sql);
      $st->execute(array($username, $hash, '1', '1999-01-01'));
   }
   catch (PDOException $e)
   {
      /* Exception (SQL error) */
	  echo $e->getMessage();
      return FALSE;
   }
   
   /* If no exception occurs, return true */
   return TRUE;
}

 

Here we use the PHP password_hash function to generate the account password’s secure hash. As we saw before in the login function, we can use the password_verify function to verify if a password from its hash.

If you are going to use this add_account function in your application, be sure to add some validation check. For example, you probably want to validate username and password before adding the new account, and check if the username already exists. If you need any advice just ask a question in the comments.

 

Now let’s see how to delete an account with the static function called delete_account:

/* Deletes an account */
public static function delete_account($account_id, &$db)
{
   /* Note: you should only allow "admin" users to run this function 
      and carefully check the $account_id value */
   
   try
   {
      /* First, we close any open session the account may have */
      $sql = 'DELETE FROM sessions WHERE (session_account_id = ?)';
      $st = $db->prepare($sql);
      $st->execute(array($account_id));
       
      /* Now we delete the account record */
      $sql = 'DELETE FROM accounts WHERE (account_id = ?)';
      $st = $db->prepare($sql);
      $st->execute(array($account_id));
   }
   catch (PDOException $e)
   {
      /* Exception (SQL error) */
      echo $e->getMessage();
      return FALSE;
   }
   
   /* If no exception occurs, return true */
   return TRUE;
}

 

The last function of this tutorial, edit_account, can be used to edit an existing account:

/* Edit an existing user; arguments set to NULL are not changed */
public static function edit_account($account_id, &$db, $username = NULL, $password = NULL, $enabled = NULL, $expiry = NULL)
{  
   /* Note: 
      each argument should be checked and validated before running the update query.
      You should check the strings' length (like in the other functions), the correct 
      format of the expiry date and so on.
   */
   
   /* Array of values for the PDO statement */
   $sql_vars = array();
    
   /* Edit query */
   $sql = 'UPDATE accounts SET ';
    
   /* Now we check which fields need to be updated */
   if (!is_null($username))
   {
      $sql .= 'account_name = ?, ';
      $sql_vars[] = $username;
   }
    
   if (!is_null($password))
   {
      $sql .= 'account_password = ?, ';
      $sql_vars[] = password_hash($password, PASSWORD_DEFAULT);
   }
   
   if (!is_null($enabled))
   {
      $sql .= 'account_enabled = ?, ';
      $sql_vars[] = strval(intval($enabled, 10));
   }
   
   if (!is_null($expiry))
   {
      $sql .= 'account_expiry = ?, ';
      $sql_vars[] = $expiry;
   }
   
   if (count($sql_vars) == 0)
   {
      /* Nothing to change */
	  return TRUE;
   }
   
   $sql = mb_substr($sql, 0, -2) . ' WHERE (account_id = ?)';
   $sql_vars[] = $account_id;
    
   try
   {
      /* Execute query */
      $st = $db->prepare($sql);
      $st->execute($sql_vars);
   }
   catch (PDOException $e)
   {
      /* Exception (SQL error) */
      echo $e->getMessage();
      return FALSE;
   }
   
   /* If no exception occurs, return true */
   return TRUE;
}

You can choose to change some or all of the account’s parameter, depending on which arguments you pass to the edit_account function. The first two arguments are always required (the first one is the id of the account to edit and the second one is the database resource), while the others are the new values for the account fields and are optional.

The function will only edit a parameter if its corresponding argument has been set. If not, or if it’s set to NULL, it will be left unchanged.

This tutorial ends here. I hope it will help you getting started with user authentication.

I really thank you for reading this post, and if you liked it please take a second to share it!

 

Alex