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.