My name is Stefan Ashwell and when I'm not coding personal projects or painting miniatures, I'm a Software Engineer at Holmes Media in Market Harborough.

This blog is a place for me to write about things that require that little bit extra to solve. We all have gaps in our knowledge and every now and then I need to reach out on search engines or blogs to find an answer. Often, these answers come from multiple sources. This blog serves as a personal resource, but if it's helpful for me I'm sure it'll help others too!

User Authenication in CodeIgniter – Some advanced techniques

Posted on 12 July 2016

Today we look at user authentication in codeigniter and some best practices to use when building a secure login environment for members of an application. Our goal here is not just to allow users to login and access members only content. We can do this very easily by setting some session data after checking the username & password entered exist in the database. We want to expand on this. It should be as secure as we can make it, and for it to be useful to us as administrators too, seeing who has logged in and when for example.

This article is part of the CodeIgniter Development Series where we discuss some advanced and real world topics involving the CodeIgniter framework.

So, let’s get stuck in…

User Authentication in CodeIgniter – Goals

  • Security
    We want our login system to be secure – we want to do everything we can to stop people’s accounts being hacked into.
  • Tracking
    We’d like to know who’s logged in, when they were last active and what they’ve looked at.
  • Efficiency
    We want to include features to ensure the system is efficient and doesn’t slow everything down.

A Bit of Theory

Before we jump in and start coding, it’s important to discuss some theory on how we can achieve the above goals.

Security

Firstly, on the security side of things we need to decide how we will determine if a user is logged in. Personally I prefer to do this using a cookie rather than a PHP Session. Reason being you can keep users logged in for longer periods of time with cookies (ie – provide a “remember me” option). However including any user information in the cookie itself is a security risk, because anybody can access the cookies in their browser and find the data stored. This is not just bad on shared computers – if you’re saving someone’s user id for example, what’s stopping someone guessing another user’s user id and setting their own cookie?

We will be using a database table along with a cookie to determine if a user is logged in. The cookie itself will hold just a hashed string of characters that reference a field in the database table. If a session exists the user is logged in, if not we can delete the cookie and get them to log in.

Taking this further, it makes sense to check other things at this stage too. The user’s last login can be saved in the database table for example – then if they’ve not been active for a set amount of time we can force log them out. We can also track a user’s IP address, and if the user’s current IP address is different log them out. It could be perfectly innocent as a lot of users will not have a static IP address, but for the sake of security getting them to log in again when it does change isn’t a big loss.

Tracking

This database table leads nicely into the tracking we want to do. Every time a user visits a page on your application, we will perform the above checks to ensure a valid session exists. At the same time we can update their last active date. You could – if you wanted to go further – have another database table that logs pages viewed or actions taken. Doing so would give you a lot of data to run statistics on, providing useful metrics for future development or marketing purposes.

Efficiency

With this kind of tracking however, we need to be careful with performance. It doesn’t take an expert to realise that if you’re inserting a new row in a table every time a user visits a page your table is going to grow very big very fast. Some tweaks and carefully inserting only when you need to can help here, along with regularly clearing data you don’t need any more.

So that’s the theory out of the way, time to start coding!

Database Tables

As you will have gathered by now, everything revolves around a database table for user sessions. Here’s an example table definition for user_sessions:

CREATE TABLE `user_sessions` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `hash` char(40) NOT NULL DEFAULT '',
  `users_id` int(11) NOT NULL,
  `created_date` datetime NOT NULL,
  `last_active` datetime NOT NULL,
  `inactive` tinyint(1) NOT NULL DEFAULT '0',
  `ip` varchar(45) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

As you can see the table references the users table with users_id. The hash field will hold a 40 character SHA1 hash which is saved in the user’s cookie. We save the date the user first logged in with this session in created_date and the last_active date holds when the user was last active in the system. The inactive field is set to 1 when a user is logged out so we know it’s an old session. Finally the ip field will hold the user’s IP address.

If we wanted to track pages a session visits here’s an example table definition for sessions_track:

CREATE TABLE `sessions_track` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `sessions_id` int(11) NOT NULL,
  `uri` varchar(255) NOT NULL DEFAULT '',
  `last_visited` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

