CodeIgniter Authentication

Introduction

The internet is a double edged sword. We love web apps because everyone can access them. This advantage also puts us at security risks due to the factor that anyone can access our web apps. Authentication is used to ensure that only authorized users are permitted to access certain sections of the web application.
If only it were that simple.
Hackers can exploit authentication implementation weaknesses to gain unauthorized access.
This tutorial lays a strong foundation that will help you implement a better and stronger authentication system that will make it difficult for attackers to gain unauthorized entry. The tutorial starts with the theory followed by the implementation. If you are already familiar with the theoretical aspect and authentication best practices then you can skip the theory and go directly to the practical implementation.
Laravel Blog

Topics to be covered

We will cover the following topics in this tutorial
  • Tutorial pre-requisites
  • What is authentication?
  • User authentication best practices
  • Factors to consider when choosing an authentication library
  • Ion Auth
  • CodeIgniter Authentication Example (Tutorial Project)

Tutorial Pre-requisites

This tutorial assumes you are familiar with;
  • PHP basics and Object-oriented programming
  • CodeIgniter Basics
  • You have a web server, PHP and MySQL already configured and running
  • You have a cool IDE i.e. NetBeans IDE, Aptana Studio etc.
  • You have access to a command line/terminal that you can use to run commands
  • You have composer. Composer is a PHP dependencies manager.
  • You have been following the tutorial series [optional but highly recommended]

What is authentication?

Authentication is the process of determining whether someone or something is who they claim they are. The common convention when authenticating users is using a combination of either an email address or username and password. If the email address or username and password combination match with what the system has in its database, then access to the system is granted. If the combination does not match, access to the system is denied.

User authentication best practices

Weaknesses in authentication implementation can be exploited to gain access to the system. This section briefly discusses some of the factors that you should consider to avoid common mistakes
  1. Hash Password – a password hash is an encrypted password that cannot be decrypted. This comes in handy if an attacker managers to gain access to the database. At least they won’t be able to know what the password is right away.
  2. Avoid using MD5 as the hashing algorithm – MD5 is a fast algorithm. It is very easy to use brute force to crack the hashed passwords
  3. Hash passwords using a slow algorithm – this is huge, algorithms such as bcrypt are slow and require a lot of computing power to use brute force to crack the hashed passwords.
  4. Set the minimum password characters – the standard limit is usually 8 characters as the minimum. Short passwords can be easily cracked compared to longer ones
  5. Enforce password complexity – Ensure that all passwords must at least contain special characters, numbers, and a combination of lower and upper case letters.
  6. Secure Socket Layer (SSL) – SSL encrypts the communication and makes it difficult for hackers to eavesdrop on the network. SSL uses https protocol. For ordinary applications such as blogs you can usually get away with http but if you are developing an application with sensitive data then you should serious consider using SSL.
  7. User roles – not all users are equal. Administrators can create new accounts and give privileges to users. Ordinary users should not be able allowed to perform such tasks.

Factors to consider when choosing an authentication library

A library is a set of functions that perform specialized tasks. CodeIgniter has a number of open source authentication libraries written by third parties. It is possible to write your own authentication system but why re-invent the wheel when you can use existing solutions?
The following points should guide you when selecting an authentication library.
  1. Loosely coupled – the library should not be too depend on the framework. Some of the authentication libraries force you to build the application on top of them. It’s better to avoid such libraries.
  2. Active development – most libraries are hosted on online repositories such as GitHub. If the library hasn’t been updated in years then you must avoid it. Why should you? Because attackers may have discovered vulnerabilities in the library and if the vulnerabilities weren’t fixed then your application becomes vulnerable to attacks.
  3. Implements authentication best practices – the library shouldn’t necessary implement all of the best practices listed above but at a minimum, it should be able to implement the best practices that meet your requirements.
  4. Compatibility with framework version – If you are working with the latest framework version and the library was tested with an old version, then you should test the library with the latest framework and look for common security flaws.

Ion Auth

Ion Auth is a simple and lightweight CodeIgniter Authentication library. It was created by Ben Edmunds and its online repository is on GitHub. The online documentation for Ion Auth can be found on http://benedmunds.com/ion_auth/
Ion Auth works with CodeIgniter 3.*

CodeIgniter Authentication Example (Tutorial Project)

