Joseph Crawford Using wordpress because he is lazy

20Dec/0858

CakePHP 1.2.* Auth Component Tutorial

There is an updated version of this post for CakePHP 1.3.*

There are a lot of tutorials out there on how to use the Auth component in CakePHP but everything that I've found so far has been lacking for my need - so I wrote my own. Recently I needed a way to set up a simple authentication mechanism I could use for customer websites in order for them to log in and manage their content. Since I use CakePHP for everything these days one of the simplest solutions was to utilize the built in authentication component which can be a little tricky to set up since the documentation is a little vague and scattered over the Internet.

All of this code is provided in a zip file at the bottom of this post. Also, feel free to leave any feedback if you have comments or suggestions.

Database

The first thing you will want to do is create a user table in your site database:

/app/config/sql/users.sql

CREATE TABLE `users` (
    `id` int(11) unsigned NOT NULL auto_increment,
    `username` varchar(20) NOT NULL default '',
    `password` varchar(40) NOT NULL default '',
    `name` varchar(40) NOT NULL default '',
    `email` varchar(60) NOT NULL default '',
    `created` datetime default NULL,
    `updated` datetime default NULL,
    PRIMARY KEY  (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 AUTO_INCREMENT=1;

Your table doesn't have to be exactly like this so feel free to adjust it to your own needs. However later on I will include CRUD pages for the users and you may need to adjust them to suit your changes.

User Model

Now that you have your table set up lets create the 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
112
<?php
class User extends AppModel {
    var $name = 'User';
    var $useTable = 'users';
    var $validate = array(
        'username' => array(
            'empty' => array(
                'rule' => 'notEmpty',
                'required' => true,
                'allowEmpty' => false,
                'message' => 'Please enter a username'
            ),
            'length' => array(
                'rule' => array('minLength', 4),
                'required' => true,
                'allowEmpty' => true,
                'message' => 'Usernames must be at lest 4 characters long'
            ),
            'alphanum' => array(
                'rule' => 'alphaNumeric',
                'required' => true,
                'allowEmpty' => true,
                'message' => 'Only letters and numbers are allowed in usernames'
            ),
            'unique' => array(
                'rule' => 'isUnique',
                'required' => true,
                'allowEmpty' => true,
                'message' => 'That username is already in use by another user'
            )
        ),
        'clear_password' => array(
            'empty' => array(
                'rule' => 'notEmpty',
                'required' => true,
                'allowEmpty' => false,
                'on' => 'create',
                'message' => 'Please enter a password'
            ),
            'length' => array(
                'rule' => array('minLength', 6),
                'required' => true,
                'allowEmpty' => true,
                'message' => 'Passwords must be at lease 6 characters long'
            )
        ),
        'confirm_password' => array(
            'empty' => array(
                'rule' => 'notEmpty',
                'required' => true,
                'allowEmpty' => false,
                'on' => 'create',
                'message' => 'Please confirm the password'
            ),
            'emptyUpdate' => array(
                'rule' => 'emptyUpdate',
                'required' => true,
                'on' => 'update',
                'message' => 'Please confirm the password'
            ),
            'match' => array(
                'rule' => 'matchPasswords',
                'required' => true,
                'allowEmpty' => true,
                'message' => 'The passwords you entered do not match'
            )
        ),
        'name' => array(
            'rule' => 'notEmpty',
            'required' => true,
            'allowEmpty' => false,
            'message' => 'Please enter a name'
        ),
        'email' => array(
            'empty' => array(
                'rule' => 'notEmpty',
                'required' => true,
                'allowEmpty' => false,
                'message' => 'Please enter an email address'
            ),
            'email' => array(
                'rule' => 'email',
                'required' => true,
                'allowEmpty' => true,
                'message' => 'Please enter a valid email address'
            ),
            'unique' => array(
                'rule' => 'isUnique',
                'required' => true,
                'allowEmpty' => true,
                'message' => 'That email address is already in use by another user'
            )
        )
    );

    function emptyUpdate() {
        if (!empty($this->data['User']['clear_password']) &amp;&amp; empty($this->data['User']['confirm_password'])) {
            return false;
        } else {
            return true;
        }
    }

    function matchPasswords() {
        if ($this->data['User']['clear_password'] != $this->data['User']['confirm_password']) {
            return false;
        } else {
            return true;
        }
    }
}
?>

I went ahead and included all the validation that I use as well so again feel free to adjust as necessary.

Users Controller

And then for the Users Controller...

/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
103
104
<?php
class UsersController extends AppController {
    var $name = 'Users';
    var $uses = array('User');

    function beforeFilter() {
        parent::beforeFilter();
    }

    function index() {
        $users = $this->User->find('all');
        if (empty($users)) {
            $this->Session->setFlash('There are no users defined', 'default', array('class'=>'bad'));
        } else {
            $this->set('users', $users);
        }
    }

    function view($id) {
        $user = $this->User->findById($id);
        if (!empty($user)) {
            $this->set('user', $user);
        } else {
            $this->Session->setFlash('Invalid User ID', 'default', array('class'=>'bad'));
            $this->redirect('index');
        }
    }

    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->User->save($this->Auth->hashPasswords($this->data), false);
                $this->Session->setFlash($this->data['User']['name'].' Added', 'default', array('class'=>'good'));
                $this->redirect('index');
            } else {
                $this->Session->setFlash('Please correct the errors below', 'default', array('class'=>'bad'));
            }
        }
    }

    function edit($id = null) {
        if (!empty($this->data)) {
            foreach ($this->data['User'] as $field => $data) {
                if (!in_array($field, array('clear_password', 'confirm_password'))) {
                    $fields[] = $field;
                }
            }
            if (!empty($this->data['User']['clear_password']) || !empty($this->data['User']['confirm_password'])) {
                $fields[] = 'password';
                $fields[] = 'clear_password';
                $fields[] = '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']['confirm_password'];
                }
                $this->User->save($this->Auth->hashPasswords($this->data), false, $fields);
                $this->Session->setFlash($this->data['User']['name'].' Updated', 'default', array('class'=>'good'));
                $this->redirect('index');
            }
        } else {
            $user = $this->User->findById($id);
            if (empty($user)) {
                $this->Session->setFlash('Invalid User ID', 'default', array('class'=>'bad'));
                $this->redirect('index');
            } else {
                unset($user['User']['password']);
                $this->data = $user;
            }
        }
    }

    function delete($id) {
        $user = $this->User->findById($id);
        if (empty($user)) {
            $this->Session->setFlash('Invalid User ID', 'default', array('class'=>'bad'));
        } else {
            if ($user['User']['id'] == $this->Session->read('Auth.User.id')) {
                $this->Session->setFlash('Sorry, you can not delete yourself', 'default', array('class'=>'bad'));
            } else {
                if ($this->User->del($id)) {
                    $this->Session->setFlash($user['User']['name'].' Deleted', 'default', array('class'=>'good'));
                } else {
                    $this->Session->setFlash('Failed to delete '.$user['User']['name'], 'default', array('class'=>'bad'));
                }
            }
        }
        $this->redirect('index');
    }

    function login() {
        // Auth Magic
    }

    function logout() {
        $this->Session->del('Auth.User');
        $this->Session->setFlash('Your are now logged out', 'default', array('class'=>'bad'));
        $this->redirect('login');
    }
}
?>

