Blogs  Work culture, passion and sharing ideas

User authentication with Community Auth in CodeIgniter 3 - A role based permission approach

Thou shall not pass. So, you've build a SaaS application in CodeIgniter and now direly in need of a rock solid user authentication and permission system for your web application.

User authentication and ACL's (Action Control Lists) are two very important features of MVC frameworks that not only narrows down your development hours but also helps built a robust authentication system without worrying about security pitfalls.

Sadly it's a feature that should have been intrinsically built into CodeIgniter but it's not. Unlike other MVC or HMVC frameworks based on PHP like CakePHP, Laravel or Yii, CodeIgniter lags in many core functionalities. To name a few User Authentication, ACL, DB Migration, Theming, Scaffolding, CLI interface, RESTful API services, dynamic generation of Models, Controllers and Views and the list can go on, but we're not here to rant about CodeIgniter's shortcomings it's a great framework for any development size.

Community Auth by Robert B Gottier is an excellent User authentication and ACL library for CodeIgniter. It's the easiest drop-in third-party package that fits right into CodeIgniter and gives you the flexibility of creating robust user authentication and ACL permissions for your web application.

Assuming you have setup your default configurations in /myapp/config, have a workable database configuration and required knowledge to setup CodeIgniter third_party components we'll get directly to setting up a fresh CodeIgniter install and using the Community Auth built in shell script to setup all the files required. Fire up your terminal and clone the reporsitory using git clone https://bitbucket.org/skunkbad/community-auth-for-codeigniter-3.git to your application/third_party folder or you can also download it from Community Auth Repo and place it in your application/third_party folder.

Back into terminal and cd /myapp/application to your application directory, this is very important as Community Auth will copy required installation files to your application folder. Now execute the shell script with sh ./third_party/community_auth/sh/.install.sh. Depending on your system file/folder preferences this file may be hidden. If everything went right you'll see these files created in your application folder:

  1. /myapp/application/controllers/Examples.php
  2. /myapp/application/controllers/Key_creator.php
  3. /myapp/application/core/MY_Controller.php
  4. /myapp/application/core/MY_Input.php
  5. /myapp/application/core/MY_Model.php
  6. /myapp/application/hooks/auth_constants.php
  7. /myapp/application/hooks/auth_sess_check.php
  8. /myapp/.htaccess

Next import Community Auth database tables in your database. You may have to sudo-in and cd /myapp/application/third_party/community_auth/sql and import tables into your database with:

mysql -u root -p root myappdb < install.sql

You can read full documentation about Community Auth configuration at Community Auth Docs but this should get you started. Finally, it's ready to be tested, fire up your browser and browse to http://localhost/myapp/examples and you'll see the login page.

Community Auth manages ACL's by categories (human readable permission names), actions (actions or methods) and finally records (the action linked to user id). According to Community Auth

"The idea of user permissions based on user roles is little extreme for larger sites, instead implementing ACL's for a single role may achieve the same thing"

Community Auth itself does not offer out-of-the box interface for ACL management and requires custom solution. In a time constraint project it's much easier to setup permissions based on user roles rather than role based ACL's. Although, I would still encourage using ACL's as recommended by Community Auth.

ACL's or User permissions do not offer authentication so first we'll need to check for user authentication before anything is accessed. The best place to start is your MY_Controller.php which is extended using Auth_Controller.php class. Your MY_Controller.php should look like this:

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

/**
 * Include Auth Controller to your MY_Controller and extend your MY_Controller
 * from Auth_Controller.
*/

require_once APPPATH . 'third_party/community_auth/core/Auth_Controller.php';

class MY_Controller extends Auth_Controller {

  protected $data = array();

  protected $permissions, $role_permissions = null;

  function __construct() {

    parent::__construct();

    // Get user permissions from config file
    $this->permissions = $this->config->item('user_role_permissions');

    /**
     * This will check if a user is logged in or not.
     * If a user is not logged in than he will be redirected to login page.
     * Enforcing security before any of the controllers/methods are called.
     */

    if ($this->auth_role == null) {
      switch($this->router->method){
        case 'login':
        case 'logout':
        case 'recover':
        case 'recovery_verification':
          if ($this->router->class == 'myauth'){
            break;
          }
        default:
          $this->require_min_level(1);
      }
    }
  }

  function post_auth_hook() {

    // Don't even bother going further if nobody is logged in or its a login, logout method
    if ($this->auth_role == null || $this->router->method == 'login' || $this->router->method == 'logout' || $this->router->method == 'recover' || $this->router->method == 'recovery_verification') {
      return parent::post_auth_hook();
    }

    // For admin everything is graceland!
    if ($this->auth_role <> null && $this->auth_role == 'admin') {
      return parent::post_auth_hook();
    }

    // Get current role permissions
    $this->role_permissions = $this->get_role_permissions();

    // Check if a controller is allowed
    if ($this->is_controller_allowed() == false) {
      $this->show_permission_denied();
    }

    // Check if a controller/method is allowed
    if ($this->is_controller_method_allowed() == false) {
      $this->show_permission_denied();
    }

    return parent::post_auth_hook(); // TODO: Change the autogenerated stub
  }

  /**
   * Checks if current user role can access a Controller
   *
   * @return bool
   */