Throughout these tutorial series, we have been working with a tutorial project and we will implement the authentication part. Download Ion Auth from the GitHub Repository Unzip the contents of the download. Ion Auth has the following directories that correspond to the directories in the application directory for CodeIgniter.
  • config – this directory contains ion_auth.php config file
  • controllers – contains a sample controller auth.php
  • language – Ion Auth supports multiple languages and comes with a number of default language settings
  • libraries – Ion Auth uses bcrypt for authentication. This directory contains the library file for bcrypt and ion_auth itself.
  • migrations – if you prefer managing the database using migrations then you can use the files in the directory.
  • models – contains a sample model that interacts with the database.
  • sql – contains SQL statements for creating the default tables that Ion Auth requires. You should only use the files if you don’t like using migrations.
  • userguide – contains the documentation for ion auth
  • views – contains the sample views for activities such as login, signup, and registration forms.
Copy the unzipped contents into the application directory of ci-my-admin

Ion Auth Migrations

The migration file starts with 001. CodeIgniter expects a valid timestamp as the prefix for the migration file and we changed the default migration directory to /database/migrations.
Open the command prompt / terminal
Browser to the web root of our application

cd "C:\xammp\htdocs\ci-my-admin"
Run the following command to create a new migration file for Ion Auth.

php index.php tools migration Ion_Auth
Open /application/database/migrations/20151109080812_Ion_Auth.php
Modify the code to the following

<?php defined('BASEPATH') OR exit('No direct script access allowed');

class Migration_Ion_auth extends CI_Migration {

    public function up()
    {
        // Drop table 'groups' if it exists
        $this->dbforge->drop_table('groups', TRUE);

        // Table structure for table 'groups'
        $this->dbforge->add_field(array(
            'id' => array(
                'type' => 'MEDIUMINT',
                'constraint' => '8',
                'unsigned' => TRUE,
                'auto_increment' => TRUE
            ),
            'name' => array(
                'type' => 'VARCHAR',
                'constraint' => '20',
            ),
            'description' => array(
                'type' => 'VARCHAR',
                'constraint' => '100',
            )
        ));
        $this->dbforge->add_key('id', TRUE);
        $this->dbforge->create_table('groups');

        // Dumping data for table 'groups'
        $data = array(
            array(
                'id' => '1',
                'name' => 'admin',
                'description' => 'Administrator'
            ),
            array(
                'id' => '2',
                'name' => 'members',
                'description' => 'General User'
            )
        );
        $this->db->insert_batch('groups', $data);


        // Drop table 'users' if it exists
        $this->dbforge->drop_table('users', TRUE);

        // Table structure for table 'users'
        $this->dbforge->add_field(array(
            'id' => array(
                'type' => 'MEDIUMINT',
                'constraint' => '8',
                'unsigned' => TRUE,
                'auto_increment' => TRUE
            ),
            'ip_address' => array(
                'type' => 'VARCHAR',
                'constraint' => '16'
            ),
            'username' => array(
                'type' => 'VARCHAR',
                'constraint' => '100',
            ),
            'password' => array(
                'type' => 'VARCHAR',
                'constraint' => '80',
            ),
            'salt' => array(
                'type' => 'VARCHAR',
                'constraint' => '40'
            ),
            'email' => array(
                'type' => 'VARCHAR',
                'constraint' => '100'
            ),
            'activation_code' => array(
                'type' => 'VARCHAR',
                'constraint' => '40',
                'null' => TRUE
            ),
            'forgotten_password_code' => array(
                'type' => 'VARCHAR',
                'constraint' => '40',
                'null' => TRUE
            ),
            'forgotten_password_time' => array(
                'type' => 'INT',
                'constraint' => '11',
                'unsigned' => TRUE,
                'null' => TRUE
            ),
            'remember_code' => array(
                'type' => 'VARCHAR',
                'constraint' => '40',
                'null' => TRUE
            ),
            'created_on' => array(
                'type' => 'INT',
                'constraint' => '11',
                'unsigned' => TRUE,
            ),
            'last_login' => array(
                'type' => 'INT',
                'constraint' => '11',
                'unsigned' => TRUE,
                'null' => TRUE
            ),
            'active' => array(
                'type' => 'TINYINT',
                'constraint' => '1',
                'unsigned' => TRUE,
                'null' => TRUE
            ),
            'first_name' => array(
                'type' => 'VARCHAR',
                'constraint' => '50',
                'null' => TRUE
            ),
            'last_name' => array(
                'type' => 'VARCHAR',
                'constraint' => '50',
                'null' => TRUE
            ),
            'company' => array(
                'type' => 'VARCHAR',
                'constraint' => '100',
                'null' => TRUE
            ),
            'phone' => array(
                'type' => 'VARCHAR',
                'constraint' => '20',
                'null' => TRUE
            )

        ));
        $this->dbforge->add_key('id', TRUE);
        $this->dbforge->create_table('users');

        // Dumping data for table 'users'
        $data = array(
            'id' => '1',
            'ip_address' => '127.0.0.1',
            'username' => 'administrator',
            'password' => '$2a$07$SeBknntpZror9uyftVopmu61qg0ms8Qv1yV6FG.kQOSM.9QhmTo36',
            'salt' => '',
            'email' => 'admin@admin.com',
            'activation_code' => '',
            'forgotten_password_code' => NULL,
            'created_on' => '1268889823',
            'last_login' => '1268889823',
            'active' => '1',
            'first_name' => 'Admin',
            'last_name' => 'istrator',
            'company' => 'ADMIN',
            'phone' => '0',
        );
        $this->db->insert('users', $data);


        // Drop table 'users_groups' if it exists
        $this->dbforge->drop_table('users_groups', TRUE);

        // Table structure for table 'users_groups'
        $this->dbforge->add_field(array(
            'id' => array(
                'type' => 'MEDIUMINT',
                'constraint' => '8',
                'unsigned' => TRUE,
                'auto_increment' => TRUE
            ),
            'user_id' => array(
                'type' => 'MEDIUMINT',
                'constraint' => '8',
                'unsigned' => TRUE
            ),
            'group_id' => array(
                'type' => 'MEDIUMINT',
                'constraint' => '8',
                'unsigned' => TRUE
            )
        ));
        $this->dbforge->add_key('id', TRUE);
        $this->dbforge->create_table('users_groups');

        // Dumping data for table 'users_groups'
        $data = array(
            array(
                'id' => '1',
                'user_id' => '1',
                'group_id' => '1',
            ),
            array(
                'id' => '2',
                'user_id' => '1',
                'group_id' => '2',
            )
        );
        $this->db->insert_batch('users_groups', $data);


        // Drop table 'login_attempts' if it exists
        $this->dbforge->drop_table('login_attempts', TRUE);

        // Table structure for table 'login_attempts'
        $this->dbforge->add_field(array(
            'id' => array(
                'type' => 'MEDIUMINT',
                'constraint' => '8',
                'unsigned' => TRUE,
                'auto_increment' => TRUE
            ),
            'ip_address' => array(
                'type' => 'VARCHAR',
                'constraint' => '16'
            ),
            'login' => array(
                'type' => 'VARCHAR',
                'constraint' => '100',
                'null', TRUE
            ),
            'time' => array(
                'type' => 'INT',
                'constraint' => '11',
                'unsigned' => TRUE,
                'null' => TRUE
            )
        ));
        $this->dbforge->add_key('id', TRUE);
        $this->dbforge->create_table('login_attempts');

    }

