Joseph Crawford Using wordpress because he is lazy

8Nov/1017

CakePHP 1.3.* Auth Component Tutorial Basic Authentication

Last year I wrote a tutorial on setting up the Auth Component for basic authentication which is now slightly outdated since the release of CakePHP 1.3. I plan to post several versions of this progressing up to a more complex setup which includes the ACL Component. It seems every website's authentication needs are slightly different but, hopefully, I can cover the most common and give advice on customizing each one.

For this tutorial you will need to have CakePHP 1.3 already set up and connecting to a database. (Note: Some of the php functions I have used here are for PHP5 so if you are still running PHP4 you may encounter some errors.) As before all of the code used here is provided in a zip file at the bottom of the post. I don't want this to be just a lot of code so I will try to explain everything as much as possible along the way. Please feel free to comment with any recommended changes.

Database

The first thing you will want to do is create the users table in your site database or update what you already have. If you would like to name it something other than users go ahead and do so now. (I will address the changes you might need to make later.)

If you already have a users table with existing users you could run into problems, and need to take a few extra steps to update the accounts. Cake uses its own security salt that is set in /app/config/core.php to encrypt each new password. Since the salt for each Cake site should be unique; your existing passwords will not match. If the passwords are not encrypted at all then you could write a script to loop through each account and update the old passwords. I will not get into that here but, if anybody needs help with that, just let me know and I can explain further. If your old passwords are encrypted using md5 you might be able to use Security::setHash('md5').

/app/config/schema/users.sql

