22 Simple User Authentication
The Big Picture
If you're new to CakePHP, you'll be strongly tempted to copy and paste this code for use in your mission critical, sensitive-data-handling production application. Resist ye: this chapter is a discussion on Cake internals, not application security. While I doubt we'll provide for any extremely obvious security pitfalls, the point of this example is to show you how Cake's internals work, and allow you to create a bulletproof brute of an application on your own.
Cake has access control via its built-in ACL engine, but what about user authentication and persistence? What about that?
Well, for now, we've found that user authentication systems vary from application to application. Some like hashed passwords, others, LDAP authentication - and almost every app will have User models that are slightly different. For now, we're leaving it up to you. Will this change? We're not sure yet. For now, we think that the extra overhead of building this into the framework isn't worth it, because creating your own user authentication setup is easy with Cake.
You need just three things:
- A way to authenticate users (usually done by verifying a user's identity with a username/password combination)
- A way to persistently track that user as they navigate your application (usually done with sessions)
- A way to check if a user has been authenticated (also often done by interacting with sessions)
In this example, we'll create a simple user authentication system for a client management system. This fictional application would probably be used by an office to track contact information and related notes about clients. All of the system functionality will be placed behind our user authentication system except for few bare-bones, public-safe views that shows only the names and titles of clients stored in the system.
We'll start out by showing you how to verify users that try to access the system. Authenticated user info will be stored in a PHP session using Cake's Session Component. Once we've got user info in the session, we'll place checks in the application to make sure application users aren't entering places they shouldn't be.
One thing to note - authentication is not the same as access control. All we're after in this example is how to see if people are who they say they are, and allow them basic access to parts of the application. If you want to fine tune this access, check out the chapter on Cake's Access Control Lists. We'll make notes as to where ACLs might fit in, but for now, let's focus on simple user authentication.
I should also say that this isn't meant to serve as some sort of primer in application security. We just want to give you enough to work with so you can build bulletproof apps of your own.
Authentication and Persistence
First, we need a way to store information about users trying to access our client management system. The client management system we're using stores user information in a database table that was created using the following SQL:
Table 'users', Fictional Client Management System Database
CREATE TABLE `users` ( `id` int(11) NOT NULL auto_increment, `username` varchar(255) NOT NULL, `password` varchar(32) NOT NULL, `first_name` varchar(255) NOT NULL, `last_name` varchar(255) NOT NULL, PRIMARY KEY (`id`) )
CREATE TABLE `users` (`id` int(11) NOT NULL auto_increment,`username` varchar(255) NOT NULL,`password` varchar(32) NOT NULL,`first_name` varchar(255) NOT NULL,`last_name` varchar(255) NOT NULL,PRIMARY KEY (`id`))
Pretty simple, right? The Cake Model for this table can be pretty bare:
<?php
class User extends AppModel
{
var $name = 'User';
}
?> <?phpclass User extends AppModel{var $name = 'User';}?>
First thing we'll need is a login view and action. This will provide a way for application users to attempt logins and a way for the system to process that information to see if they should be allowed to access the system or not. The view is just a HTML form, created with the help of Cake's Html Helper:
/app/views/users/login.thtml
<?php if ($error): ?>
<p>The login credentials you supplied could not be recognized. Please try again.</p>
<?php endif; ?>
<form action="<?php echo $html->url('/users/login'); ?>" method="post">
<div>
<label for="username">Username:</label>
<?php echo $html->input('User/username', array('size' => 20)); ?>
</div>
<div>
<label for="password">Password:</label>
<?php echo $html->password('User/password', array('size' => 20)); ?>
</div>
<div>
<?php echo $html->submit('Login'); ?>
</div>
</form>
<?php if ($error): ?><p>The login credentials you supplied could not be recognized. Please try again.</p><?php endif; ?><form action="<?php echo $html->url('/users/login'); ?>" method="post"><div><label for="username">Username:</label><?php echo $html->input('User/username', array('size' => 20)); ?></div><div><label for="password">Password:</label><?php echo $html->password('User/password', array('size' => 20)); ?></div><div><?php echo $html->submit('Login'); ?></div></form>
This view presents a simple login form for users trying to access the system. The action for the form is /users/login, which is in the UsersController and looks like this:
/app/controllers/users_controller.php (partial)
<?php
class UsersController extends AppController
{
function login()
{
//Don't show the error message if no data has been submitted.
$this->set('error', false);
// If a user has submitted form data:
if (!empty($this->data))
{
// First, let's see if there are any users in the database
// with the username supplied by the user using the form:
$someone = $this->User->findByUsername($this->data['User']['username']);
// At this point, $someone is full of user data, or its empty.
// Let's compare the form-submitted password with the one in
// the database.
if(!empty($someone['User']['password']) && $someone['User']['password'] == $this->data['User']['password'])
{
// Note: hopefully your password in the DB is hashed,
// so your comparison might look more like:
// md5($this->data['User']['password']) == ...
// This means they were the same. We can now build some basic
// session information to remember this user as 'logged-in'.
$this->Session->write('User', $someone['User']);
// Now that we have them stored in a session, forward them on
// to a landing page for the application.
$this->redirect('/clients');
}
// Else, they supplied incorrect data:
else
{
// Remember the $error var in the view? Let's set that to true:
$this->set('error', true);
}
}
}
function logout()
{
// Redirect users to this action if they click on a Logout button.
// All we need to do here is trash the session information:
$this->Session->delete('User');
// And we should probably forward them somewhere, too...
$this->redirect('/');
}
}
?> <?phpclass UsersController extends AppController{function login(){//Don't show the error message if no data has been submitted.$this->set('error', false);// If a user has submitted form data:if (!empty($this->data)){// First, let's see if there are any users in the database// with the username supplied by the user using the form:$someone = $this->User->findByUsername($this->data['User']['username']);// At this point, $someone is full of user data, or its empty.// Let's compare the form-submitted password with the one in// the database.if(!empty($someone['User']['password']) && $someone['User']['password'] == $this->data['User']['password']){// Note: hopefully your password in the DB is hashed,// so your comparison might look more like:// md5($this->data['User']['password']) == ...// This means they were the same. We can now build some basic// session information to remember this user as 'logged-in'.$this->Session->write('User', $someone['User']);// Now that we have them stored in a session, forward them on// to a landing page for the application.$this->redirect('/clients');}// Else, they supplied incorrect data:else{// Remember the $error var in the view? Let's set that to true:$this->set('error', true);}}}function logout(){// Redirect users to this action if they click on a Logout button.// All we need to do here is trash the session information:$this->Session->delete('User');// And we should probably forward them somewhere, too...$this->redirect('/');}}?>
Not too bad: the contents of the login() action could be less than 20 lines if you were concise. The result of this action is either 1: the user information is entered into the session and forwarded to the landing page of the app, or 2: kicked back to the login screen and presented the login form (with an additional error message).
Access Checking in your Application
Now that we can authenticate users, let's make it so the application will kick out users who try to enter the system from points other than the login screen and the "basic" client directory we detailed earlier.
One way to do this is to create a function in the AppController that will do the session checking and kicking for you.
/app/app_controller.php
<?php
class AppController extends Controller
{
function checkSession()
{
// If the session info hasn't been set...
if (!$this->Session->check('User'))
{
// Force the user to login
$this->redirect('/users/login');
exit();
}
}
}
?> <?phpclass AppController extends Controller{function checkSession(){// If the session info hasn't been set...if (!$this->Session->check('User')){// Force the user to login$this->redirect('/users/login');exit();}}}?>
Now you have a function you can use in any controller to make sure users aren't trying to access controller actions without logging in first. Once this is in place you can check access at any level - here are some examples:
Forcing authentication before all actions in a controller
<?php
class NotesController extends AppController
{
// Don't want non-authenticated users looking at any of the actions
// in this controller? Use a beforeFilter to have Cake run checkSession
// before any action logic.
function beforeFilter()
{
$this->checkSession();
}
}
?> <?phpclass NotesController extends AppController{// Don't want non-authenticated users looking at any of the actions// in this controller? Use a beforeFilter to have Cake run checkSession// before any action logic.function beforeFilter(){$this->checkSession();}}?>
Forcing authentication before a single controller action
<?php
class NotesController extends AppController
{
function publicNotes($clientID)
{
// Public access to this action is okay...
}
function edit($noteId)
{
// But you only want authenticated users to access this action.
$this->checkSession();
}
}
?> <?phpclass NotesController extends AppController{function publicNotes($clientID){// Public access to this action is okay...}function edit($noteId){// But you only want authenticated users to access this action.$this->checkSession();}}?>
Now that you have the basics down, you might want to venture out on your own and implement some advanced or customized features past what has been outlined here. Integration with Cake's ACL component might be a good first step.