Before Filter

I have found that it is a good habit to always inherit your app controller's before filter. You will not want to forget this if you customize any of the Auth component variables. Otherwise they will not be set as you expect when you declare a beforeFilter method in any other controller. You may have noticed the array of methods I passed to Auth component. You will need to uncomment that line in order to add users after enabling the Auth component in your app controller or you will be locked out of your site. You will also want to add similar before filters in all your other controllers to control access accordingly. Please see the documentation for the allow method for more information.

Password Hash Issue

One of the things I find irritating about the Auth component is how it uses the hash in the config file to encrypt the password by default instead of using something easy like md5. I'm sure they have their reasons and supposedly you can change it by setting Security::setHash('md5'); somewhere but I wasn't able to get that working right. So I just figured out how to work with it as is.

If you take a look at the add function in the users controller you will see how I managed it. The first problem was that Auth encrypts the password field immediately. So if you had any errors in the registration the password that is placed back in the form field is encrypted and will no longer match the confirm password field. I changed the name of the password field to clear_password and then did my validation against that. After it passes validation the password is set and encrypted in the save.

Only change the password if one is entered problem

Now if you look at the edit function you will notice another problem. Usually in order to avoid creating a separate change password function I like to set up the password fields in the edit user page. If the password and confirm password are not entered then the password will not be changed. This creates a problem with cake because it likes to validate fields for us and the Auth component will even take a blank value and produce a nice 40 character hash string. So the craziness that is going on there is just a way around that issue using the additional fields attribute for the save method.

Other Controllers

Now don't forget to open up your other controllers and add a before method with an exception for your public pages:

function beforeFilter() {
    parent::beforeFilter();
    $this->Auth->allow('index');
}

You could also allow('*') in your app_controller and then specify which pages to deny, whichever way is easiest for you.

If you have any plugins you will also need to add before filters to the plugin app controller and all of the plugin controllers. If all the methods in a controller require authentication you can feel free to leave out the Auth->allow. Also never add an exception for login and logout. I have seen several people doing this and it is not necessary and could possibly break the auth component.

User Views

There is not a whole lot to explain here. It is just a the views for the users controller. Feel free to use them or ignore them.

