Building a Complete CodeIgniter Application: Part 5
Jim O'Halloran • September 5, 2010
php codeigniter feedignitionIntroduction
In our last part we built a model class for our users, and laid the groundwork, with libraries for sessions and filters. So here we go with User Authentication the other bit...
Signup Form
In order to use our feed reader, users will need a user account on the site. We'll let the users sign up for their own accounts through a simple signup form. Given that we'll need user accounts later on for the login and logout functionality, it probably makes sense to start here.
We're going to need a new controller and a view for the signup form itself. I
like to group related functions together into a single controller, so we'll
create one controller which handles Signup, Login and Logout. These are all user
related functions, so we'll call our controller class User
. Create a file
called user.php
in system/application/controllers
with the following contents.
<?php if (!defined('BASEPATH')) exit('No direct script access allowed');
class User extends Controller {
function __construct() {
parent::Controller();
$this->load->model('UserModel');
}
}
?>
I've elected to load the UserModel
in the controller's constructor because
we'll need it in every controller method. Doing it in the constructor saves us
from having to remember to do it in each controller method.
Now we need a signup form. First, lets create a view containing the form with
the fields we need. I use the same controller method to handle the initial form
display and it's submission, so I've included a hidden field called submitted
which makes it easy to tell the difference. There are other ways of doing this,
but this is the easiest. Create a new file called
system/application/views/user/signup.php
, and place the following into it:
<?php $this->load->view('header', array('title'=>'New User Signup')); ?>
<h1>Signup</h1>
<?php if ($this->validation->error_string != '') { ?>
<ul class="error"><?= $this->validation->error_string ?></ul>
<?php } ?>
<form action="?" method="POST">
<input type="hidden" name="submitted" value="true">
<p><label for="username">Username:</label><input type="text" name="username" size="10" value="<?=htmlspecialchars($this->validation->username);?>"></p>
<p><label for="password1">Password:</label><input type="password" name="password1" size="10" value=""></p>
<p><label for="password2">Confirm Password:</label><input type="password" name="password2" size="10" value=""></p>
<p><label for="email">Email Address:</label><input type="email" name="email" size="20" value="<?=htmlspecialchars($this->validation->email);?>"></p>
<p><label for="first_name">First Name:</label><input type="text" name="first_name" size="20" value="<?=htmlspecialchars($this->validation->first_name);?>"></p>
<p><label for="last_name">Last Name:</label><input type="text" name="last_name" size="20" value="<?=htmlspecialchars($this->validation->last_name);?>"></p>
<p><input type="submit" value="Register"></p>
</form>
<?php $this->load->view('footer'); ?>
We'll apply some CSS later to align the fields in neat columns, and tidy up the form, but that's all we need to achieve the desired functionality. We'll use the CI Validation library to ensure all of the required data is supplied, and there's provision at the top of the form to display the CI error messages. We'll also re-populate the fields with the previous data using the validator.
Now lets build the controller method which will make this form work. The basic tasks are as follows:
- Load the validation library and set up the validation rules for the form.
- Determine whether the form has been submitted, if it hasn't display the signup view.
- If the form has been submitted validate it. If it fails validation, display the form again.
- If the form passes validation assign the form values to the UserModel we created last time and save it.
- Add the user's information to the session to indicate they've been logged in, then redirect them back to the home page.
The code to achieve all of that is as follows:
function signup() {
$this->load->library('validation');
$this->validation->set_rules(array(
'username' => 'trim|required|min_length[3]|max_length[20]',
'password1' => 'trim|required|matches[password2]',
'password2' => 'trim|required',
'email' => 'trim|min_length[2]|max_length[100]|valid_email',
'first_name' => 'trim|min_length[2]|max_length[50]',
'last_name' => 'trim|min_length[2]|max_length[50]',
));
$this->validation->set_fields(array(
'username' => 'Username',
'password1' => 'Password',
'password2' => 'Confirm Password',
'email' => 'Email Address',
'first_name' => 'First Name',
'last_name' => 'Last Name',
));
$this->validation->set_error_delimiters('<li>', '</li>');
if ($this->input->post('submitted') === false) {
// Not submitted
$this->load->view('user/signup');
} else {
// Submitted
if ($this->validation->run()) {
$this->UserModel->username = $this->validation->username;
$this->UserModel->password = $this->validation->password1;
$this->UserModel->email = $this->validation->email;
$this->UserModel->first_name = $this->validation->first_name;
$this->UserModel->last_name = $this->validation->last_name;
$this->UserModel->save();
$this->UserModel->set_session();
redirect('');
} else {
$this->load->view('user/signup');
}
}
}
The validation rules were carefully chosen to: 1. Require only the absolutely essential fields, making the fields which are "nice to have" such as name and email address optional. 1. Clean whitespace from the input with the trim function. 1. Ensure password1 and password2 fields contained the same values. 1. Ensure the data that is entered isn't longer than the database fields that is meant to contain it.
The individual rules are described in the CI documentation, but their purpose
is fairly self explanatory. I've added a new method to the UserModel
class which
is used to assign the user details to the session. This new function contains
the following:
public function set_session() {
$this->session->set_userdata('user', array(
'id' => $this->_id,
'username' => $this->username,
'first_name' => $this->first_name,
'last_name' => $this->last_name
));
}
This function simply uses the session library to create a session variable
called user
which contains an array with the key fields from the user table.
We'll use the presence of this user variable in the session to denote a
successful login.
Log Out Function
The logout function doesn't really require any user interface, so we don't need a view for logout. I've said before that the presence of the "user" variable in the session will indicate a successful login, so logging out means we just need to do two things. First, we need to remove the user variable from the session, then we need to redirect to a page which we know doesn't require the user to be logged in (e.g. the login form itself). In our user controller, create the following function:
function logout() {
$this->session->unset_userdata('user');
redirect('user/login');
}
Pretty self-explanatory really.
Login Form
The basic structure of the login function is the same as the sign up form. First
we initialize the form, check to see if the form was submitted, validate it and
log the user in. We'll need a view, so create a new file in
system/application/views/user
called login.php
and place the following into it:
<?php $this->load->view('header', array('title'=>'Login')); ?>
<h1>Signup</h1>
<?php if ($this->validation->error_string != '') { ?>
<ul class="error"><?= $this->validation->error_string ?></ul>
<?php } ?>
<?php if ($auth_fail) { ?>
<p class="error">Incorrect username or password.</p>
<?php } else {?>
<p>Please enter your username and password to login.</p>
<?php } ?>
<form action="?" method="POST">
<input type="hidden" name="submitted" value="true">
<p><label for="username">Username:</label><input type="text" name="username" size="10"></p>
<p><label for="password">Password:</label><input type="password" name="password" size="10"></p>
<p><input type="submit" value="Login"></p>
</form>
<?php $this->load->view('footer'); ?>
Again the structure should be recognisable from the signup form. We'll use CSS to tidy it up later on, but for now it'll work but look a bit messy.
We'll also need a controller method to drive the process. Add the following to the User controller:
function login() {
$this->load->library('validation');
$this->validation->set_rules(array(
'username' => 'trim|required',
'password' => 'trim|required',
));
$this->validation->set_fields(array(
'username' => 'Username',
'password' => 'Password',
));
$this->validation->set_error_delimiters('<li>', '</li>');
if ($this->input->post('submitted') === false) {
// Not submitted
$this->load->view('user/login', array('auth_fail' => false));
} else {
// Submitted
if ($this->validation->run()) {
$logged_in = $this->UserModel->authenticate($this->validation->username, $this->validation->password);
if ($logged_in) {
redirect('');
} else {
$this->load->view('user/login', array('auth_fail' => true));
}
} else {
$this->load->view('user/login', array('auth_fail' => false));
}
}
}
Actually processing the login requires that we do a few things:
- Retrieve the user record with the supplied username from the database.
- Obtain the current password salt.
- Salt and hash the supplied password.
- Compare the result with the one stored against the user record.
Given that this requires a database query, and I prefer to keep SQL contained within my models. We'll add an "authenticate" function to the model which will do the heavy lifting. The authenticate function will return a boolean indicating whether the login as successful, and if it is it'll also register the "user" session variable.
public function authenticate($username, $password) {
$found = $this->load($username);
if ($found) {
if ($this->hash_password($password) == $this->password_hash) {
$this->set_session();
return true;
}
}
return false;
}
The controller method posted above, uses this method already, so we've not got a working login system.
Enforcing Logins
There's one element missing now from our User Authentication system. If a user knows the URL for a page which is protected by login, they can bypass the login form and go directly to it. We need to close that hole, and this is where the filters system we put in place in part 4 comes into play.
Creating a filter is a two step process. First a configuration entry needs to be added specifying where the controller is to be used. Next a class needs to be created defining the actions a filter is to take.
Using the system/application/config/filters.php
file we can configure which
filters are attached to which URLs. This can be done on either an inclusive
(filter just specified controller methods) or exclusive (filter everything
except) basis. Lets create a filter called "auth" which will apply to everything
except the sign up, login and logout methods. Add the following to
system/application/config/filters.conf
after the existing filters and before the
closing PHP tag.
$filter['auth'] = array(
'exclude', // Apply login filter to all pages except login and support pages.
array('user/login,logout,signup'),
);
Next we need to create the filter class itself. A filter class is a class
consisting of two functions. before()
is run after the controller's constructor,
and after()
which is run after the appropriate controller method. We'll only need
the before()
method, although we'll need to define both just to keep the filters
system happy. You'll need to create a file called
system/application/filters/auth.php
, and add the following content:
<?php if (!defined('BASEPATH')) exit('No direct script access allowed');
class Auth_filter extends Filter {
function before() {
if (!function_exists('get_instance')) return "Can't get CI instance";
$CI= &get_instance();
$success = false;
$user= $CI->session->userdata('user');
if ($user === false) {
redirect("/user/login");
}
}
function after() {
}
}
?>
This function uses the CI function get_instance
to obtain a reference to the
core CI object. The CI object is used to obtain the user variable from the
session. If the user variable doesn't exist in the session the user is
redirected to the login page, otherwise the controller method is allowed to run.
Conclusion
Our application now has a very basic user authentication system. Users can sign up, login and log out. We're still lacking the ability to actually subscribe to feeds, and our application is still pretty ugly. The user authentication scheme could also use some work, a lost password function would be good, and a "remember me" option on the login would also be a good thing. Plenty more work to do, but there series isn't over yet.
Spoiler Alert (2021) I never wrote a Part 6. So I guess it did end here.