Building a Complete CodeIgniter Application: Part 4
Jim O'Halloran • September 5, 2010
php codeigniter feedignitionIt's been a very long time since there was a new installment to this tutorial series (Jim from the future: No kidding! I don't know when I wrote this, but the nest date I can find is Sep 2010. I think that's when I moved the draft to a new wiki, I probably actually write this in 2008 or thereabouts. Fairly safe to say that when I wrote "it's been a long time, I didn't think 14 years type long time. But now it's 2021, and it's just seeing the light of day!), but we're back again with something new.
At the outset, I stated that we were going to build a multiuser feed reader. This means the one instance of our feed reader can serve many users, with each individual being able to log in and view their own feeds. To date we've built a proof of concept single user feed reader, but clearly the login and authentication mechanism is very important to the overall system.
CodeIgniter developers are blessed with a number of different authentication libraries (FreakAuth, ErkanaAuth, EzAuth and many others) which over varying levels of functionality. However, it seems like all we've done to date is integrate third party libraries, so lets build our own user authentication system. It's not actually that hard, and I like to use the hook and filters system for authentication. So let's take a look at those aspects of CodeIgniter.
Sessions
If we're going to do user logins, we somehow need to track whether the user has logged in or not. The obvious way to do this is using sessions, and for this task I prefer the Native Session library by Dariusz Debowczyk. Native Session is a PHP wrapper for the PHP session functions. It won't work on shared hosting servers where the PHP session functions aren't available. Native Session has one significant advantage over the normal CI session library, it doesn't send the entire session data to the browser in a cookie. This prevents users from seeing the contents of their session, it also neatly avoids cookie size limits, and saves bandwidth.
Setting up Native Session is pretty easy, just obtain the Native Session class
from the CI Wiki, and drop it in the
system/application/libraries folder
. We'll need to use the session functions on
virtually every page, so let's autoload the session library rather than doing it
manually every time. Open system/application/config/autoload.php
and find the
line:
$autoload['libraries'] = array('database');
... and change it to ...
$autoload['libraries'] = array('database', 'session');
Filters
We'll use the filter system later on to restrict access to our feed reader to
logged in users. First we'll want to obtain the filters system from the CI
Wiki. Unzip the filters system and place the filter
folder in system/application/hooks
. There will also be a filters folder in the
zip file, place that in system/application
. The filters system attaches to the
CI page rending process using Hooks, and allows the developer to create classes
which operate on certain URLs. We'll use this later to make sure the user is
logged in before allowing them to access certain controllers.
Hooks are turned off by default, so we'll need to enable them. Open
system/application/config/config.php
, and look for this line...
$config['enable_hooks'] = FALSE;
.. and change it to ...
$config['enable_hooks'] = TRUE;
Lastly, we'll want to set up the hooks used by the filters system. My config is
slightly different to standard. Create a file in system/application/config/
called hooks.php
with the following contents:
<?php if (!defined('BASEPATH')) {exit('No direct script access allowed');}
/*
| -------------------------
| Hooks
| -------------------------
| This file lets you define "hooks" to extend CI without hacking the core
| files. Please see the user guide for info:
|
| http://www.codeigniter.com/user_guide/general/hooks.html
|
*/
$hook['post_controller_constructor'][] = array(
'class' => '',
'function' => 'pre_filter',
'filename' => 'init.php',
'filepath' => 'hooks/filters',
'params' => array()
);
$hook['post_controller'][] = array(
'class' => '',
'function' => 'post_filter',
'filename' => 'init.php',
'filepath' => 'hooks/filters',
'params' => array()
);
?>
Clean Up
Before we get stuck into our user authentication system, lets tidy up a few
things from earlier. First set the feeds controller to be the default
controller, later we'll use a page from the feeds controller as the home page
on our system. Open system/application/config/routes.php
and look for the line
beginning with $route['default_controller']
, and change it to....
$route['default_controller'] = "feed";
We'll also want to display "Logged in as Jim O'Halloran (log out)" at the top of
every page once a user is logged in. Open systam/application/views/header.php
,
and add the following code immediately after the body tag...
<?
$CI = &get_instance();
$user = $CI->session->userdata('user');
if ($user !== false) {
?><div class="loggedin">Logged in as <?=htmlentities($user['first_name'].' '.$user['last_name']); ?> (<a href="<?=site_url('user/logout'); ?>">logout</a>)</div>
<? }
?>
This code fragment uses the session library to get the user information from the
session. The session library's userdata()
function returns false if the item
doesn't exist in the user session, and we'll rely on this to tell whether a user
is logged in. If the $user variable isn't false, someone has logged in, so we'll
output a div with the user's information. Note the use of htmlentities()
to
avoid Cross Site Scripting (XSS) problems with the user info. Also notice the
use of the site_url
function to create an absolute path for the log out page
we'll create later. Later we'll also use CSS to style this div nicely, for the
moment it's ugly but works.
Basic User Model
Lets get started by creating a database table for our users. We just need simple username, password, first name, last name, and email fields. We'll also add in an autoincrementing id field to provide a unique ID field. Use your favorite MySQL admin tool to run the following SQL against the database we created back in part 1:
CREATE TABLE `users` (
`id` int(11) NOT NULL auto_increment,
`username` varchar(30) default NULL,
`password` varchar(81) NOT NULL,
`email` varchar(100) NOT NULL,
`first_name` varchar(50) NOT NULL,
`last_name` varchar(50) NOT NULL, PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
We'll both salt and hash the passwords we store in the database, so the password
field is 81 characters long to allow for two SHA1 hashes and a delimiter. Next
we'll start by creating a model class which can load and save data from this
table. I like to use a class which has member variables for each of the fields,
and load()
, save()
and reset()
methods for retrieving, saving, and creating a
new record. Create a file in system/application/models/usermodel.php
, and start
with the following class definition...
<?php if (!defined('BASEPATH')) exit('No direct script access allowed');
class UserModel extends Model {
private $_id = false;
public $username = '';
public $password_hash = '';
public $email = '';
public $first_name = '';
public $last_name = '';
function __construct() {
parent::Model();
}
}
?>
At this point we've got a pretty useless model class with member variables for
each of the database fields. I make the $id
field private because I don't want
controllers to be able to change the id value. I also place an underscore (_) in
front of private variables to indicate their scope throughout the code, this isn't
required, but it's a useful convention. I've also renamed the password field to
password_hash
in the model. The reason for this will become clear later on.
Next we'll implement a read only property for the id
field. Place these two
functions below the class constructor:
function __get($var) {
switch ($var) {
case 'id':
return $this->_id;
}
}
function __set($var, $value) {
switch ($var) {
case 'id':
throw new Exception('id is not a settable property of UserModel');
default:
$this->$var = $value; break;
}
}
The __get
function is a magic method which PHP will call whenever we try to access
a member variable which doesn't exist in the class. The name of the member
variable that we attempted to access is passed as the first parameter of the __get
method. We'll set up a switch statement here to test this value and return the _id
member variable if the caller wanted the id
property. I've used a case
statement because we'll add a extra variiables later. The __set
function is also a
magic method, this one is called whenever we try to assign to a member variable
which doesn't exist. Two parameters are passed to __set
, the name of the variable,
and the value that was assigned. We'll once more set up a switch structure to
test the variable name and take action. We want to prevent users of the class
from changing the id
, so if someone tries to assign to our fake id
property,
we'll throw an exception. All other variables we'll just assign directly to the
class, to ensure CI can still do its thing.
Now we want to provide a method to retrieve a user from the database, drop the
following function into the model below the __set
method:
function load($id_or_row) {
if (is_array($id_or_row)) {
$row = $id_or_row;
} elseif (is_numeric($id_or_row)) {
$rs = $this->db->query('SELECT * FROM users WHERE id = ?', array($id_or_row));
if ($rs->num_rows() > 0) {
$row = $rs->row_array();
} else {
return false;
}
} else {
$rs = $this->db->query('SELECT * FROM users WHERE username = ?', array($id_or_row));
if ($rs->num_rows() > 0) {
$row = $rs->row_array();
} else {
return false;
}
}
$this->_id = $row['id'];
$this->username = $row['username'];
$this->password_hash = $row['password'];
$this->email = $row['email'];
$this->first_name = $row['first_name'];
$this->last_name = $row['last_name'];
return true;
}
I like my load methods to be fairly flexible. This one can accept either a numeric value containing the ID to load, a string containing the username, or an array containing all of the field values. The latter is handy when you query the database for lots of users and want to create a model object from each because then you don't need to query the database again for each record.
So we can load from the database, now what about saving. Here we'll set up a
save method which handles the creation of either an update or an insert statement
depending on the record's _id
field. We use the boolean false value to denote a
record which hasn't yet been assigned. Note: You could use ActiveRecord for
this, but I've been writing SQL for years, and I'm more comfortable with that,
so that's what I'll do.
The code for the save function is as follows, place it below the load function in our class:
function save() {
if ($this->_id === false) {
$this->db->query("INSERT INTO users(username, password, email, first_name, last_name) VALUES(?, ?, ?, ?, ?)",
array($this->username, $this->password_hash, $this->email, $this->first_name,
$this->last_name));
$this->_id = $this->db->insert_id();
} else {
$this->db->query("UPDATE users SET username=?, password=?, email=?, first_name=?, last_name=? WHERE id=?",
array($this->username, $this->password_hash, $this->email, $this->first_name, $this->last_name, $this->_id));
}
}
Finally, I like to have a reset method which resets the member variables back to defaults, this makes adding multiple records to the database easier. Here's my reset method:
function reset() {
$this->_id = false;
$this->username = '';
$this->password_hash = '';
$this->email = '';
$this->first_name = '';
$this->last_name= '';
}
Now we've got some basic model functionality which we'd use for any model where we want CRUD functionality.
Handling Passwords
Storing passwords is always a sensitive issue. Some people just store them in the database, but the problem here is that if an attacker can get a copy of the database, they can log in as any user on the system. You can usually tell systems which have been designed in this way because the lost password routine actually sends you your password in an email. This is obviously not ideal from a security perspective.
Then once people have figured out that plain text passwords are a bad idea, they'll usually MD5 the passwords before putting them in the database. MD5 is a one way hasn algorithm, data goes in and a (generally) unique hash comes out. It's not possible to obtain the original password from it's MD5 hash without using brute force (hashing all possible passwords), which makes it impossible to give back the original password via a "lost password" email. Simple MD5'ing is ok, but has two problems. First, tables of hashes for a large number of possible passwords (rainbow tables) are available on the Internet which make brute forcing MD5'ed passwords easier. Secondly, two users with the same password have the same hash stored in the database.
A minor step up from MD5'ed passwords is to hash with SHA1 instead. The SHA1 has is longer, making the rainbow tables for SHA1 much bigger, and therefore reversing SHA1 passwords slightly more resource intensive.
The approach I want to take however, is a salted SHA1 hash. Salting a password adds some random data to the original password. The salt and the hashed salted password are then stored in the database. Salting passwords makes a rainbow table attack less practical because the rainbow table would need to contain hashes for all possible passwords and all possible salts (an exponentially bigger table). As no two users will have the same salt, the password hash stored for two users with the same password will also be different. Salted SHA1 passwords aren't impenetrable, but they're better than the alternatives discussed above, and it's what we'll use on this project.
Jim from 2021 here again: SHA-1 was broken and considered insecure in 2020. SHA-256 would the be minimum today. Or better yet, use PHP's native password hashing functions.
We'll use the "password" field in the database for storing both the salt and the
hashed password. We'll store them in the format "extract_salt
function doesn't find a hashed password in the password_hash property, it'll
create a new salt. Here's my extract_salt()
and generate_salt()
functions, place
these at the bottom of the UserModel
class before the closing }
:
private function extract_salt() {
if (strlen($this->password_hash) == 0) {
return $this->generate_salt();
} else {
$bits = explode('$', $this->password_hash);
if (count($bits) < 2) {
return $this->generate_salt();
} else {
return $bits[0];
}
}
}
private function generate_salt() {
return sha1(uniqid());
}
These functions are private because a controller or view should never need to
call them directly. We'll also need a function which can be used both to hash a
new password, and validate an existing password. My hash_password
function first
uses the extract_salt
method defined above to obtain a salt. Here we rely on the
fact that extract_salt
will generate a new salt if one doesn't already exist,
then we combine the salt and the plaintext password together and call the PHP
sha1()
function on the combination to get the hashed password. Finally it
returns both the salt and hashed password.
private function hash_password($plaintext) {
$salt = $this->extract_salt();
return $salt . '$' . sha1($salt.$plaintext);
}
Now to make the process more manageable, lets revisit the __get
and __set
magic
methods. First we'll add a read only password_salt
property to our users and
make allowance for a write only password
property:
function __get($var) {
switch ($var) {
case 'id':
return $this->_id;
case 'password_salt':
return $this->extract_salt();
case 'password':
throw new Exception('password is not a gettable property of UserModel');
}
}
Now we'll extend the __set
magic method and create a write only property which
can be used set a plain text password for the user without forcing the users of
the UserModel
class to understand how to properly salt a password.
function __set($var, $value) {
switch ($var) {
case 'password':
$this->password_hash = $this->hash_password($value);
default:
$this->$var = $value;
break;
}
}
Conclusion
At this point, we've got a fully functional model class for our users, including a simple method to maintain a salted password hash. Next time we'll put this model class to use and build a signup form, a login form and we'll enforce logins for feed reading.