    public function down()
    {
        $this->dbforge->drop_table('users', TRUE);
        $this->dbforge->drop_table('groups', TRUE);
        $this->dbforge->drop_table('users_groups', TRUE);
        $this->dbforge->drop_table('login_attempts', TRUE);
    }
}
If you are not familiar with the concept of migrations then I recommend you read this tutorial CodeIgniter Migration.
Run the following command to execute the migration

php index.php tools migrate
The migration file will create the following tables
  1. users – creates the users table
  2. groups – creates the groups table. Groups are used to assign roles to users.
  3. users_groups – the relationship between users and groups is many to many. This is the intermediate table that links users and groups
  4. login_attemps - records login information.
The migration file also creates a default user admin@admin.com and the default password is password.

Auth Controller

Let’s now modify the code for the controller
Open /application/modules/auth/controllers/auth.php
Modify the code to the following

<?php

if (!defined('BASEPATH'))
    exit('No direct script access allowed');

class Auth extends MY_Controller {

    function __construct() {
        parent::__construct();
        $this->load->database();
        $this->load->library(array('ion_auth', 'form_validation'));
        $this->load->helper(array('url', 'language'));

        $this->form_validation->set_error_delimiters($this->config->item('error_start_delimiter', 'ion_auth'), $this->config->item('error_end_delimiter', 'ion_auth'));

        log_message('debug', 'CI My Admin : Auth class loaded');
    }

    public function index() {
        if ($this->ion_auth->logged_in()) {
            redirect('admin/dashboard', 'refresh');
        } else {
            $data['page'] = $this->config->item('ci_my_admin_template_dir_public') . "login_form";
            $data['module'] = 'auth';

            $this->load->view($this->_container, $data);
        }
    }

