This chapter will cover the following topics:
Setting up a basic authentication system
Using and configuring the
Auth
componentAllowing 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
Integrating with OpenID
This chapter explains how to set up authentication on a CakePHP application, starting from the most basic setup and finishing with advanced authorization mechanisms. 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.
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. Finally, the last recipe shows us how to integrate our application with OpenID.
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.
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`) );
1. Create a file named
users_controller.php
and place it inside yourapp/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 yourapp/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 yourapp/
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 thelogin
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 yourapp/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 tohttp://localhost/users/login
, and finally logging out by browsing tohttp://localhost/users/logout
.After creating a user, you should see the login form with a success message, as shown in the following screenshot:
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
.
Note
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.
Note
To obtain more information about the authorize
setting, see the recipe Using and configuring the Auth component.
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.
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.
Note
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 }
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.
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;
1. Modify the definition of the
Auth
component in yourAppController
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 thecomponents
property declaration, at the beginning of thebeforeFilter
method in yourAppController
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 yourapp/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 thelogout()
method:public function dashboard() { }
5. Finally, create the view for this newly added action in a file named
dashboard.ctp
and place it in yourapp/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:
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.
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';
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'sisAuthorized
method, which returnstrue
to allow access, orfalse
to reject it. This method is particularly useful when obtaining the logged-in user (refer to the Getting the current user's information recipe)model
: It is similar tocontroller
; instead of using the controller to call the method, it looks for theisAuthorized
method in theUser
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 tomodel
; instead of using the model to call the method, it looks for theisAuthorized
method in a given class. In order to specify which class, set theAuthComponent::$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 theAcl
component to check for access, which allows a much more grained access control.crud
: It is similar toactions
; 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'
.)
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.
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');
?>
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 thelogin
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:
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.
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.
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.
We should have a working authentication system, so follow the entire recipe, Setting up a basic authentication system.
1. Edit your
app/app_controller.php
file and add the followingAuth
component settings to theAuth
component. Also add theCookie
component by making the following changes to thecomponents
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 yourUsersController
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 yourUsersController
class:if ($this->Cookie->read('User') != null) { $this->Cookie->delete('User'); }
5. Finally, add the following method to your
AppController
class, right below thecomponents
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()); } } } }
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).
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.
We should have a working authentication system, so follow the recipe, Setting up a basic authentication system.
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 yourcake/libs/view/layouts
folder to your application'sapp/views/layouts
folder. Place the following code in theapp/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 theusers
table, proceed to create a file nameduser.php
and place it in yourapp/models
directory. If you do have one already, make sure you add theget
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; } } ?>
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).
Note
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.
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.
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`;
1. Edit your
app/config/core.php
file and look for the line that defines theRouting.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 thecomponents
property declaration to include the following setting for theAuth
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 theisAuthorized
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 yourcake/libs/view/layouts
folder to your application'sapp/views/layouts
folder. While editing this layout, place the following code in theapp/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); ?>
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' );
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.
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
Note
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 namedusers_controller.php
placed inside yourapp/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 yourapp/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 yourapp/
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 itslogin()
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 namedadd.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 yourapp/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 yourapp/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'] ); } } } ?>
Note
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
.
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.
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.
OpenID (http://openid.net) is a great way to allow users to log in without having to have an actual username in your application. It is a solution that is widely adopted, and has proven itself on many popular sites (such as Google, Yahoo, MySpace, and AOL).
This recipe shows how to add support for OpenID logins in a transparent way, while still working with a valid Auth
implementation.
We should have a working authentication system, so follow the recipe, Setting up a basic authentication system.
We will also need the PHP OpenID Library. Download the latest release from https://github.com/openid/php-openid/downloads and extract the folder named Auth
from the downloaded file into your app/vendors
folder. You should now have a directory named Auth
inside your vendors
folder.
Finally, we need to download the OpenID plugin for CakePHP. Go to http://github.com/mariano/openid/downloads and download the latest release. Uncompress the downloaded file into your app/plugins
folder. You should now have a directory named openid
inside app/plugins
.
1. Edit your
AppController
class and change the reference for theAuth
component fromAuth
toOpenid.OpenAuth
. Thecomponents
property should now look like this:public $components = array( 'Openid.OpenAuth' => array( 'authorize' => 'controller' ), 'Session' );
2. Next, edit the login view (in
app/views/users/login.ctp
) and add a field to allow the user to specify their OpenID URL. The view should now look like this:<?php echo $this->Form->create(array('action'=>'login')); echo $this->Form->inputs(array( 'legend' => 'Login', 'openid' => array('label' => 'OpenID URL'), 'username', 'password' )); echo $this->Form->end('Login'); ?>
You should now be able to log in using either a valid username and password combination, or an OpenID URL, as shown in the following screenshot:
As the OpenAuth
component (a part of the openid
plugin) extends the CakePHP built-in Auth
component, it works in a similar fashion. When the component cannot seem to find a way to log in the user with a username and password, it will check whether the OpenID URL is specified.
If this is the case, it will attempt to authenticate the URL against the OpenID server. When it does, the user is taken to the OpenID server so the application can be granted permission to access the OpenID credentials. When permission is given, the user is taken back to the application, at a point on which the OpenAuth
component is able to mark the user as logged in, and resume the normal application work flow.
The openid
plugin has further options to customize its behavior; including the ability to specify which user information should be given back. Check the documentation in http://github.com/mariano/openid.
Being a standard Auth
implementation, this integration can be combined with any of the other recipes we have seen in this chapter, which allows for a flexible open authentication solution. If you do, make sure to note that the user given back by the OpenAuth
component does not contain a valid user record, so you should create one upon log in.
Even when you are using the OpenAuth
component which clearly has a different name than Auth
, you can still use $this->Auth
to set properties or call, for example, the allow
method. This is possible because the component creates an alias.