CakePHP Authentication

After last weeks Auth component, it is now time to go into the full Authentication of a user. In order to use the full power of the Auth component, the table should be named “users”. In the table I created, there were a few different things put in, but for the sake of this, I will limit those.

CREATE TABLE IF NOT EXISTS `users` (
	`user_id` int(11) NOT NULL auto_increment,
	`username` varchar(25) NOT NULL,
	`password` varchar(250) NOT NULL,
	`full_name` varchar(250) NOT NULL,
	`email` varchar(250) NOT NULL,
	`remote_address` varchar(16) NOT NULL,
	`last_login` datetime default NULL,
	`last_login_ip` varchar(16) default NULL,
	`created` datetime NOT NULL,
	`modified` datetime default NULL,
	PRIMARY KEY  (`user_id`),
	UNIQUE KEY `username` (`username`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

In this table, there is a lot you really do not need, but here is the breakdown: ‘user_id’ is needed for my purposes, ‘username’ and ‘password’ are named as such to be able to use the Auth component methods. The other fields are for personalization (full_name and email). The next three are just for simplistic CYA that should always be good practice, grap the registered IP address, date the user last logged in and the IP they logged in from. Is this a foolproof way of CYA? No. But it starts you out on the right track. The last two I always put in all of my tables, as CakePHP updates those automatically, so this also helps to track when created and when changed.

Now that the table is done, we need to provide some quick validation for registration and such. In the model, the code should look similar to this:

var $name = 'User';
var $primaryKey = 'user_id';
var $validate = array(
	'username' => array(
		'alphaNumeric' => array(
			'rule'		=> 'alphaNumeric',
			'required'	=> true,
			'on'		=> 'create',
			'message'	=> 'Username must be only letters and numbers, no special characters'
		),
		'between' => array(
			'rule' 		=> array('between', 5, 20),
			'on'		=> 'create',
			'message'	=> 'Username must be between 5 and 20 characters',
		),
		'isUnique' => array(
			'rule'		=> 'isUnique',
			'on'		=> 'create',
			'message'	=> 'This username is already taken. Please choose a different one.'
		)
	),
	'email' => array(
		'rule'		=> array('email', true),
		'required'	=> true,
		'message'	=> 'Please provide a valid email address'
	),
);


The data validation is a topic for another post, and the Cookbook has a good entry for this as well. In this example, there is multiple validations for the username, and one for the email. We want the username to be only alphanumeric, between 5 and 20 characters in length, and it needs to be unique. The email just needs to be a valid email address. Each validation rule has its own error message. But we have provided simple validation for the model.

Now it is time to create/update the controller. If you have Baked the controller, then you have 5 actions, index, add, edit, delete, view. I am not particularly fond of having a “register” action named “add”, so I changed this around. Remember that if you are using the Auth component to allow this in the beforeFilter() function.

$this->Auth->allow('register');

Now in my “register” function, I started off with checking if the data set is empty. And if you Baked the controller, most of this should be in there already. The register function will have two main parts, if the form is filled out and if it is not/has errors. With using the Auth component, it is important to remember a basic part of the Auth with passwords, it hashes the passwords for you and includes the SALT string in the config file. So I will point this out right now. YOU MUST RESET THE PASSWORD FIELD IF YOU DO NOT WANT EMPTY STRINGS FOR PASSWORDS. This includes if you are using data validation for this field. Auth automatically appends whatever is in the password field and appends the SALT. So if someone leaves it blank, Auth will still append the SALT and it will never be blank. The best way to check this is with AJAX/javascript for the field on the form level, not the processing level. On the processing level, if the form is blank, or contains errors, clear the password field.

function register() {
	if (!empty($this->data)) {
		// code to be put here
	}

	$this->data['User']['password'] = ''; // reset the password field
}

This now checks for the form being filled out, and if not, resets that password field. (As a note, it is always best to have the password verified by entering this twice, but for right now, we are skipping this). If the form is empty, we reset data, and have the user fill out the form again. If not, we start to look to see what data we have to work with. I always suggest to use the Data Sanitizer component to help clean up the fields. This helps ensure the data will not empty your tables or other problems occur (not all web surfers are harmless). With the sanitation, set the data to the model so the validation rules can help out.

function register() {
	if (!empty($this->data)) {
		$clean = new Sanitize();
		$clean->clean($this->data);
		$this->User->set($this->data);
		// Sanitize the data using this only as an example, but all fields that require the user to type should be included
		$this->data['User']['username'] = $clean->paranoid($this->data['User']['username']);
	}

	$this->data['User']['password'] = ''; // reset the password field
}

Now we need to check those validation rules, and CakePHP has a perfect function to help do this. By using the $this->MODEL->validates() function, it will check any validation rules that have been specified for the model. If there is errors, then it kicks the user back to the form with those error messages. If it does validate, then then we can proceed to create the account.

function register() {
	if (!empty($this->data)) {
		$clean = new Sanitize();
		$clean->clean($this->data);
		$this->User->set($this->data);

		// Sanitize the data using this only as an example, but all fields that require the user to type should be included
		$this->data['User']['username'] = $clean->paranoid($this->data['User']['username']);

		// Check for validation rules
		if ($this->User->validates()) {
			// All validation rules have been satisfied, so we need to check this in to the table
                        $this->User->create();

			if ($this->User->save($this->data)) {
				$this->Session->setFlash(__('The User has been saved. Please Login using the new identification.', true));
				$this->redirect(array('action'=>'login'));
			} else {
				$this->Session->setFlash(__('The User could not be saved. Please, try again.', true));
			}
		}
	}

	$this->data['User']['password'] = ''; // reset the password field
}

First we check to see if the form has been filled out, and sent. If it has, we sanitize the data, then validate it. If it validates, we create the record. If the record can not be created, for whatever reason, it kicks them back. If the data can be saved, and the record is created, it then redirects them back to the “login” page. This is just a simplistic registration form. We can extend this to use a full blown profile table, add more sanitation, and more validation. But for right now, this is good. We now need to allow them to login. And the Auth component makes it easy.

We can make it as easy as we need, or as hard as we need. With using the Auth component, all we would really need to do is

function login() {
}

You would still need to create the view for it, having the username and password fields there, but if you would like more info in the Session, then there is just a little more to do. The first is to check to see if the login form has been filled out, then check for it to be blank, sanitize the data, and now we get to some meat here. With the Auth, we can grab other fields and set them to be used in the Session

function login() {
	if ($this->Auth->user()) {
		if (!empty($this->data)) {
			$san = new Sanitize();
			// Sanitize the input of items we do not know what the end user put in.
			$this->data['User']['username'] = $san->paranoid($this->data['User']['username']);

			// Grab the data from the User table and set them to the cookie array
			$cookie = array();
			$cookie['user_id'] = $this->Auth->user('user_id');
			$cookie['full_name'] = $this->Auth->user('full_name');
			$cookie['ast_login'] = $this->Auth->user('last_login');
			$cookie['last_login_ip'] = $this->Auth->user('last_login_ip');

			$this->Session->write('Auth.User', $cookie);
		}
	}
}

The Session->write() method will write these to the Session with the cols/vals in a foreach loop. So name the elements in the array with something useful if you intend to use them later so that you can recall them. The CakePHP API documentation provides great information on the write() function, but real quick, it is the name, then value. The next part I added to this was to update the User table with the info:

$update['User']['user_id'] = $userid;
$update['User']['last_login'] = $this->data['User']['last_login'];
$update['User']['last_login_ip'] = $this->data['User']['last_login_ip'];
$this->User->save($update, false, array('user_id', 'last_login', 'last_login_ip'));

After successful login, we can redirect using the Auth redirect, which is handy to redirect users back to pages where authentication was needed to get in.

$this->redirect($this->Auth->redirect());

One other thing I added in, just because I can not stand forms and logins that allow you to log in even if you are already logged in, is to check to see if they are logged in. If they are logged in, and try to log in again, I send them back to the home page, with a message reminding them of their current status. I also clear out the Auth message in case anything is lingering in there.

if (empty($this->data)) {
	// Check to see if they are logged in
	if (isset($_SESSION['Auth']['User']['username'])){
		$this->Session->setFlash(__('You are already logged in. ', true));
		$this->redirect(array('action' => 'index'));
	}

	// Perform other checks on the empty data and set up the form.
	$cookie = $this->Session->read('Auth.User');
	if (!is_null($cookie)) {
		if ($this->Auth->login($cookie)) {
			//  Clear auth message, just in case we use it.
			$this->Session->del('Message.auth');
			$this->redirect($this->Auth->redirect());
		}
	}
}

Seems like too much? Well, with my long-winded explanations, yes, but it is quite simple and quick when you get to coding it, and one more time, the entire login function

function login() {
	if ($this->Auth->user()) {
		if (!empty($this->data)) {
			$san = new Sanitize();
			// Sanitize the input of items we do not know what the end user put in.
			$this->data['User']['username'] = $san->paranoid($this->data['User']['username']);

			// Grab the data from the User table and set them to the cookie array
			$cookie = array();
			$cookie['user_id'] = $this->Auth->user('user_id');
			$cookie['full_name'] = $this->Auth->user('full_name');
			$cookie['ast_login'] = $this->Auth->user('last_login');
			$cookie['last_login_ip'] = $this->Auth->user('last_login_ip');

			$this->Session->write('Auth.User', $cookie); 

			$update['User']['user_id'] = $userid;
			$update['User']['last_login'] = $this->data['User']['last_login'];
			$update['User']['last_login_ip'] = $this->data['User']['last_login_ip'];
			$this->User->save($update, false, array('user_id', 'last_login', 'last_login_ip'));

			$this->redirect($this->Auth->redirect());
		}
	}

	if (empty($this->data)) {
		// Check to see if they are logged in
		if (isset($_SESSION['Auth']['User']['username'])){
			$this->Session->setFlash(__('You are already logged in. ', true));
			$this->redirect(array('action' => 'index'));
		}

		// Perform other checks on the empty data and set up the form.
		$cookie = $this->Session->read('Auth.User');
		if (!is_null($cookie)) {
			if ($this->Auth->login($cookie)) {
				//  Clear auth message, just in case we use it.
				$this->Session->del('Message.auth');
				$this->redirect($this->Auth->redirect());
			}
		}
	}

}

2 thoughts on “CakePHP Authentication”

Comments are closed.