In this table we reference the sessions table with sessions_id. The URI of the page visited and the last_visited date. We don’t really need to capture any more than this, and the smaller this table is the better!

Finally, here’s an example users table:

CREATE TABLE `users` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(55) NOT NULL DEFAULT '',
  `email` varchar(100) NOT NULL DEFAULT '',
  `password` char(40) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

I’ve kept this to the bare minimum, your system might require all manner of user information, but for the purposes of this article just the user’s name, email and a password are only really required.

MD5 vs SHA1

You’ll notice the user’s password and the hash in the tables above are 40 character SHA1 hashes rather than 32 character MD5 hashes. Put simply, SHA1 is more secure than MD5. The actual reasons why are outside the scope of this article, but with it being just as easy to hash a string using SHA1 in PHP as it is MD5, there’s no reason not to use SHA1.

sha1($password);

User Sessions Model

To achieve our goals we’ll be creating a Model. The Model will hold all the functions our application will need. To start with, let’s create the skeleton of the class first:

class User_sessions_model extends CI_Model {

        

}

Login Function:

public function login($username, $password) {

	// Is there a valid user?
	$this->db->select('id');
	$this->db->where('email', $username);
	$this->db->where('password', sha1($password));
	$query	= $this->db->get('users');
    $user   = $query->row_array();

	if ( $user['id'] ) {

		// Is there an open session for this user?
		$this->db->select('id');
		$this->db->where('users_id', $user['id']);
		$this->db->where('inactive !=', 1);
		$query		= $this->db->get('user_sessions');
	    $session   	= $query->row_array();

		if ( $session['id'] ) {

			// Close the session
			$this->destroy_session($session['id']);

		}

		// Create the new session
		$newsession['users_id']	= $user['id'];
		$newsession['ip']		= $_SERVER['REMOTE_ADDR'];
		$this->create_session($newsession);

		return true;

	} else {

		return false;

	}

}

The first part of this function is pretty standard stuff. We check to see if a user exists with the email and password passed.

If they do we check the sessions table for an active session. If there is an active session for the user we call destroy_session to close it down – we’re about to make a new one and we don’t want more than 1 active session open at once.

Then, we call create_session which creates a new session for us, and sets the relevant cookies.

Create Session Function:

public function create_session($data) {

	// Set additional data
	$data['created_date']	= date('Y-m-d H:i:s');
	$data['last_active']	= date('Y-m-d H:i:s');
	$data['inactive']		= 0;
	$data['hash']			= sha1($data['users_id'].time());

	// Perform the insert
	$this->db->insert('user_sessions', $data);

	// Create cookie
	set_cookie('user_login', $data['hash'], 0, 'your-domain');

}

This function creates a new session. All of the required data should be passed in the $data parameter as an array. Note the keys in the array match the database field name so we can use CodeIgniter’s build in database class to perform the insert for us. Then we create the user’s cookie.

This is also where we create the hash. You can do this a number of ways, but it’s a good idea to combine a number of pieces of information unique to the member with a random string or the current time. It’s also a good idea to use a different combination of values on each application you build.

Session Check Function:

public function session_check($hash) {

	// Is there a session for this hash?
	$this->db->where('hash', $hash);
	$this->db->where('inactive !=', 1);
	$query		= $this->db->get('user_sessions');
    $session   	= $query->row_array();

	if ( $session['id'] ) {

		// Check the session isn't more than 30 days old and the ips match
		if( ( strtotime($session['last_active']) < strtotime('-30 days') ) || $session['ip'] != $_SERVER['REMOTE_ADDR'] ) {

			$this->destroy_session($session['id']);
			return false;

		} else {

			// Update the last active date to now
			$this->db->set('last_active', date('Y-m-d H:i:s'));
			$this->db->where('id', $session['id']);
			$this->db->update('user_sessions');

			// Track the URI
			$trackdata['sessions_id']	= $session['id'];
			$trackdata['uri']			= $this->uri->uri_string();
			$this->track_uri($trackdata);

			// Return the session
			return $session;

		}

	} else {

		return false;

	}

}