    function login() {
        $this->form_validation->set_rules('email', 'Email', 'required');
        $this->form_validation->set_rules('password', 'Password', 'required');

        if ($this->form_validation->run() == true) {
            $remember = (bool) $this->input->post('remember');

            if ($this->ion_auth->login($this->input->post('email'), $this->input->post('password'), $remember)) {
                $this->session->set_flashdata('message', $this->ion_auth->messages());
                redirect('/admin/dashboard', 'refresh');
            } else {
                $this->session->set_flashdata('message', $this->ion_auth->errors());
                redirect('auth', 'refresh');
            }
        } else {
            $this->session->set_flashdata('message', $this->ion_auth->errors());
            (validation_errors()) ? validation_errors() : $this->session->flashdata('message');

            $data['page'] = $this->config->item('ci_my_admin_template_dir_public') . "login_form";
            $data['module'] = 'auth';
            //$data['message'] = $this->data['message'];

            $this->load->view($this->_container, $data);
        }
    }

    public function logout() {
        $this->ion_auth->logout();

        redirect('auth', 'refresh');
    }

}
HERE,
  • function __construct() {} the constructor method loads the required libraries, helpers etc. It also loads ion_auth
  • public function index() {} the index method is executed when the user requests http://localhost/ci-my-admin/auth . If the user is already logged in, they are redirected to the dashboard.
  • public function login() {} defines the function that is called when a user submits login credentials. If the login is successful, the user is redirected to the dashboard, else the user id redirected to the login page and an error message is displayed.
  • public function logout() {} logs out the user and redirects them to the login page

Protected admin sections

We want to protect the following areas of the admin panel
  • Dashboard
  • Brands
  • Categories
  • Products
We have created separate controllers for the above sections and each section has a number of URLs that we need to protect. We will use the constructor method for the respective controllers to check if a user is logged in before granting access. The advantage of this approach is you avoid repeating yourself. All the functions in the class will be protected.
As an example, open /application/modules/admin/controllers/admin.php
Modify the class constructor to the following

function __construct() {
    parent::__construct();

    $this->load->library(array('ion_auth'));

    if (!$this->ion_auth->logged_in()) {
        redirect('/auth', 'refresh');
    }
}
HERE,
  • $this->load->library(array('ion_auth')); loads ion_auth library
  • if (!$this->ion_auth->logged_in()) {} checks if the user is not logged. If the user is not logged in, he/she is redirected to the login page.
Add the above code snippet to the controllers for brands, categories and products.

Login View

Initially, the login page was just loading the dashboard. We will need to submit the login credentials to auth/login using the HTTP POST verb.
Open /application/modules/auth/views/public/themes/default/login_form.php
Modify the code to the following

<div class="container">
    <div class="row">
        <div class="col-md-4 col-md-offset-4">
            <div class="login-panel panel panel-default">
                <div class="panel-heading">
                    <h3 class="panel-title">Please Sign In</h3>
                </div>
                <div class="panel-body">
                    <?php if ($this->session->flashdata('message')): ?>
                        <div class="alert alert-danger fade in">
                            <a href="#" class="close" data-dismiss="alert">&times;</a>
                            <?= $this->session->flashdata('message') ?>
                        </div>
                    <?php endif; ?>
                    <form role="form" method="POST" action="<?= base_url('auth/login') ?>">
                        <fieldset>
                            <div class="form-group">
                                <input class="form-control" placeholder="E-mail" name="email" type="email" autofocus>
                            </div>
                            <div class="form-group">
                                <input class="form-control" placeholder="Password" name="password" type="password" value="">
                            </div>
                            <div class="checkbox">
                                <label>
                                    <input name="remember" type="checkbox" value="Remember Me">Remember Me
                                </label>
                            </div>
                            <!-- Change this to a button or input when using this as a form -->
                            <button class="btn btn-lg btn-success btn-block" type="submit">Login</button>
                        </fieldset>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
We now need to modify the logout link for the main layout.
Open /application/views/admin/themes/default/header.php
Locate the link for logout and modify it to the following

<li><a href="<?=  base_url('auth/logout')?>"><i class="fa fa-sign-out fa-fw"></i> Logout</a></li>
Downloaded the attached tutorial file to get the complete source code for this tutorial.

Summary

Ion auth is a powerful lightweight authentication library for CodeIgniter that simplifies authenticating users in CodeIgniter. In admin to authenticating users, Ion auth can also be used to manager user groups.