CakePHP: Authentication Setup on an Application

Exclusive offer: get 80% off this eBook here
CakePHP 1.3 Application Development Cookbook

CakePHP 1.3 Application Development Cookbook — Save 80%

Over 70 great recipes for developing, maintaining, and deploying web applications

₨739.00    ₨147.80
by Mariano Iglesias | March 2011 | Open Source

This article explains how to set up authentication on a CakePHP application. This is accomplished through the use of tools that are built into the framework core, which allow us to quickly set up secure areas without losing flexibility to build more complex solutions.

In this article by Mariano Iglesias, author of CakePHP 1.3 Application Development Cookbook, we will cover the following topics:

  • Setting up a basic authentication system
  • Using and configuring the Auth component
  • Allowing logins with e-mail or username
  • Saving the user details after login
  • Getting the current user's information
  • Using prefixes for role-based access control
  • Setting up Access Control Layer based authentication

 

CakePHP 1.3 Application Development Cookbook

CakePHP 1.3 Application Development Cookbook

Over 70 great recipes for developing, maintaining, and deploying web applications

        Read more about this book      

(For more resources on CakePHP, see here.)

Introduction

The first two recipes show us how to set up a basic, yet fully working authentication system. The next three recipes allow our users to log in using different information, have their user details saved after a successful login, and show us how to get this user information.

Setting up a basic authentication system

The first task to be completed when we are in the process of adding authentication to an application is to identify which controllers will need user access. Normally we would make every controller and action protected by default, and then we would specify which areas of our application allow public access.

Getting ready

