A Complete Login and Authentication Application Tutorial for CakePHP 2.3

in CakePHP/PHP/Tutorials & Samples/Web Development

Cake provides a very good tutorial on how to use the Auth component here, but its not complete. So, I spent a couple hours setting up a complete tutorial that you can download and check out a live demo. Here is what we will be creating: A web application that uses CakePHP’s Auth component to login and logout as well as bar access to certain pages if the user is not authorized. Once a user is logged-in, they will be able to go the dashboard and edit users. (Please note that I am not changing the default CakePHP theme. So below is what the login screen looks like:)

Edit: July 2014 Updates
Thanks to various reader inputs, I have updated the .zip file to fix some minor issues that were noticed. I also have a live demo that you can play with here. Fixes include:

  • fixed: deleted members can no longer login
  • fixed: error when you try to use an existing username or email again for a new user

login

Users Database Table

Let’s start by creating the users database table. The users table contains a user’s username, password and role. Additional fields are meta fields such as created and modified dates as well as a status field. (I never actually delete records, just turn their status to 0.) Here is what the final users table looks like:

CREATE TABLE users (
    `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    `username` VARCHAR(128),
    `password` VARCHAR(128),
    `email` VARCHAR(128),
    `role` VARCHAR(64),
    `created` DATETIME DEFAULT NULL,
    `modified` DATETIME DEFAULT NULL,
    `status` tinyint(1) NOT NULL DEFAULT '1'
);

Routes.php

Now, lets modify the core components of CakePHP to support our login module. First, we need to modify routes.php so that we can have a custom link for login, logout and the dashboard. This step is not required but I do it so that the URLs look clean..

	Router::connect('/dashboard', array('controller' => 'users', 'action' => 'index'));
	Router::connect('/login', array('controller' => 'users', 'action' => 'login'));
	Router::connect('/logout', array('controller' => 'users', 'action' => 'logout'));

We also need to modify the home page so that it now points to our login action.

	Router::connect('/', array('controller' => 'users', 'action' => 'login'));

User.php

Next, we need to create a file called User.php in the app\Model folder. This is our users model where all of our validation logic will be. To accomplish this, I used some of Cake’s buil-in validation rules as well as my own custom validation rules. My rules are pretty simple: Usernames must be unique, non-empty, alphanumeric and be between 5 and 15 characters. Passwords must have a minimum length of 6 and must match the confirmation password. Emails must be unique and be at least 6 characters in length. Finally, roles must be in the list of accepted roles that I have selected, which include: ‘king’, ‘queen’, ‘bishop’, ‘rook’, ‘knight’ and ‘pawn’. Some of the custom validation functions I created include isUniqueUsername() to determine if a username is unique, isUniqueEmail() to determine if an email is unique, alphaNumericDashUnderscore() to only allow alphanumeric values, equaltofield() to check if a value is equal to another value. You can learn more about CakePHP’s custom validation rules here. Finally, I had to alter the beforeSave() function so that I hash the passwords that I get before saving them. For security reasons, you should never store unencrypted passwords in your Database. Below is the full code for User.php

App::uses('AuthComponent', 'Controller/Component');

class User extends AppModel {
	
	public $avatarUploadDir = 'img/avatars';
    
	public $validate = array(
        'username' => array(
            'nonEmpty' => array(
                'rule' => array('notEmpty'),
                'message' => 'A username is required',
				'allowEmpty' => false
            ),
			'between' => array( 
				'rule' => array('between', 5, 15), 
				'required' => true, 
				'message' => 'Usernames must be between 5 to 15 characters'
			),
			 'unique' => array(
				'rule'    => array('isUniqueUsername'),
				'message' => 'This username is already in use'
			),
			'alphaNumericDashUnderscore' => array(
				'rule'    => array('alphaNumericDashUnderscore'),
				'message' => 'Username can only be letters, numbers and underscores'
			),
        ),
        'password' => array(
            'required' => array(
                'rule' => array('notEmpty'),
                'message' => 'A password is required'
            ),
			'min_length' => array(
				'rule' => array('minLength', '6'),  
				'message' => 'Password must have a mimimum of 6 characters'
			)
        ),
		
		'password_confirm' => array(
            'required' => array(
                'rule' => array('notEmpty'),
                'message' => 'Please confirm your password'
            ),
			 'equaltofield' => array(
				'rule' => array('equaltofield','password'),
				'message' => 'Both passwords must match.'
			)
        ),
		
		'email' => array(
			'required' => array(
				'rule' => array('email', true),    
				'message' => 'Please provide a valid email address.'    
			),
			 'unique' => array(
				'rule'    => array('isUniqueEmail'),
				'message' => 'This email is already in use',
			),
			'between' => array( 
				'rule' => array('between', 6, 60), 
				'message' => 'Usernames must be between 6 to 60 characters'
			)
		),
        'role' => array(
            'valid' => array(
                'rule' => array('inList', array('king', 'queen', 'bishop', 'rook', 'knight', 'pawn')),
                'message' => 'Please enter a valid role',
                'allowEmpty' => false
            )
        ),
		
		
		'password_update' => array(
			'min_length' => array(
				'rule' => array('minLength', '6'),   
				'message' => 'Password must have a mimimum of 6 characters',
				'allowEmpty' => true,
				'required' => false
			)
        ),
		'password_confirm_update' => array(
			 'equaltofield' => array(
				'rule' => array('equaltofield','password_update'),
				'message' => 'Both passwords must match.',
				'required' => false,
			)
        )

		
    );
	
		/**
	 * Before isUniqueUsername
	 * @param array $options
	 * @return boolean
	 */
	function isUniqueUsername($check) {

		$username = $this->find(
			'first',
			array(
				'fields' => array(
					'User.id',
					'User.username'
				),
				'conditions' => array(
					'User.username' => $check['username']
				)
			)
		);

		if(!empty($username)){
			if($this->data[$this->alias]['id'] == $username['User']['id']){
				return true; 
			}else{
				return false; 
			}
		}else{
			return true; 
		}
    }

	/**
	 * Before isUniqueEmail
	 * @param array $options
	 * @return boolean
	 */
	function isUniqueEmail($check) {

		$email = $this->find(
			'first',
			array(
				'fields' => array(
					'User.id'
				),
				'conditions' => array(
					'User.email' => $check['email']
				)
			)
		);

		if(!empty($email)){
			if($this->data[$this->alias]['id'] == $email['User']['id']){
				return true; 
			}else{
				return false; 
			}
		}else{
			return true; 
		}
    }
	
	public function alphaNumericDashUnderscore($check) {
        // $data array is passed using the form field name as the key
        // have to extract the value to make the function generic
        $value = array_values($check);
        $value = $value[0];

        return preg_match('/^[a-zA-Z0-9_ \-]*$/', $value);
    }
	
	public function equaltofield($check,$otherfield) 
    { 
        //get name of field 
        $fname = ''; 
        foreach ($check as $key => $value){ 
            $fname = $key; 
            break; 
        } 
        return $this->data[$this->name][$otherfield] === $this->data[$this->name][$fname]; 
    } 

	/**
	 * Before Save
	 * @param array $options
	 * @return boolean
	 */
	 public function beforeSave($options = array()) {
		// hash our password
		if (isset($this->data[$this->alias]['password'])) {
			$this->data[$this->alias]['password'] = AuthComponent::password($this->data[$this->alias]['password']);
		}
		
		// if we get a new password, hash it
		if (isset($this->data[$this->alias]['password_update']) && !empty($this->data[$this->alias]['password_update'])) {
			$this->data[$this->alias]['password'] = AuthComponent::password($this->data[$this->alias]['password_update']);
		}
	
		// fallback to our parent
		return parent::beforeSave($options);
	}

}

AppController.php

Next, AppController needs to be modified so that it uses Cake’s Auth component. This is where we tell the Auth component to redirect users to the index page after a successful login and to the login page after they logout. Once we do so, we need to update the beforeFilter() function to only allow the login action to be authorized in any controller. All other actions will only be accessible after the user is logged-in. I’ve also setup an isAuthorized() function that could be used to manage access to various pages. (The isAuthorized() function is not covered in this post, but its quite easy to setup one..)

	public $components = array(
		'DebugKit.Toolbar',
		'Session',
        'Auth' => array(
            'loginRedirect' => array('controller' => 'users', 'action' => 'index'),
            'logoutRedirect' => array('controller' => 'users', 'action' => 'login'),
			'authError' => 'You must be logged in to view this page.',
			'loginError' => 'Invalid Username or Password entered, please try again.'
 
        ));
	
	// only allow the login controllers only
	public function beforeFilter() {
        $this->Auth->allow('login');
    }
	
	public function isAuthorized($user) {
		// Here is where we should verify the role and give access based on role
		
		return true;
	}

UsersController.php

At this point, we can create our Users controller by creating a file called UsersController.php in app/Controller. This controler contains functions for login, logout, edit, index, add, edit and delete. There is also a function called activate, which is used to turn a user’s status back to active after they have been deleted. (remember that I don’t actually delete users. I just change their status flag). One important thing to note is that I override the beforeFilter() function defined in AppController so that I now allow login() and add() functions to be visible without requiring authorization. If we don’t do this, we will never be able to add users to our application. The rest of the controller is pretty straightforward and the full code is presented below:


class UsersController extends AppController {

	public $paginate = array(
        'limit' => 25,
        'conditions' => array('status' => '1'),
    	'order' => array('User.username' => 'asc' ) 
    );
	
    public function beforeFilter() {
        parent::beforeFilter();
        $this->Auth->allow('login','add'); 
    }
	


	public function login() {
		
		//if already logged-in, redirect
		if($this->Session->check('Auth.User')){
			$this->redirect(array('action' => 'index'));		
		}
		
		// if we get the post information, try to authenticate
		if ($this->request->is('post')) {
			if ($this->Auth->login()) {
				$this->Session->setFlash(__('Welcome, '. $this->Auth->user('username')));
				$this->redirect($this->Auth->redirectUrl());
			} else {
				$this->Session->setFlash(__('Invalid username or password'));
			}
		} 
	}

	public function logout() {
		$this->redirect($this->Auth->logout());
	}

    public function index() {
		$this->paginate = array(
			'limit' => 6,
			'order' => array('User.username' => 'asc' )
		);
		$users = $this->paginate('User');
		$this->set(compact('users'));
    }


    public function add() {
        if ($this->request->is('post')) {
				
			$this->User->create();
			if ($this->User->save($this->request->data)) {
				$this->Session->setFlash(__('The user has been created'));
				$this->redirect(array('action' => 'index'));
			} else {
				$this->Session->setFlash(__('The user could not be created. Please, try again.'));
			}	
        }
    }

    public function edit($id = null) {

		    if (!$id) {
				$this->Session->setFlash('Please provide a user id');
				$this->redirect(array('action'=>'index'));
			}

			$user = $this->User->findById($id);
			if (!$user) {
				$this->Session->setFlash('Invalid User ID Provided');
				$this->redirect(array('action'=>'index'));
			}

			if ($this->request->is('post') || $this->request->is('put')) {
				$this->User->id = $id;
				if ($this->User->save($this->request->data)) {
					$this->Session->setFlash(__('The user has been updated'));
					$this->redirect(array('action' => 'edit', $id));
				}else{
					$this->Session->setFlash(__('Unable to update your user.'));
				}
			}

			if (!$this->request->data) {
				$this->request->data = $user;
			}
    }

    public function delete($id = null) {
		
		if (!$id) {
			$this->Session->setFlash('Please provide a user id');
			$this->redirect(array('action'=>'index'));
		}
		
        $this->User->id = $id;
        if (!$this->User->exists()) {
            $this->Session->setFlash('Invalid user id provided');
			$this->redirect(array('action'=>'index'));
        }
        if ($this->User->saveField('status', 0)) {
            $this->Session->setFlash(__('User deleted'));
            $this->redirect(array('action' => 'index'));
        }
        $this->Session->setFlash(__('User was not deleted'));
        $this->redirect(array('action' => 'index'));
    }
	
	public function activate($id = null) {
		
		if (!$id) {
			$this->Session->setFlash('Please provide a user id');
			$this->redirect(array('action'=>'index'));
		}
		
        $this->User->id = $id;
        if (!$this->User->exists()) {
            $this->Session->setFlash('Invalid user id provided');
			$this->redirect(array('action'=>'index'));
        }
        if ($this->User->saveField('status', 1)) {
            $this->Session->setFlash(__('User re-activated'));
            $this->redirect(array('action' => 'index'));
        }
        $this->Session->setFlash(__('User was not re-activated'));
        $this->redirect(array('action' => 'index'));
    }

}

All that remains is creating our ctp files that match the actions in our controller. Here they are:

login.ctp

This is the main page that is used to login to the system. It requires a valid username and password combination.

<div class="users form">
<?php echo $this->Session->flash('auth'); ?>
<?php echo $this->Form->create('User'); ?>
    <fieldset>
        <legend><?php echo __('Please enter your username and password'); ?></legend>
        <?php echo $this->Form->input('username');
        echo $this->Form->input('password');
    ?>
    </fieldset>
<?php echo $this->Form->end(__('Login')); ?>
</div>
<?php
 echo $this->Html->link( "Add A New User",   array('action'=>'add') ); 
?>

add.ctp

This is the page that allows you to add new users to the system. It is available when you are logged-in and when you are logged-out. The password is required twice, like in any professional website and validation rules, set in Users.php are enforced

<!-- app/View/Users/add.ctp -->
<div class="users form">

<?php echo $this->Form->create('User');?>
    <fieldset>
        <legend><?php echo __('Add User'); ?></legend>
        <?php echo $this->Form->input('username');
		echo $this->Form->input('email');
        echo $this->Form->input('password');
		echo $this->Form->input('password_confirm', array('label' => 'Confirm Password *', 'maxLength' => 255, 'title' => 'Confirm password', 'type'=>'password'));
        echo $this->Form->input('role', array(
            'options' => array( 'king' => 'King', 'queen' => 'Queen', 'rook' => 'Rook', 'bishop' => 'Bishop', 'knight' => 'Knight', 'pawn' => 'Pawn')
        ));
		
		echo $this->Form->submit('Add User', array('class' => 'form-submit',  'title' => 'Click here to add the user') ); 
?>
    </fieldset>
<?php echo $this->Form->end(); ?>
</div>
<?php 
if($this->Session->check('Auth.User')){
echo $this->Html->link( "Return to Dashboard",   array('action'=>'index') ); 
echo "<br>";
echo $this->Html->link( "Logout",   array('action'=>'logout') ); 
}else{
echo $this->Html->link( "Return to Login Screen",   array('action'=>'login') ); 
}
?>

which produces this form if the user is logged-in:

add-user-loggedin

and this form if the user is logged-out:

add-user-loggedout

index.ctp

Index.ctp acts as my dashboard and can only be accessed when a user is logged-in. This is where the list of users is displayed. It also uses Cake’s paginator to display the data.

<div class="users form">
<h1>Users</h1>
<table>
    <thead>
		<tr>
			<th><?php echo $this->Form->checkbox('all', array('name' => 'CheckAll',  'id' => 'CheckAll')); ?></th>
			<th><?php echo $this->Paginator->sort('username', 'Username');?>  </th>
			<th><?php echo $this->Paginator->sort('email', 'E-Mail');?></th>
			<th><?php echo $this->Paginator->sort('created', 'Created');?></th>
			<th><?php echo $this->Paginator->sort('modified','Last Update');?></th>
			<th><?php echo $this->Paginator->sort('role','Role');?></th>
			<th><?php echo $this->Paginator->sort('status','Status');?></th>
			<th>Actions</th>
		</tr>
	</thead>
	<tbody>						
		<?php $count=0; ?>
		<?php foreach($users as $user): ?>				
		<?php $count ++;?>
		<?php if($count % 2): echo '<tr>'; else: echo '<tr class="zebra">' ?>
		<?php endif; ?>
			<td><?php echo $this->Form->checkbox('User.id.'.$user['User']['id']); ?></td>
			<td><?php echo $this->Html->link( $user['User']['username']  ,   array('action'=>'edit', $user['User']['id']),array('escape' => false) );?></td>
			<td style="text-align: center;"><?php echo $user['User']['email']; ?></td>
			<td style="text-align: center;"><?php echo $this->Time->niceShort($user['User']['created']); ?></td>
			<td style="text-align: center;"><?php echo $this->Time->niceShort($user['User']['modified']); ?></td>
			<td style="text-align: center;"><?php echo $user['User']['role']; ?></td>
			<td style="text-align: center;"><?php echo $user['User']['status']; ?></td>
			<td >
			<?php echo $this->Html->link(    "Edit",   array('action'=>'edit', $user['User']['id']) ); ?> | 
			<?php
				if( $user['User']['status'] != 0){ 
					echo $this->Html->link(    "Delete", array('action'=>'delete', $user['User']['id']));}else{
					echo $this->Html->link(    "Re-Activate", array('action'=>'activate', $user['User']['id']));
					}
			?>
			</td>
		</tr>
		<?php endforeach; ?>
		<?php unset($user); ?>
	</tbody>
</table>
<?php echo $this->Paginator->prev('<< ' . __('previous', true), array(), null, array('class'=>'disabled'));?>
<?php echo $this->Paginator->numbers(array(   'class' => 'numbers'     ));?>
<?php echo $this->Paginator->next(__('next', true) . ' >>', array(), null, array('class' => 'disabled'));?>
</div>				
<?php echo $this->Html->link( "Add A New User.",   array('action'=>'add'),array('escape' => false) ); ?>
<br/>
<?php 
echo $this->Html->link( "Logout",   array('action'=>'logout') ); 
?>

It produces this form:

dashboard

Edit.ctp

Edit allows you to edit data about a given user. In my case, I don’t allow users to change their username and their password only gets changed if they enter a value. (Most of these rules were inspired by the rules that WordPress has for users ) The code is below

<!-- app/View/Users/add.ctp -->
<div class="users form">
<?php echo $this->Form->create('User'); ?>
    <fieldset>
        <legend><?php echo __('Edit User'); ?></legend>
        <?php 
		echo $this->Form->hidden('id', array('value' => $this->data['User']['id']));
		echo $this->Form->input('username', array( 'readonly' => 'readonly', 'label' => 'Usernames cannot be changed!'));
		echo $this->Form->input('email');
        echo $this->Form->input('password_update', array( 'label' => 'New Password (leave empty if you do not want to change)', 'maxLength' => 255, 'type'=>'password','required' => 0));
		echo $this->Form->input('password_confirm_update', array('label' => 'Confirm New Password *', 'maxLength' => 255, 'title' => 'Confirm New password', 'type'=>'password','required' => 0));
		

		echo $this->Form->input('role', array(
            'options' => array( 'king' => 'King', 'queen' => 'Queen', 'rook' => 'Rook', 'bishop' => 'Bishop', 'knight' => 'Knight', 'pawn' => 'Pawn')
        ));
		echo $this->Form->submit('Edit User', array('class' => 'form-submit',  'title' => 'Click here to add the user') ); 
?>
    </fieldset>
<?php echo $this->Form->end(); ?>
</div>
<?php 
echo $this->Html->link( "Return to Dashboard",   array('action'=>'index') ); 
?>
<br/>
<?php 
echo $this->Html->link( "Logout",   array('action'=>'logout') ); 
?>

Here is a screenshot:

edit-user

Download it all

That concludes the tutorial. You can download the entire tutorial in zip format here.

Tags:

Mifty Yusuf is a Montreal-based software developer who enjoys playing with new web technologies as well as comic books and illustrations. He beleives that, no matter what the question is, the answer is always Batman!

233 Comments

  1. Hi Mifty,
    Great work. I have download your source code completed the database configuration.
    on browser, getting this error while running this project.

    Warning: include(Cake\bootstrap.php): failed to open stream: No such file or directory in C:\xampp\htdocs\cakephp\login_project\webroot\index.php on line 77

    Warning: include(): Failed opening ‘Cake\bootstrap.php’ for inclusion (include_path=’C:\xampp\htdocs\cakephp\lib;.;C:\xampp\php\PEAR’) in C:\xampp\htdocs\cakephp\login_project\webroot\index.php on line 77

    Fatal error: CakePHP core could not be found. Check the value of CAKE_CORE_INCLUDE_PATH in APP/webroot/index.php. It should point to the directory containing your \cake core directory and your \vendors root directory. in C:\xampp\htdocs\cakephp\login_project\webroot\index.php on line 86

    This is my first CakePHP project. Sorry for this type of basic questions . please help

  2. Hello,
    I am using FB-connect so in-spite of user controllers login function I am using facebook_connected function. In that some-where I am writing $this->Auth->login(); But it is not letting me getting success. Please help. Thanks.

    • Hi, I dont know how to fix this issue. But if it may help you, I have a social login tutorial that allows you to login with facebook.

  3. thanks much, it worked like a charm, with the exception of the bug in the isUniqueUsername function. Therainb0w, resolved and now i am up and running.

  4. ur folder when set up in local host shows

    Warning: include(C:\wamp\www\cake-2_3-user-auth\lib\Cake\bootstrap.php)

    Warning: include() [function.include]: Failed opening ‘C:\wamp\www\cake-2_3-user-auth\lib\Cake\bootstrap.php’ for inclusion (include_path=’.;C:\php\pear’) in

    Fatal error: CakePHP core could not be found. Check the value of CAKE_CORE_INCLUDE_PATH in APP/webroot/index.php. It should point to the directory containing your \cake core directory and your \vendors root directory

    these errors can be seen.not working..plz check and let me know

    • hello taher,
      I only include the app folder, not the full CakePHP install. Download the full isntall of CakePHP and then overwrite the install folder with my app folder.

  5. when i run project it will give error like

    Undefined property: UsersController::$Auth [APP/controllers/users_controller.php,

  6. The tutorial is amazing and i have implement that but there is a problem except these everthing is work’s fine also it maintaining session of this user but, when user login it can’t redirect to index page it gives me Fatal error please sir, can you find it

    Fatal error: Call to undefined method AuthComponent::redirectUrl() in UsersController.php
    thank you,
    sir

  7. if($this->data[$this->alias][‘id’] == $username[‘User’][‘id’])—-> This line is not working in my wamp server..
    And in my project i have Super admin and Admin is there. Super Admin’s position is ‘1’ and Admin’s position is ‘0’. The only functionality difference is Super admin can Add Admin, Activate Admin, and block Admin. Admin can only Add, Activate, Block the users. Pls help me

    • Hi Mifty ,
      I have two users… Admin and general user… i have two database tables. they are “users” and “admin”. User authendication working finely. How to use AppController for admin authendication also. i need to use both the authendication in AppController.. Please give some idea…….

  8. Hi !
    Thank you for the tutorial. I tried it but i have this error :
    “Parse error: parse error in C:\xampp\htdocs\example\cakeauth\app\Plugin\DebugKit\Controller\Component\ToolbarComponent.php on line 182”
    Can you help me, please ?

  9. Hello brother,
    You are really great. your post is more helpful for me. I am new in cakephp, i want to learn cakephp properly. for this i need some individual task like as contact us, captcha, shopping cart etc. please, will you provide me these projec?

    thanks for giving an important post….

    my Skype.. s_quadercse
    gmail.. salmanquadercse@gmail.com

    • sorry slaman. Due to time constraints, I don’t have time to support any individual projects. I simply provide these tutorials as a way for you to jumpstart your projects or get ideas to inspire you 🙂

  10. I have used same code . When I register it will register successfully. But when I login it always shows Invalid username or password

    I have entered right username and password

  11. Actully i want to add a functionality is that any user making account after that a EMAIL should be go on user email id and as soon as user click on that email link then user will authorise to open there account..

  12. Congratulations on your tutorial …
    I need to put functions for each User, exemple king can edit and delete, and knight can not edit and delete.
    Thanks in advance.

  13. i thanked you Mifty for this tutoriel, because it have helpe me to resolve my hashage. but when i login the system does not return the username in the message to welcom . So if you can to helpe me

    • Hi. you can simply do a check for the user’s access level. Then, depending on the user’s access level, you grant them access or you restrict access and redirect them to another page. this could be a base function that you put in APPControler so that all other controller functions inherit it.

  14. Hi mifty

    Thank you for your tutorial, i am having a problem with my login, my login and all works fine but when i fire my website as Localhost/xyz its get redirected to Localhost/xyz/login but i want it to go to homepage when i hit localhost/xyz which is view/pages/home.ctp

    Please help me out

Leave a Reply

Your email address will not be published.

*

Latest from CakePHP

Go to Top