This function is used when a user visits a page. The hash stored in their cookie is passed in the $hash parameter, and we check to see if there’s a session that isn’t inactive in the database. If there is an active session, we perform a check to see if their session is too old, and a check to ensure the user’s current IP address matches the one in the session. If any of the checks fail we return false and call destroy_session.

Otherwise, we update the last active date and we call track_uri to track the URI the user has visited. Finally we return the session itself.

Destroy Session Function:

public function destroy_session($session_id) {

	// Mark as inactive in database
	$this->db->set('inactive', 1);
	$this->db->where('id', $session['id']);
	$this->db->update('user_sessions');

	// Destroy the cookie
	delete_cookie('user_login', 'your-domain');

}

Here we mark a session as inactive and destroy the user’s cookie.

Track URI Function:

public function track_uri($data) {

	// Set additional data
	$data['last_visited']	= date('Y-m-d H:i:s');

	// Perform the insert
	$this->db->insert('sessions_track', $data);

}

This function inserts the current session, URI and date into the sessions_track table.

Usage Examples

In the above user sessions model there are a number of functions that provide us with our secure user logins system – but how do we use them? Here are a few examples:

Handling a login form

public function login($username, $password) {

	// Is a user already logged in?
	$users_data = $this->users_model->session_check($this->input->cookie('user_login'));

	if ( $users_data['id'] ) {
		redirect('members');
	}

	// Login form submission
	if ( $this->input->post('frm_submit') ) {

		$users_id = $this->users_model->login($this->input->post('email'), $this->input->post('password'));

		if ( $users_id ) {

			redirect('dashboard');

		} else {

			$data['error_message'] = 'Login failed, please check your username and password are correct and try again';

		}

	}

	// Load view
	$this->load->view('login', $data);

}

Notice how the login form also checks for a logged in user. If there is one it redirects to the members section – we don’t want them to log in twice!

Checking if a user is logged in

public function members_only_page() {

	// Is a user already logged in?
	$users_data = $this->users_model->session_check($this->input->cookie('user_login'));

	if ( !$users_data['id'] ) {
		redirect('login');
	}

	// User is logged in and valid - continue with members only content

	// Load view
	$this->load->view('members_only_page', $data);

}

Typically, this will be done on every controller function. The if statement checking if a user is logged in and redirecting if not can be removed for pages that aren’t member’s only – this way you can display a login link if a user is not logged in for example.

Handling logging out

public function logout() {

	// Is a user already logged in?
	$users_data = $this->users_model->session_check($this->input->cookie('user_login');

	if ( $users_data['id'] ) {
		$this->users_model->destroy_session($users_data['id']);
	}

	redirect('login');

}

Auto load the model

// config/autoload.php

$autoload['model'] = array('User_sessions_model');

For most systems, this functionality will be applicable to every page. Therefore it makes sense to autoload the model in order to have access to it at all times.

Performance Tweaks

Smart inserting into sessions_track

In our tracking function we could be a bit smarter with the data we insert. Firstly we could check to see if the same track request has happened within the last few seconds. This simple throttling will stop multiple requests erroneously filling the table up. Secondly, tracking the time of every request to a URI might be a bit overkill, so we can check to see if the session has already tracked the URI, and if it has just increment the visits field. Of course, this does require the addition of a visits field in the database table too.

Cleaning up data we don’t need in sessions_track

Via a cron job, you could remove all data that is more than 30 days old (or any age depending on how long you want to track stats for). If you wanted to be really clever, you could even export this data out to a csv and save it on the server before deleting, so you have historical data at your disposal if you ever did want to run stats on large amounts of historical data.

Utilising Database Indexes

The addition of a few carefully placed indexes can dramatically improve the performance of these tables. Typically any field a WHERE query is performed against.

Final Thoughts

Thanks for reading this article, hopefully you found some of the code and concepts useful. I’d like to finish with the caveat that the above advice is not the holy bible to the most secure user authentication, just the culmination of what I’ve learnt over the years building this type of authentication into CodeIgniter projects.

All of the above code can be downloaded from the articles github repo:

https://github.com/stef686/codebastard-user-sessions-demo