We must have a users table that should contain, at least, two fields: username (to hold the username) and password (to hold a hash made out of the user's password).

If you don't have a table for this purpose, you can use the following SQL statement to create it:

CREATE TABLE `users`(
`id` INT UNSIGNED AUTO_INCREMENT NOT NULL,
`username` VARCHAR(255) NOT NULL,
`password` CHAR(40) NOT NULL,
PRIMARY KEY(`id`)
);

How to do it...

  1. Create a file named users_controller.php and place it inside your app/controllers folder with the following contents:

    <?php
    class UsersController extends AppController {
    public function login() {
    }
    public function logout() {
    $this->redirect($this->Auth->logout());
    }
    }
    ?>

  2. Create a file named login.ctp in your app/views/users folder (create the folder if you don't have one already), and add the following contents:

    <?php
    echo $this->Form->create(array('action'=>'login'));
    echo $this->Form->inputs(array(
    'legend' => 'Login',
    'username',
    'password'
    ));
    echo $this->Form->end('Login');
    ?>

  3. Create a file named app_controller.php in your app/ folder with the following contents:

    <?php
    class AppController extends Controller {
    public $components = array(
    'Auth' => array(
    'authorize' => 'controller'
    ),
    'Session'
    );
    public function isAuthorized() {
    return true;
    }
    }
    ?>

  4. Modify the UsersController, and add the following code before the login method:

    public function beforeFilter() {
    parent::beforeFilter();
    $this->Auth->allow('add');
    }
    public function add() {
    if (!empty($this->data)) {
    $this->User->create();
    if ($this->User->save($this->data)) {
    $this->Session->setFlash('User created!');
    $this->redirect(array('action'=>'login'));
    } else {
    $this->Session->setFlash('Please correct the
    errors');
    }
    }
    }

  5. Create a file named add.ctp and place it in your app/views/users folder with the following contents:

    <?php
    echo $this->Form->create();
    echo $this->Form->inputs(array(
    'legend' => 'Signup',
    'username',
    'password'
    ));
    echo $this->Form->end('Submit');
    ?>

We now have a fully working authentication system. We can add new users by browsing to http://localhost/users/add, logging in by browsing to http://localhost/users/login, and finally logging out by browsing to http://localhost/users/logout.

After creating a user, you should see the login form with a success message, as shown in the following screenshot:

CakePHP 1.3 Application Development Cookbook

How it works...

We start by creating two actions in the UsersController class: login(), to show and process submissions of the login form, and logout(), to handle users logging out.

You may be surprised that the login() method has no logic whatsoever. To display the form, all we need to do is display the action's view. The form submission is taken care of by the Auth component, leaving us with no need to implement any controller logic. Therefore, the only implementation we need is to create a view for this action, which includes a simple form with two fields: username, and password.

The inputs method of CakePHP's FormHelper is a shortcut designed to avoid multiple calls to the input method. By using it, we can create a full form with elements without the need to call FormHelper::input() several times.

The logout() controller action simply calls the Auth component's logout() method. This method removes the logged-in user data from the session, and returns the address to which the user should be redirected after logging out, obtained from the previously configured logoutRedirect setting of the component (defaults to the application's home page if the setting was not configured.)

Next, we add two components to the controller: Session, and Auth. The Session component is needed to create the messages (through the use of its setflash() method) that informs the user if a login attempt was unsuccessful, or if a user was created.

The Auth component operates between your controller's actions and the incoming request by means of the beforeFilter callback method. It uses it's authorize setting to check what type of authentication scheme is to be used.

Once the Auth component is added to a controller, all actions in that controller are not accessible unless there is a valid user logged in. This means that if we had any actions that should be public (such as the login() and add() actions in our controller), we would have to tell the Auth component about them.

If one wishes to make some actions public, one can add the name of these actions to the allowedActions setting of the Auth component, or by calling its allow() method. We use the later approach to tell the Auth component that the add() action should be reachable without a logged-in user. The login() action is automatically added to the list of public actions by the Auth component.

When the user attempts to reach an action that is not within the public actions, the Auth component checks the session to see if a user is already logged in. If a valid user is not found, it redirects the browser to the login action. If there is a user who is logged in, it uses the controller's isAuthorized method to check if the user has access. If its return value is true, it allows access, otherwise access is rejected. In our case, we implemented this method in AppController, our base controller class. If the attempted action requires a user who is logged in, the login() action is executed. After the user submits data using the login form, the component will first hash the password field, and then issue a find operation on the User model to find a valid account, using the posted username and password. If a valid record is found, it is saved to the session, marking the user as logged in.

Hashing a password confirmation field

When the Auth component is enabled on a controller and the user submits a form with a field named password (regardless if it is being rendered in the login form), the component will automatically hash the password field before executing the controller's action.

The Auth component uses the salt defined in the configuration setting Security.salt (in your app/config/core.php file) to calculate the hash. Different salt values will produce different hashes even when using the same password. Therefore, make sure you change the salt on all your CakePHP applications, thus enhancing the security of your authentication system.

This means that the action will never hold the plain password value, and this should be particularly noted when utilizing mechanisms to confirm password validations. When you are implementing such validation, make sure you hash the confirmation field using the proper method:

if (!empty($this->data)) {
$this->data['User']['confirm_password'] = $this->Auth-
>password($this->data['User']['confirm_password']);
// Continue with processing
}

Using and configuring the Auth component

If there is something that defines the Auth component, it is its flexibility that accounts for different types of authentication modes, each of these modes serving different needs. In this recipe, you will learn how to modify the component's default behavior, and how to choose between the different authentications modes.

Getting ready

We should have a fully working authentication system, so follow the entire recipe Setting up a basic authentication system.

We will also add support to have disabled user accounts. Add a field named active to your users table with the following SQL statement:

ALTER TABLE `users`
ADD COLUMN `active` TINYINT UNSIGNED NOT NULL default 1;

How to do it...

  1. Modify the definition of the Auth component in your AppController class, so it looks like the following:

    public $components = array(
    'Auth' => array(
    'authorize' => 'controller',
    'loginRedirect' => array(
    'admin' => false,
    'controller' => 'users',
    'action' => 'dashboard'
    ),
    'loginError' => 'Invalid account specified',
    'authError' => 'You don\'t have the right permission'
    ),
    'Session'
    );

  2. Now while still editing your app/app_controller.php file, place the following code right below the components property declaration, at the beginning of the beforeFilter method in your AppController class:

    public function beforeFilter() {
    if ($this->Auth->getModel()->hasField('active'))
    {$this->Auth->userScope = array('active' => 1);
    }
    }

  3. Copy the default layout from cake/libs/view/layouts/default.ctp to your app/views/layouts directory, and make sure you place the following line in your layout where you wish to display authentication messages:

    <?php echo $this->Session->flash('auth'); ?>

  4. Edit your app/controllers/users_controller.php file and place the following method right below the logout() method:
    public function dashboard() {
    }
  5. Finally, create the view for this newly added action in a file named dashboard.ctp and place it in your app/views/users folder with the following contents:
    <p>Welcome!</p>

If you now browse to http://localhost/users/login and enter the wrong credentials (wrong username and/or password), you should see the error message shown in the following screenshot:

CakePHP 1.3 Application Development Cookbook

How it works...

As the Auth component does its magic right before a controller action is executed, we either need to specify its settings in the beforeFilter callback, or pass them in an array when adding the component to the components property. A common place to do it is in the beforeFilter() method of the AppController class, as by doing so we can share the same authentication settings throughout all our controllers.

This recipe changes some Auth settings, so that whenever a valid user logs in, they are automatically taken to a dashboard action in the UsersController (done via the loginRedirect setting.) It also adds some default error messages through the component's respective settings: loginError for when the given account is invalid, and authError for when there is a valid account, but the action is not authorized (which can be achieved by returning false from the isAuthorized() method implemented in AppController.)

It also sets the component's userScope setting in AppController::beforeFilter(). This setting allows us to define which conditions the User find operation need to match to allow a user account to log in. By adding the userScope setting, we ensure that only user records that have the active field set to 1 are allowed access.

Changing the default user model

As you may have noticed, the role of the User model is crucial, not only to fetch the right user account, but also to check the permissions on some of the authentication schemes. By default, the Auth component will look for a User model, but you can change which model is to be used by setting the userModel property or the userModel key in the settings array.

For example, if your user model is Account, you would add the following setting when adding the Auth component to your controller:

'userModel' => 'Account'

Or equivalently, you would add the following to the beforeFilter method of your AppController class, in the block of code where you are setting up the component:

$this->Auth->userModel = 'Account';

There's more...

The $authorize property of the Auth component (or the authorize key in the Auth component settings array) defines which authentication scheme should be used. Possible values are:

  • controller: It makes the component use the controller's isAuthorized method, which returns true to allow access, or false to reject it. This method is particularly useful when obtaining the logged-in user
  • model: It is similar to controller; instead of using the controller to call the method, it looks for the isAuthorized method in the User model. First, it tries to map the controller's action to a CRUD operation (one of 'create', 'read', 'update', or 'delete'), and then calls the method with three arguments: the user record, the controller that is being accessed, and the CRUD operation (or actual controller action) that is to be executed.
  • object: It is similar to model; instead of using the model to call the method, it looks for the isAuthorized method in a given class. In order to specify which class, set the AuthComponent::$object property to an instance of such a class. It calls the method with three arguments: the user record, the controller that is being accessed, and the action that is to be executed.
  • actions: It uses the Acl component to check for access, which allows a much more grained access control.
  • crud: It is similar to actions; the difference lies in the fact that it first tries to map the controller's action to a CRUD operation (one of 'create', 'read', 'update', or 'delete'.)

 

CakePHP 1.3 Application Development Cookbook Over 70 great recipes for developing, maintaining, and deploying web applications
Published: March 2011
eBook Price: ₨739.00
Book Price: ₨1,232.00
See more
Select your format and quantity:

 

        Read more about this book      

(For more resources on CakePHP, see here.)

Allowing logins with username or e-mail

By default the Auth component will use the given username posted in the login form to check for a valid user account. However, some applications have two separate fields: one to define the username, and another one to define the user's e-mail. This recipe shows how to allow logins using either a username or an e-mail.

Getting ready

We should have a fully working authentication system, so follow the entire recipe, Setting up a basic authentication system.

We also need the field to hold the user's e-mail address. Add a field named email to your users table with the following SQL statement:

ALTER TABLE `users`
ADD COLUMN `email` VARCHAR(255) NOT NULL;

We need to modify the signup page so users can specify their e-mail address. Edit your app/views/users/add.ctp file and make the following changes:

<?php
echo $this->Form->create();
echo $this->Form->inputs(array(
'legend' => 'Signup',
'email',
'username',
'password'
));
echo $this->Form->end('Submit');
?>

How to do it...

  1. Edit your app/views/users/login.ctp file and make the following changes to it:

    <?php
    echo $this->Form->create(array('action'=>'login'));
    echo $this->Form->inputs(array(
    'legend' => 'Login',
    'username' => array('label'=>'Username / Email'),
    'password'
    ));
    echo $this->Form->end('Login');
    ?>

  2. Edit your UsersController class and make sure the login action looks like the following:

    public function login() {
    if (
    !empty($this->data) &&
    !empty($this->Auth->data['User']['username']) &&
    !empty($this->Auth->data['User']['password'])
    ) {
    $user = $this->User->find('first', array(
    'conditions' => array(
    'User.email' => $this->Auth-
    >data['User']['username'],
    'User.password' => $this->Auth-
    >data['User']['password']
    ),
    'recursive' => -1
    ));
    if (!empty($user) && $this->Auth->login($user)) {
    if ($this->Auth->autoRedirect) {
    $this->redirect($this->Auth-
    >redirect());
    }
    } else {
    $this->Session->setFlash($this->Auth-
    >loginError, $this->Auth->flashElement, array(), 'auth');
    }
    }
    }

If you now browse to http://localhost/users/login and you can enter the user's e-mail and password to log in, as shown in the following screenshot:

CakePHP 1.3 Application Development Cookbook

How it works...

When the Auth component is unable to find a valid user account using the username and password fields, it gives the control back to the login action. Therefore, in the login action we can check if there is any submitted data. If that is the case, we know that the Auth component was not able to find a valid account.

With this in mind, we can try to find a user account with an e-mail that matches the given username. If there is one, we log the user in and redirect the browser to the default action, similar to what the component would do on a successful attempt.

If we cannot find a valid user account, we simply set the flash message to the default error message specified in the Auth component.

There's more...

You may have noticed that when looking for the user record, we used $this->Auth->data rather than $this->data to use the actual posted values. The reason for this is because the Auth component will not only automatically hash the password field, but also remove its value from the controller's data property, so if you need to show the login form again, the password field will not be pre-filled for the user.

Saving the user details after login

One of the most typical functionalities offered by sites with authentication capabilities is the ability to let the user choose (by clicking on a checkbox) whether they want the system to remember their account after logging in.

Getting ready

We should have a working authentication system, so follow the entire recipe, Setting up a basic authentication system.

How to do it...

  1. Edit your app/app_controller.php file and add the following Auth component settings to the Auth component. Also add the Cookie component by making the following changes to the components property: AppController (in the $components property) must include the following mandatory setting (if it is not there, add it inside the array of settings for the component):

    public $components = array(
    'Auth' => array(
    'authorize' => 'controller',
    'autoRedirect' => false
    ),
    'Cookie',
    'Session'
    );

  2. Edit your app/views/users/login.ctp view file and make the following changes:

    <?php
    echo $this->Form->create(array('action'=>'login'));
    echo $this->Form->inputs(array(
    'legend' => 'Login',
    'username',
    'password',
    'remember' => array('type' => 'checkbox', 'label' =>
    'Remember me')
    ));
    echo $this->Form->end('Login');
    ?>

  3. Now, add the following code to the end of the login action of your UsersController class:

    UsersController class:
    if (!empty($this->data)) {
    $userId = $this->Auth->user('id');
    if (!empty($userId)) {
    if (!empty($this->data['User']['remember'])) {
    $user = $this->User->find('first', array(
    'conditions' => array('id' => $userId),
    'recursive' => -1,
    'fields' => array('username', 'password')
    ));
    $this->Cookie->write('User', array_intersect_key(
    $user[$this->Auth->userModel],
    array('username'=>null, 'password'=>null)
    ));
    } elseif ($this->Cookie->read('User') != null) {
    $this->Cookie->delete('User');
    }
    $this->redirect($this->Auth->redirect());
    }
    }

  4. Next, add the following code to the beginning of the logout() method of your UsersController class:

    if ($this->Cookie->read('User') != null) {
    $this->Cookie->delete('User');
    }

  5. Finally, add the following method to your AppController class, right below the components property declaration:

    public function beforeFilter() {
    if ($this->Auth->user() == null) {
    $user = $this->Cookie->read('User');
    if (!empty($user)) {
    $user = $this->Auth->getModel()->find('first', array(
    'conditions' => array(
    $this->Auth->fields['username'] =>
    $user[$this->Auth->fields['username']],
    $this->Auth->fields['password'] =>
    $user[$this->Auth->fields['password']]
    ),
    'recursive' => -1
    ));
    if (!empty($user) && $this->Auth->login($user)) {
    $this->redirect($this->Auth->redirect());
    }
    }
    }
    }

How it works...

The first task we needed to accomplish was to disable the automatic redirect in the Auth component. By doing so, we are able to catch both successful and failed log in attempts, which allows us to check if they remember me checkbox is selected. If the checkbox is indeed checked, we create a cookie named User that contains the values for the username and password fields with a value equal to the user ID that logged in. Remember that the password value is automatically encrypted by the Auth component, so it is safe for storage. The Cookie component adds another layer of security by automatically encrypting and decrypting the given values.

In AppController::beforeFilter(), when there is no logged-in user, we check to see if the cookie is set. If it is, we use the values for the username and password fields stored in the cookie to log in a user, and then redirect the browser to the login action.

Finally, we delete the cookie when it is appropriate (when a user logs in without the checkbox selected, or when the user manually logs out).

Getting the current user's information

CakePHP's authentication system will provide us with the necessary tools to build a strong, flexible Auth based application. We can then use it to fetch the current user information and make it available throughout our application.

In this recipe, we will see how to save the current logged-in user's information so it is accessible from any point of our CakePHP application, including its layout, while adding a helpful method to the User model to make the job easier.

Getting ready

We should have a working authentication system, so follow the recipe, Setting up a basic authentication system.

How to do it...

  1. Add the following method to your AppController class:

    public function beforeFilter() {
    $user = $this->Auth->user();
    if (!empty($user)) {
    Configure::write('User', $user[$this->Auth-
    >getModel()->alias]);
    }
    }

  2. Also in your AppController class, add the following method inside the class definition:

    public function beforeRender() {
    $user = $this->Auth->user();
    if (!empty($user)) {
    $user = $user[$this->Auth->getModel()->alias];
    }
    $this->set(compact('user'));
    }

  3. Copy the default CakePHP layout file named default.ctp from your cake/libs/view/layouts folder to your application's app/views/layouts folder. Place the following code in the app/views/layouts/default.ctp folder. While editing this layout, add the following code right where you want login / logout links to appear:

    <?php if (!empty($user)) { ?>
    Welcome back <?php echo $user['username']; ?>!
    <?php
    echo $this->Html->link('Log out', array('plugin'=>null,
    'admin'=>false, 'controller'=>'users', 'action'=>'logout'));
    } else {
    echo $this->Html->link('Log in', array('plugin'=>null,
    'admin'=>false, 'controller'=>'users', 'action'=>'login'));
    }
    ?>

  4. Add the following method to the User model. If you do not have a model created for the users table, proceed to create a file named user.php and place it in your app/models directory. If you do have one already, make sure you add the get method to it:

    <?php
    class User extends AppModel {
    public static function get($field = null) {
    $user = Configure::read('User');
    if (empty($user) || (!empty($field) && !array_key_
    exists($field, $user))) {
    return false;
    }
    return !empty($field) ? $user[$field] : $user;
    }
    }
    ?>

How it works...

By storing the user record in an application-wide configuration variable, we are able to obtain the current user information from anywhere in our application, whether it is controllers, components, models, and so on. This gives us the power to know if there's a logged-in user at any point.

We also need to make sure that views are able to learn whether there is a logged-in user. Even though a view could, technically speaking, still have access to the configure variable, it is normally more elegant to set a view variable to avoid any interaction with PHP classes from the view (except for the view helpers).

When you set variables for the view in AppController, it is very important to make sure no controller action will overwrite the variable. Choose a unique name wisely, and make sure you don't set a view variable with the same name in your controllers.

Finally, we add a handy method to the User model, so we can obtain the current user from our controllers without having to deal with the Configure variable. We can also use the get method to collect a particular bit of user information. For example, to fetch the current user's username from a controller, we would do something like the following:

$userName = User::get('username');

You should not have to load the User model class yourself, as the Auth component does it for you.

 

CakePHP 1.3 Application Development Cookbook Over 70 great recipes for developing, maintaining, and deploying web applications
Published: March 2011
eBook Price: ₨739.00
Book Price: ₨1,232.00
See more
Select your format and quantity:

 

        Read more about this book      

(For more resources on CakePHP, see here.)

Using prefixes for role-based access control

Even though CakePHP provides a very powerful access control layer, sometimes we just need to implement user roles without having to go into the details of specifying which role is allowed access to which action.

This recipe shows how to limit access to certain actions by role-using routing prefixes, which constitutes a perfect solution for simple role-based authentication. In order to accomplish this recipe, we will assume the need to add three user roles in our application: administrators, managers, and users.

Getting ready

We should have a working authentication system, so follow the recipe, Setting up a basic authentication system. The users table should also contain a field to hold the user's role (named role.) Add this field with the following SQL statement:

ALTER TABLE `users`
ADD COLUMN `role` VARCHAR(255) DEFAULT NULL AFTER `password`;

How to do it...

  1. Edit your app/config/core.php file and look for the line that defines the Routing.prefixes setting. If it is commented out, uncomment it. Then change it to:
    Configure::write('Routing.prefixes', array('admin', 'manager'));
  2. Add the following code at the end of your UsersController class definition:

    public function dashboard() {
    $role = $this->Auth->user('role');
    if (!empty($role)) {
    $this->redirect(array($role => true, 'action'
    => 'dashboard'));
    }
    }
    public function admin_dashboard() {
    }
    public function manager_dashboard() {
    }

  3. Create a view for each of these actions, and put content into it to reflect which view is being rendered. Therefore, you would have to create three files:
    • app/views/users/admin_dashboard.ctp
    • app/views/users/manager_dashboard.ctp
    • app/views/users/dashboard.ctp

    For example, the contents for dashboard.ctp could simply be:

    <h1>Dashboard (User)</h1>
  4. Edit your app/controllers/app_controller.php file and change the components property declaration to include the following setting for the Auth component:

    public $components = array(
    'Auth' => array(
    'authorize' => 'controller',
    'loginRedirect' => array(
    'admin' => false,
    'controller' => 'users',
    'action' => 'dashboard'
    )
    ),
    'Session'
    );

  5. While still editing your AppController class, change the isAuthorized method and replace it entirely with the following:

    public function isAuthorized() {
    $role = $this->Auth->user('role');
    $neededRole = null;
    $prefix = !empty($this->params['prefix']) ?
    $this->params['prefix'] :
    null;
    if (
    !empty($prefix) &&
    in_array($prefix, Configure::read('Routing.
    prefixes'))
    ) {
    $neededRole = $prefix;
    }
    return (
    empty($neededRole) ||
    strcasecmp($role, 'admin') == 0 ||
    strcasecmp($role, $neededRole) == 0
    );
    }

  6. Copy the default CakePHP layout file named default.ctp from your cake/libs/view/layouts folder to your application's app/views/layouts folder. While editing this layout, place the following code in the app/views/layouts/default.ctp layout file, right where you want the link to the dashboard to appear.

    <?php
    $dashboardUrl = array('controller'=>'users',
    'action'=>'dashboard');
    if (!empty($user['role'])) {
    $dashboardUrl[$user['role']] = true;
    }
    echo $this->Html->link('My Dashboard', $dashboardUrl);
    ?>

How it works...

CakePHP will recognize prefixes defined in the Routing.prefixes setting as part of the URL, when they are preceding a normal route. For example, if admin is a defined prefix, the route /admin/articles/index will translate to the admin_index action in ArticlesController.

Since we are utilizing the controller authentication scheme in the Auth configuration, we know that every time a user is trying to access a non-public action, AppController::isAuthorized() is executed, and inside the method we set true if the user has access, or false otherwise.

Knowing that, we can check to see if a prefix is being used when a controller action is about to be executed. If the current route being accessed includes a prefix, we can match that prefix against the user's role to make sure they have access to the requested resource.

We are able to link to a role-only resource just by prefixing it with the appropriate prefix in the route. For example, to link to the manager's dashboard, the URL would be:

array(
'manager' => true,
'controller' => 'users',
'action' => 'dashboard'
);

Setting up Access Control Layer-based authentication

The more roles an application has, the more complex its Access Control Layer becomes. Luckily, one of the authentication schemes provided by the Auth component allows us to easily define which actions are accessible by certain roles (known as groups), using command-line tools. In this recipe, you will learn how to set up ACL on your application.

Getting ready

We should have a table to hold the roles, named groups.

If you do not have one already, create it using the following statement:

CREATE TABLE `groups`(
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL,
PRIMARY KEY(`id`)
);

If you do not have any records in your groups table, create some by running the following SQL statement:

INSERT INTO `groups`(`id`, `name`) VALUES
(1, 'Administrator'),
(2, 'Manager'),
(3, 'User');

We must also have a users table to hold the users, which should contain a field (named group_id) to contain a reference to the group a user belongs to. If you do not have such a table, create it using the following statement:

CREATE TABLE `users`(
`id` INT NOT NULL AUTO_INCREMENT,
`group_id` INT NOT NULL,
`username` VARCHAR(255) NOT NULL,
`password` CHAR(40) NOT NULL,
PRIMARY KEY(`id`),
KEY `group_id`(`group_id`),
CONSTRAINT `users__groups` FOREIGN KEY(`group_id`) REFERENCES
`groups`(`id`)
);

We also need to have the ARO / ACO tables initialized. Using your operating system console, switch to your application directory, and run:

  • If you are on a GNU Linux / Mac / Unix system:
    ../cake/console/cake schema create DbAcl
  • If you are on Microsoft Windows:
    ..\cake\console\cake.bat schema create DbAcl

How to do it...

The following initial steps are very similar to what is shown in Setting up a basic authentication system. However, there are some differences between the two that are crucial, so make sure you go through these instructions carefully.

  1. Create a controller for the User model (in a file named users_controller.php placed inside your app/controllers folder), which should contain the following:

    <?php
    class UsersController extends AppController {
    public function login() {
    }
    public function logout() {
    $this->redirect($this->Auth->logout());
    }
    }
    ?>

  2. Create a file named login.ctp in your app/views/users folder (create the folder if you do not have one already), with the following contents:

    <?php
    echo $this->Form->create(array('action'=>'login'));
    echo $this->Form->inputs(array(
    'legend' => 'Login',
    'username',
    'password'
    ));
    echo $this->Form->end('Login');
    ?>

  3. Create a file named app_controller.php in your app/ folder. Make sure it contains the following:

    <?php
    class AppController extends Controller {
    public $components = array(
    'Acl',
    'Auth' => array(
    'authorize' => 'actions',
    'loginRedirect' => array(
    'admin' => false,
    'controller' => 'users',
    'action' => 'dashboard'
    )
    ),
    'Session'
    );
    }
    ?>

  4. Modify the UsersController class and add the following code before its login() method:

    public function beforeFilter() {
    parent::beforeFilter();
    $this->Auth->allow('add');
    }
    public function add() {
    if (!empty($this->data)) {
    $this->User->create();
    if ($this->User->save($this->data)) {
    $this->Session->setFlash('User created!');
    $this->redirect(array('action'=>'login'));
    } else {
    $this->Session->setFlash('Please correct the
    errors');
    }
    }
    $this->set('groups', $this->User->Group->find('list'));
    }

  5. Add the view for the action in the folder app/views/users by creating a file named add.ctp with the following contents:

    <?php
    echo $this->Form->create();
    echo $this->Form->inputs(array(
    'legend' => 'Signup',
    'username',
    'password',
    'group_id'
    ));
    echo $this->Form->end('Submit');
    ?>

  6. Create a file named group.php and place it in your app/models folder with the following contents:

    <?php
    class Group extends AppModel {
    public $actsAs = array('Acl' => 'requester');
    public function parentNode() {
    if (empty($this->id) && empty($this->data)) {
    return null;
    }
    $data = $this->data;
    if (empty($data)) {
    $data = $this->find('first', array(
    'conditions' => array('id' => $this-
    >id),
    'fields' => array('parent_id'),
    'recursive' => -1
    ));
    }
    if (!empty($data[$this->alias]['parent_id'])) {
    return $data[$this->alias]['parent_id'];
    }
    return null;
    }
    }
    ?>

  7. Create a file named user.php and place it in your app/models folder with the following contents:

    <?php
    class User extends AppModel {
    public $belongsTo = array('Group');
    public $actsAs = array('Acl' => 'requester');
    public function parentNode() {
    }
    public function bindNode($object) {
    if (!empty($object[$this->alias]['group_id'])) {
    return array(
    'model' => 'Group',
    'foreign_key' => $object[$this->alias]
    ['group_id']
    );
    }
    }
    }
    ?>

    Take note of the IDs for all the records in your groups table, as they are needed to link each group to an ARO record.

  8. Run the following commands in your console (change the references to 1, 2, 3 to meet your own group IDs, if they are different).
    • If you are on a GNU Linux / Mac / Unix system, the commands are:
      ../cake/console/cake acl create aro root Groups
      ../cake/console/cake acl create aro Groups Group.1
      ../cake/console/cake acl create aro Groups Group.2
      ../cake/console/cake acl create aro Groups Group.3
    • If you are on Microsoft Windows, the commands are:
      ..\cake\console\cake.bat acl create aro root Groups
      ..\cake\console\cake.bat acl create aro Groups Group.1
      ..\cake\console\cake.bat acl create aro Groups Group.2
      ..\cake\console\cake.bat acl create aro Groups Group.3
  9. Add the following code at the end of your UsersController class definition:

    public function dashboard() {
    $groupName = $this->User->Group->field('name',
    array('Group.id'=>$this->Auth->user('group_id'))
    );
    $this->redirect(array('action'=>strtolower($groupName)));
    }
    public function user() {
    }
    public function manager() {
    }
    public function administrator() {
    }

  10. Create a view for each of these actions, and put some distinctive content on each one of them to reflect which view is being rendered. Therefore, you have to create three files:
    • app/views/users/user.ctp
    • app/views/users/manager.ctp
    • app/views/users/administrator.ctp.

    For example the contents for user.ctp could simply be:

    <h1>Dashboard (User)</h1>
  11. We have to tell ACL about these restricted actions. Run the following commands in your console.
    • If you are on a GNU Linux / Mac / Unix system, the commands are:
      ../cake/console/cake acl create aco root controllers
      ../cake/console/cake acl create aco controllers Users
      ../cake/console/cake acl create aco controllers/Users
      logout
      ../cake/console/cake acl create aco controllers/Users user
      ../cake/console/cake acl create aco controllers/Users
      manager
      ../cake/console/cake acl create aco controllers/Users
      administrator
    • If you are on Microsoft Windows, the commands are:
      ..\cake\console\cake.bat acl create aco root controllers
      ..\cake\console\cake.bat acl create aco controllers Users
      ..\cake\console\cake.bat acl create aco controllers/Users
      logout
      ..\cake\console\cake.bat acl create aco controllers/Users
      user
      ..\cake\console\cake.bat acl create aco controllers/Users
      manager
      ..\cake\console\cake.bat acl create aco controllers/Users
      administrator
  12. Finally, we have to grant permissions by linking each ARO (groups) to each ACO (controller's actions). Run the following commands in your console.
    • If you are on a GNU Linux / Mac / Unix system, the commands are:
      ../cake/console/cake acl grant Group.1 controllers/Users
      all
      ../cake/console/cake acl grant Group.2 controllers/Users/
      logout all
      ../cake/console/cake acl grant Group.2 controllers/Users/
      manager all
      ../cake/console/cake acl grant Group.3 controllers/Users/
      logout all
      ../cake/console/cake acl grant Group.3 controllers/Users/
      user all
    • If you are on Microsoft Windows, the commands are:
      ..\cake\console\cake.bat acl grant Group.1 controllers/
      Users all
      ..\cake\console\cake.bat acl grant Group.2 controllers/
      Users/logout all
      ..\cake\console\cake.bat acl grant Group.2 controllers/
      Users/manager all
      ..\cake\console\cake.bat acl grant Group.3 controllers/
      Users/logout all
      ..\cake\console\cake.bat acl grant Group.3 controllers/
      Users/user all

We now have a fully working ACL based authentication system. We can add new users by browsing to http://localhost/users/add, logging in with http://localhost/users/login, and finally logging out with http://localhost/users/logout.

Users should only have access to http://localhost/users/user, managers to http://localhost/users/manager, and administrators should be able to access all those actions, including http://localhost/users/administrator.

How it works...

When setting the authorize configuration option of the Auth component to actions, and after adding Acl to the list of controller-wide components, CakePHP will check to see if the current action being accessed is a public action. If this is not the case, it will check for a logged-in user with a matching ACO record. If there is no such record, it will deny access.

Once there is a matching ACO for the controller action, it will use the bindNode method in the User model to see how a user record is matched to an ARO. The method implementation we added specifies that a user record should be looked up in the aros table by means of the group that the user belongs to.

After having both the matching ACO and ARO, it lastly checks to see whether there is a valid permission set up (in the aros_acos table) for the given ARO and ACO records. If it finds one, it allows access, otherwise it will reject authorization.

It is of vital importance that each record in the groups table has a matching ARO record. We set that association by issuing aro create commands to link each group ID to an ARO record of the form Group.ID, where ID is the actual ID.

Similarly, all controller actions that are not within the defined public actions should have a matching ACO record. Just as with AROs, we create the association between controller's actions and ACOs issuing aco create commands, setting the ACO name to be the action name, and making them child of an ACO which name is the controller name.

Finally, to grant the permission of an ARO (group) to an ACO (controller's actions), we issue acl grant commands, specifying as the first argument the ARO (Group.ID) and the second argument either a whole controller (such as controllers/Users), or a specific controller action (such as controllers/Users/logout). The last argument to the grant command (all) simply gives a further control of the type of access, and makes more sense when using ACL to control access to custom objects, or when using the crud authentication scheme.

There's more...

While developing an application, the task of matching each controller action to an ACO may be somewhat troublesome. Fortunately, several people in the CakePHP community felt the need for an easier solution. One of the solutions that I'd recommend is adopting acl_extras, a plugin developed by Mark Story, the lead developer of the CakePHP 1.3 release. By using this plugin, you will be able to continuously synchronize your controllers with the acos table. Find more about it, including its installation instructions, at http://github.com/markstory/acl_extras.

Summary

The first two recipes show us how to set up a basic, yet fully working authentication system. The next three recipes allow our users to log in using different information, have their user details saved after a successful login, and show us how to get this user information. The sixth recipe shows a more complex authorization technique that relies on route prefixes. The seventh recipe sets up a complex authentication system through the use of CakePHP's Access Control Layer.


Further resources on this subject:


About the Author :


Mariano Iglesias

Mariano has been programming since the early age of 11. He quickly became proficient with C and C++, producing his first commercial applications at 15 years of age. He then continued to learn several other programming languages, PHP being one of them. He has contributed to several open source projects, becoming part of the core development team of both CakePHP and Lithium, two of the best PHP frameworks. He currently divides his time between open source programming, his commercial work, his hobbies, and spending time with his family.

Books From Packt


Expert PHP 5 Tools
Expert PHP 5 Tools

PHP jQuery Cookbook
PHP jQuery Cookbook

PHP 5 E-commerce Development
PHP 5 E-commerce Development

PHP 5 Social Networking
PHP 5 Social Networking

AJAX and PHP: Building Modern Web Applications 2nd Edition
AJAX and PHP: Building Modern Web Applications 2nd Edition

PHP 5 CMS Framework Development - 2nd Edition
PHP 5 CMS Framework Development - 2nd Edition

CMS Design Using PHP and jQuery
CMS Design Using PHP and jQuery

CodeIgniter 1.7 Professional Development
CodeIgniter 1.7 Professional Development


Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software