/app/views/users/add.ctp

1
2
3
4
5
6
7
8
9
10
11
<h2>Add User</h2>
<?php
echo $form->create('User', array('url' => '/users/add'));
echo $form->input('username', array('size' => '20'));
echo $form->input('clear_password', array('type' => 'password', 'size'=>'20'));
echo $form->input('confirm_password', array('type'=> 'password', 'size' => '20'));
echo $form->input('name', array('size' => '35'));
echo $form->input('email',array('label' => 'Email Address', 'size' => '40'));
echo '<div><input type="submit" value="Submit" /> or '.$html->link('Cancel', array('action' => 'index')).'</div>';
echo $form->end();
?>

/app/views/users/edit.ctp

1
2
3
4
5
6
7
8
9
10
11
12
13
<h2>Edit User</h2>
<?php
echo $form->create('User', array('url' => '/users/edit'));
echo $form->input('username', array('size' => '20'));
echo 'Leave the password fields blank if you do not wish to change this users password';
echo $form->input('clear_password', array('type' => 'password', 'size'=>'20'));
echo $form->input('confirm_password', array('type'=>'password', 'size' => '20'));
echo $form->input('name', array('size' => '35'));
echo $form->input('email',array('label' => 'Email Address', 'size' => '40'));
echo $form->hidden('id');
echo '<div><input type="submit" value="Submit" /> or '.$html->link('Cancel', array('action' => 'index')).'</div>';
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
<h2>Users</h2>
<div>
<?php
echo $html->link('Add User', array('action' => 'add'));
?>
</div>
<hr />
<table width="100%">
    <tbody>
        <tr>
            <th>ID</th>
            <th>Name</th>
            <th>Email</th>
            <th></th>
        </tr>
<?php
if (!empty($users)) {
    foreach ($users as $i) {
        echo '<tr>';
        echo '  <td>'.$i['User']['id'].'</td>';
        echo '  <td>'.$i['User']['name'].'</td>';
        echo '  <td><a href="mailto: '.$i['User']['email'].'">'.$i['User']['email'].'</a></td>';
        echo '  <td class="actions">';
        echo $html->link('View', array('action' => 'view', $i['User']['id'])).' | ';
        echo $html->link('Edit', array('action' => 'edit', $i['User']['id'])).' | ';
        echo $html->link('Delete', array('action' => 'delete', $i['User']['id']), null, 'Are you sure you want to delete this user?');
        echo '  </td>';
        echo '';
    }
}
?>
    </tbody>
</table>

/app/views/users/login.ctp

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

/app/views/users/view.ctp

1
2
3
4
5
6
7
8
9
10
11
12
<h2><?php echo $user['User']['name']; ?></h2>
<div>
<?php
echo $html->link('Back to Users', array('action' => 'index')).' | ';
echo $html->link('Edit', array('action' => 'edit', $user['User']['id'])).' | ';
echo $html->link('Delete', array('action' => 'delete', $user['User']['id']), null, 'Are you sure you want to delete this user?');
?>
</div>
<hr />
<p>Username: <?php echo $user['User']['username']; ?></p>
<p>Email Address: <?php echo '<a href="mailto: '.$user['User']['email'].'">'.$user['User']['email'].''; ?>
</p>

App Controller

Now that everything is set up let's enable the auth component in the app controller (If you do not have one you may need to create it).

/app/app_controller.php

1
2
3
4
5
6
7
8
9
10
11
<?php
class AppController extends Controller {
    var $components = array('Auth');

    function beforeFilter() {
        $this->Auth->loginAction = array('controller' => 'users', 'action' => 'login');
        $this->Auth->logoutRedirect = array('controller' => 'users', 'action' => 'login');
        $this->Auth->loginRedirect = array('controller' => 'users', 'action' => 'index');
    }
}
?>

Login Action

The login action used here is actually set by default. You will need to change this if you have a different name for you users controller or login action.

Logout Redirect

The logout redirect is where users will be redirected after logging out. This is also set to users->login by default.

Login Redirect

Login redirect is actually a fall back option. Do not expect this to always redirect your users to the page specified by this option after logging in. The auth component stores the last visited page in the session and will redirect a successful login there unless autoRedirect is set to false in which case you will need to write a custom login function and handle redirection there. Or if a user has no previous session information (for example if they linked directly to the login page) in which case the auth component will use the login redirect method. This behavior is not very well documented on the Cake website. I actually found the answer in a closed bug report.

Because of the default options though you should be able to leave that stuff out and still be okay.

All Done!

Now you may visit /users/add and create a user account so you can log in and manage your site. DO NOT forget to remove the allow in your users controller for all the different pages you do not want to be public.

You can download all the code here.

I have disabled this download because you should be using Cake 1.3 by now. I have an article with code here.