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 my name is awdhesh kumar and i create theme integrat in cakephp2.0, i red your tutorial very good explane in you tutorial i ask how to send email in cakephp2.0 plz explane Mifty sir

  2. please send me same tutorial in cakephp 3.2 .so please help me. and how we search the values of table from the database .for exampl i have 4 field in table alike as name,username ,email, title and i want to search the values of title,email,username from the table. so please reply me and if possible then send me all code on my gmail id is:-> mandeepkhaleriya.08@gmail.com …so please hhelp me….please …..please………

    • Hello. Unfortunately, I have moved on to other projects so I will not be able to explain how to do this with CakePHP 3.2. However, the code is straightforward enough that you should be able to port it to 3.2 easily.

      • please send me code for make a login page in cakephp 3.2 .if username and password is correct from the database its login otherwise its give na incorrect username and password…so please send me code…….the above code is not working ….so please send in in cakephp 3.2…pls help me…

  3. hell Ders Mifty…
    Can you upload new topic in cakephp 3.2 for upload a file and save in db and fetch file path from db and display it and download that file.
    And how we use PayPal gateway API in cakephp 3.2…hope you will reply me…
    so please reply these Quuestion If Anyone Knows….
    Thanx Mandeep Kahleriya…….

  4. hell Dears Mifty
    Can you upload new topic in cake php 3.2 for upload a file and save in db and fetch file path from db and display it and download that file.
    And how we use PayPal gateway API in cake php 3.2…hope you will reply me…
    so please reply these Question If Anyone Knows….
    Thankyou
    Mandeep Kahleriya…….

  5. I was suggested this web site by my cousin. I am not certain whether or not this put up is written through him as nobody else recognize such certain about my problem.
    You’re amazing! Thanks!

  6. My developer is trying to persuade mᥱ tto mоve to .net from PHP.
    I haᴠe always disliked thee idea bеcauѕe of the costs.
    But he’s tryioong none tɦᥱ less. I’ve been սsing Movable-type on vаrious websites for ɑbout ɑ year and am
    nervous about switching tto ɑnother platform.
    ӏ haνe heard goood tҺings aƄout blogengine.net.

    Іs therе a way I can import all myy wordpress posts іnto it?
    Anny help ѡould ƅе гeally appreciated!

  7. Hi! Can you describe the mechanism of saving social profile photo with different providers (like facebook, google) and Vkontakte or Yandex? I`ve tried to save image from url, but i can`t get the image extention. E.g., Yandex gives url of image like http://upics.yandex.net/193130706/normal . Facebook and Google urls had size parameters after image extention, like https://lh3.googleusercontent.com/-QCkd4bBSRWs/AAAAAAAAAAI/AAAAAAAAAEs/KRyNtO1zabo/photo.jpg?sz=200

    Can you recommend general solution for all providers, or it needed to write different code for each of them?
    Appreciate for any help of advice 🙂

  8. Hey this is somewhat of off topic but I was wanting to know
    if blogs use WYSIWYG editors or if you have to manually
    code with HTML. I’m starting a blog soon but have no coding knowledge so I wanted to get guidance from someone with experience.
    Any help would be enormously appreciated!

  9. you are in point of fact a just right webmaster. The web site
    loading velocity is incredible. It sort of feels that you are doing any unique
    trick. Moreover, The contents are masterwork.
    you’ve done a fantastic job on this topic!

  10. I was very happy to find this web site. I want to to thank you
    for ones time due to this wonderful read!! I definitely enjoyed every little bit of it and i also have you saved
    to fav to check out new information on your web site.

Leave a Reply

Your email address will not be published.

*

Latest from CakePHP

Go to Top