CREATE TABLE IF NOT EXISTS `users` (
  `id` int(11) unsigned NOT NULL auto_increment,
  `username` varchar(20) NOT NULL,
  `password` varchar(40) NOT NULL,
  `clear_password` varchar(20) default NULL,
  `first_name` varchar(20) default NULL,
  `last_name` varchar(20) default NULL,
  `email` tinytext NOT NULL,
  `status` enum('Active','Inactive') NOT NULL default 'Active',
  `last_login` datetime default NULL,
  `last_access` datetime default NULL,
  `created` datetime default NULL,
  `modified` datetime default NULL,
  PRIMARY KEY  (`id`),
  KEY `status` (`status`),
  KEY `login` (`username`,`password`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 ;

Your table doesn't have to be exactly like this so feel free to adjust it to your own needs. However, further down, I will include CRUD pages for the users and if you use them (and you change this) you will need to update those also.

User Model

Now that you have your table set up; create your user model. (Again, if you customized your user table at all, you will need to update this accordingly.) I also included a few validation rules. (If your users table is called something different like “people” the easy fix for this would be to set var $uses = 'people' in your user model.)

/app/models/user.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
<?php
class User extends AppModel {
    var $name = 'User';
    var $virtualFields = array(
        'full_name' => "CONCAT(User.first_name, ' ', User.last_name)",
    );
    var $validate = array(
        'username' => array(
            'empty' => array(
                'rule' => 'notEmpty',
                'required' => true,
                'allowEmpty' => false,
                'message' => 'Username is required',
            ),
            'minlength' => array(
                'rule' => array('minLength', 4),
                'required' => true,
                'allowEmpty' => true,
                'message' => 'Usernames must be at least 4 characters long',
            ),
            'maxlength' => array(
                'rule' => array('maxLength', 20),
                'required' => true,
                'allowEmpty' => true,
                'message' => 'Usernames may not be more than 20 characters long',
            ),
            'alphanum' => array(
                'rule' => 'alphaNumeric',
                'required' => true,
                'allowEmpty' => true,
                'message' => 'Usernames may only contain letters and numbers',
            ),
            'unique' => array(
                'rule' => 'isUnique',
                'required' => true,
                'allowEmpty' => true,
                'message' => 'That username is already in use',
            ),
        ),
        'clear_password' => array(
            'empty' => array(
                'rule' => 'notEmpty',
                'required' => true,
                'allowEmpty' => false,
                'on' => 'create',
                'message' => 'Password is required',
            ),
            'length' => array(
                'rule' => array('minLength', 6),
                'required' => true,
                'allowEmpty' => true,
                'message' => 'Passwords must be at least 6 characters long',
            ),
        ),
        'confirm_password' => array(
            'empty_create' => array(
                'rule' => 'notEmpty',
                'required' => true,
                'allowEmpty' => false,
                'on' => 'create',
                'message' => 'Please confirm the password 1',
            ),
            'empty_update' => array(
                'rule' => 'validateConfirmPasswordEmptyUpdate',
                'required' => true,
                'allowEmpty' => true,
                'on' => 'update',
                'message' => 'Please confirm the password 2',
            ),
            'match' => array(
                'rule' => 'validateConfirmPasswordMatch',
                'required' => true,
                'allowEmpty' => true,
                'message' => 'The passwords do not match',
            ),
        ),
        'email' => array(
            'empty' => array(
                'rule' => 'notEmpty',
                'required' => true,
                'allowEmpty' => false,
                'message' => 'Email is required',
            ),
            'valid' => array(
                'rule' => 'email',
                'required' => true,
                'allowEmpty' => true,
                'message' => 'Please enter a valid email address',
            ),
        ),
    );
   
    /**
     * Callback function for confirm_password
     * Used to check the confirm_password field is not empty on update
     * @return bool
     */

    function validateConfirmPasswordEmptyUpdate() {
        return !empty($this->data['User']['clear_password']) && !empty($this->data['User']['confirm_password']);
    }
   
    /**
     * Callback function for confirm_password
     * Used to check if clear_password and confirm_password match
     * @return bool
     */

    function validateConfirmPasswordMatch() {
        return $this->data['User']['clear_password'] == $this->data['User']['confirm_password'];
    }
}
?>

Virtual Fields

This is a thing I found recently that can be very handy. I like to have the first name and last name in separate columns in my users table and this allows me to combine them into a full name without having to do any additional coding later. The Html Helper's paginator sort functions also work with it, which is nice - definitely something to keep in mind for other applications.

Password

There are two separate validations for the password because I like to always have a confirm password for user management. The clear_password set is pretty straight forward but some additional things are needed for the confirm_password. This will also address an issue with passwords when editing a user. It is nice to edit a user and not have to re-enter a password. So I added some things to allow the password fields to be blank on edit. However if one of them is not empty it will not validate.

Validation Callbacks

These are two methods used for the confirm_password validation.

Users Controller

Now we need a controller to handle user management and the login methods.

/app/controllers/users_controller.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
<?php
class UsersController extends AppController {
    var $name = 'Users';
    var $paginate = array(
        'User' => array(
            'limit' => 20,
            'order' => array(
                'User.full_name' => 'asc',
            ),
        ),
    );
   
    /**
     * Set this to false if you don't want to store clear passwords in the database
     * @var bool
     * @access private
     */

    var $_store_clear_password = true;
   
    function index() {
        $users = $this->paginate('User');
        $this->set(compact('users'));
    }
   
    function add() {
        if (!empty($this->data)) {
            $this->User->set($this->data);
            if ($this->User->validates()) {
                $this->data['User']['password'] = $this->data['User']['clear_password'];
                $this->data = $this->Auth->hashPasswords($this->data);
                if (!$this->_store_clear_password) {
                    unset($this->data['User']['clear_password']);
                }
                $this->User->save($this->data, false);
                $this->Session->setFlash('User Added');
                $this->redirect('index');
            }
        }
    }
   
    function edit($id = null) {
        if (!empty($this->data)) {
            $fields = array_keys($this->data['User']);
            if (!empty($this->data['User']['clear_password']) || !empty($this->data['User']['confirm_password'])) {
                $fields[] = 'password';
            } else {
                $fields = array_diff($fields, array('clear_password', 'confirm_password'));
            }
            $this->User->set($this->data);
            if ($this->User->validates()) {
                if (!empty($this->data['User']['clear_password'])) {
                    $this->data['User']['password'] = $this->data['User']['clear_password'];
                }
                $this->data = $this->Auth->hashPasswords($this->data);
                if (!$this->_store_clear_password) {
                    unset($this->data['User']['clear_password']);
                }
                $this->User->save($this->data, false, $fields);
                $this->Session->setFlash('User Updated');
                $this->redirect('index');
            }
        } else {
            $user = $this->User->findById($id);
            if (empty($user)) {
                $this->Session->setFlash('Invalid User ID');
                $this->redirect('add');
            } else {
                unset($user['User']['clear_password']);
                $this->data = $user;
            }
        }
    }
   
    function delete() {
        $delete_count = 0;
        if (!empty($this->data['User']['delete'])) {
            foreach($this->data['User']['delete'] as $id => $delete) {
                if ($delete == 1) {
                    if ($this->User->delete($id)) {
                        $delete_count++;
                    }
                }
            }
        }
        $this->Session->setFlash($delete_count . ' User' . (($delete_count == 1) ? ' was' : 's were') . ' deleted');
        $this->redirect('index');
    }
   
    function login() {
        if (!empty($this->data) && $this->Auth->user()) {
            $this->User->id = $this->Auth->user('id');
            $this->User->saveField('last_login', date('Y-m-d H:i:s'));
            $this->redirect($this->Auth->redirect());
        }
    }
   
    function logout() {
        $this->Session->setFlash('You are now logged out');
        $this->redirect($this->Auth->logout());
    }
}
?>

I know the acronym is CRUD but I just interpret it as list, add, edit and delete. They are more common terms for data management to end users and a “view” page is usually kind of pointless. However, if you need one, feel free to add it.

Paginate

I added pagination to this version only to demonstrate some more of Cake's awesomeness. This is nothing new, but nice if you needed it, because now you don't have to retro-fit it.

Store Clear Password

This is an option to store clear passwords in the database which is kind of frowned upon but when you need it-you need it. I have had a lot of people demand for the ability to retrieve passwords and not have to reset them when forgotten. I did not include a reset password section. I might add one in a later post but, for now, its all about authentication and user management. If you don't want to store clear passwords then just set the $_store_clear_password variable to false and forget about it. You may feel the need to change the name of the password field in the views after that but, please don't, it has a different name for a reason.

Index

Really nothing special here. Just grabbing the list of users and tossing them to the view.

Add

The Add and Edit methods are slightly different than usual. Mostly because of the password fields. I changed the name of password to clear_password in the view mainly so, that in case of a validation error, the encrypted password is not sent back to the view. This way the password is not encrypted until after the user data is validated.

Edit

This is very similar to Add with one major difference. I wanted the user's password to stay the same if it was not changed in the form. So initially I blank it out, and then, if neither field has been populated, they are removed from the fields list before validation.

Delete

I see a lot of people doing delete with a GET. For example a link to /users/delete/$id. This might be fine for some applications but seems a little dangerous to me. I wanted to make it a POST and a good way to do that is by adding checkboxes to provide a multiple delete. (You could also add methods for updating the user status similarly. I have done that in the past by creating a drop down list below the users list with some on change javascript to modify the form's action.)

Login

Most of what goes on here is handled by the Auth Component automatically, but I wanted to record the user's last_login date, so I added that here. This code will not execute unless autoRedirect in the Auth Component is set to false. It is true by default so we will change it later in the App Controller.

Logout

The Auth Component will handle destroying our login session, so the only thing to do here is notifying the user (if you want to) and then redirecting to the appropriate place.

User Views

There is not a lot to explain here. You may need to make modifications to fit your needs but it is a good start.

/app/views/users/add.ctp

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
echo '<h2>Add User';
echo $form->create('User');
echo $form->input('username');
echo $form->input('clear_password', array('type' => 'password', 'label' => 'Password'));
echo $form->input('confirm_password', array('type' => 'password'));
echo $form->input('first_name');
echo $form->input('last_name');
echo $form->input('email');
echo $form->input('status', array('options' => array('Active' => 'Active', 'Inactive' => 'Inactive')));
echo $form->submit('Submit', array('after' => ' ' . $html->link('Cancel', array('action' => 'index'))));
echo $form->end();
?>

/app/views/users/edit.ctp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
echo '<h2>Edit User';
echo $form->create('User');
echo $form->hidden('id');
echo $form->input('username');
echo $form->input('clear_password', array('type' => 'password', 'label' => 'Password'));
echo $form->input('confirm_password', array('type' => 'password'));
echo $form->input('first_name');
echo $form->input('last_name');
echo $form->input('email');
echo $form->input('status', array('options' => array('Active' => 'Active', 'Inactive' => 'Inactive')));
echo $form->submit('Submit', array('after' => ' ' . $html->link('Cancel', array('action' => 'index'))));
echo $form->end();
?>

/app/views/users/index.ctp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
echo '<h2>Users';
echo '<p>' . $html->link('Add User', array('action' => 'add')) . '</p>';
if (!empty($users)) {
    echo $form->create('User', array('action' => 'delete'));
    echo '<table width="100%">';
    echo '  <thead>';
    $cells = array(
        $form->checkbox(null, array('id' => 'select-all')),
        null,
        $this->Paginator->sort('Name', 'full_name'),
        $this->Paginator->sort('Email', 'email'),
        $this->Paginator->sort('Status', 'status'),
    );
    echo $this->Html->tableHeaders($cells);
    echo '  </thead>';
    echo '  <tbody>';
    foreach($users as $i) {
        $cells = array(
            $form->checkbox('User.delete.' . $i['User']['id']),
            $html->link('Edit', array('action' => 'edit', $i['User']['id'])),
            $i['User']['full_name'],
            $i['User']['email'],
            $i['User']['status'],
        );
        echo $this->Html->tableCells($cells, array('class' => 'odd'), array('class' => 'even'));
    }
    echo '  </tbody>';
    $numbers = $this->Paginator->numbers();
    if (!empty($numbers)) {
        $counter = $this->Paginator->prev('« Previous', null, null, array('class' => 'disabled'));
        $counter .= ' | '.$numbers.' | ';
        $counter .= $this->Paginator->next('Next »', null, null, array('class' => 'disabled'));
        echo '<tfoot>';
        echo $this->Html->tableCells(array(array(array($counter, array('colspan' => count($cells))))), null, null, true);
        echo '</tfoot>';
    }
    echo '</table>';
    echo $form->end('Delete Selected');
}
?>

/app/views/users/login.ctp

1
2
3
4
5
6
7
<?php
echo '<h2>Login';
echo $form->create('User', array('action' => 'login'));
echo $form->input('username');
echo $form->input('password');
echo $form->end('Login');
?>

App Controller

Now that everything is set up lets enable the Auth Component in the App Controller. If you do not already have one just create it otherwise make the appropriate changes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class AppController extends Controller {
    var $components = array(
        'Auth' => array(
            'autoRedirect' => false,
        ),
        'Session',
    );
    var $helpers = array(
        'Html',
        'Form',
        'Session',
    );
   
    function afterFilter() {
        # Update User last_access datetime
       if ($this->Auth->user()) {
            $this->loadModel('User');
            $this->User->id = $this->Auth->user('id');
            $this->User->saveField('last_access', date('Y-m-d H:i:s'));
        }
    }
}
?>

I moved the Auth Component setup to inside the $components array in order to eliminate needing to run parent::beforeFilter() in other controller beforeFilters just to keep validation running correctly. There are a lot of options for the Auth Component that I am not using here. Most of the time the defaults work just fine. But if you are doing anything non-standard the options are fairly well explained in the book.

You might notice the afterFilter but, this isn't too important, it only records the users last_access time. If you want to make sure that is saved all the time, and you have a afterFilter in your other controllers, you will need to have a parent::afterFilter() inside it. But it will not break anything if you don't.

Almost Done...

We need to create some users and right now you probably can't because if you pull up your site it will take you to the login page. And you probably don't have an account to log in with yet. So for now open your Users Controller and add this.

function beforeFilter() {
    $this->Auth->allow('*');
}

Now open /users/add in your browser, and create an account. After that remove the above chunk of code from you users controller otherwise your user management will remain open to the world. It will force you to the login again but that's okay because you have an account now :)

Now You're Done!

Or just beginning, depends on how you look at it. Either way you should have authentication working on your site now. If you have any questions, comments or suggestions you can comment below.

Download

Comments (17) Trackbacks (2)
  1. Hello Sir,

    I have developed this code but when I refreshed the index page then the row added automatically. I have made changes on it but it didn’t work.

    Please give me any suggestion.

    Thanks

    Regards,
    Shekhar

  2. Hi I need some help here… I have a default home page in pages. I want the authentication to allow the home page to be accessed. But then i tried $this->Auth->allow(‘home’) and it did not work. What can i do?


Leave a comment

(required)