  private function is_controller_allowed() {
    // Gets all methods in current router class if nothing is found than all methods are disabled for current role
    $controller_arr_flipped     = array_flip(array($this->router->class)); // Avoid Error @ Only variables should be passed by reference
    $controller_arr_intersected = array_intersect_key($this->role_permissions, $controller_arr_flipped); // Avoid Error @ Only variables should be passed by reference

    // If no controller was defined for a role than return false
    if (empty($controller_arr_intersected) || !is_array($controller_arr_intersected)) {
      return false;
    }
    // Its allowed if we found it! Note that this only checks for existence of a class/controller and not method.
    // To check both controller/method use is_controller_method_allowed()
    return true;
  }

  /**
   * Checks if current user role can access a Controller/Method
   *
   * @return bool
   */

  private function is_controller_method_allowed() {
    // Gets all methods in current router class if nothing is found than all methods are disabled for current role
    $controller_arr_flipped     = array_flip(array($this->router->class)); // Avoid Error @ Only variables should be passed by reference
    $controller_arr_intersected = array_intersect_key($this->role_permissions, $controller_arr_flipped); // Avoid Error @ Only variables should be passed by reference
    $permissible_methods        = array_shift($controller_arr_intersected);

    // If no controller/methods were defined for a role than return false
    if (empty($permissible_methods) || !is_array($permissible_methods)) {
      return false;
    }

    // Return TRUE or FALSE based on if a method is found in $permissible_methods array with router method
    return in_array($this->router->method, $permissible_methods);
  }

  /**
   * Returns current users role permissions. Only methods!
   *
   * @return mixed
   */

  private function get_role_permissions() {
    // Get current roles permissions
    $role_arr_flipped     = array_flip(array($this->auth_role)); // Avoid Error @ Only variables should be passed by reference
    $role_arr_intersected = array_intersect_key($this->permissions, $role_arr_flipped);
    $role_perms           = array_shift($role_arr_intersected);
    return $role_perms;
  }

  public function show_permission_denied() {
    $message = "
User {$this->auth_username} does not have permission to execute this operation.<br/> Please contact <cite>admin</cite> for more!
<br /><hr>
<button onclick='javascript:window.history.go(-1);'>Go Back</button>
"
;
    show_error($message, 500, 'Permission Denied');
    exit(4);
  }

}
         

The MY_Controller.php class construct checks for controllers and actions that should be allowed without authentication but if you require no access zone than just use $this->require_min_level(1) after the parent construct. The use of $this->require_min_level(1) automatically checks for user authentication and redirects them to default url setting inside Community Auth's authentication.php for reference see $config['default_login_redirect'] = 'pages/index'; setting.

To define actual permissions I have used a user permissions configuration file in /myapp/config/permissions.php which holds an associative array defining user roles, controllers and actions. This configuration file should be added to your autoload.php. It may be a good idea to add these roles inside Community Auth's authentication.php configuration file and set relevant integer values for each role. If a controller is not specified in permissions.php than it will be inaccessible even after authentication with an exception for admin user. You have to explicitly define each controller and its methods. So, my permissions.php looks like this:

<?php
/**
 * @package        permissions.php
 * @subpackage     MyApp
 * @author         Anirudh K. Mahant
 * @created        09/05/2016 - 9:30 PM
 * @license        Creative Commons 3.0 Attribution
 * @licenseurl    https://creativecommons.org/licenses/by/3.0/us/
 * @desc           User Permissions Configuration
 * @link           https://www.ravendevelopers.com
 */


// TABLE TAKES REFRENCES FROM authentication.php IN community_auth CONFIG FILE
$config['user_role_permissions'] = array(
  'manager' => array(
    'dashboard'        => array('index'),
    'customer'         => array('index', 'add', 'edit', 'remove'),
    'orders'           => array('index', 'add', 'edit', 'remove'),
    'myauth'           => array('login', 'logout', 'recover', 'recovery_verification'),
  ),
  'staff' => array(
    'dashboard'        => array('index'),
    'customer'         => array('index', 'add', 'edit', 'remove'),
    'orders'           => array('index', 'add'), // Removed 'edit', 'remove' actions
    'myauth'           => array('login', 'logout', 'recover', 'recovery_verification'),
  ),
  'operator' => array(
    'dashboard'        => array('index'),
    'customer'         => array('index', 'add'), // Removed 'edit', 'remove' actions
    'orders'           => array('index'), // Removed 'add', 'edit', 'remove' actions
    'myauth'           => array('login', 'logout', 'recover', 'recovery_verification'),
  )
);

Here I have demonstrated three roles manager, staff and operator where dashboard, customer, orders and myauth are controllers as keys and actions or methods as values.

Also note the usage of myauth which extends MY_Controller which is responsible for all authentication and password recovery actions and they should be allowed without authentication or permission checks and it must be defined for each role in permissions configuration file. In Community Auth this controller is defined as Example class in /myapp/controllers/Example.php file.

So there you have it! You have a working role based user authentication and user permission system.

Some useful links to read about the comparison:

Copyrights © 2007-2017 Raven Developers. All rights reserved.

Powered by: Drupal 7 and Foundation 4

preliminary