Simple Auth with Users HABTM Groups in CakePHP

In almost every project I need to setup some sort of login, and usually it requires group access to certain data depending on different criteria.

For example I may have groups Admin+Member and I want Members to only be able to see other Users Profiles if the other Profile is active.

Little Helper

This function checks if the user is in a group.

config/bootstrap.php
function in_group($group,$groups) {
        if (isset($groups['Group'])) {
                $groups = $groups['Group'];
        }
        if (!$groups || !is_array($groups) || empty($groups)) {
                return false;
        }
        $allow = false;
        foreach($groups as $_group) {
                if ($_group['name'] == $group) {
                        $allow = true;
                        break;
                }
        }
        return $allow;
}

The Tables

users
CREATE TABLE IF NOT EXISTS `users` (
  `id` int(11) NOT NULL auto_increment,
  `username` char(50) NOT NULL,
  `password` char(50) NOT NULL,
  `active` tinyint(4) NOT NULL default '0',
  `email` varchar(255) NOT NULL,
  `created` datetime default NULL,
  `modified` datetime default NULL,
  PRIMARY KEY  (`id`)
);
groups
CREATE TABLE IF NOT EXISTS `groups` (
  `id` int(11) NOT NULL auto_increment,
  `name` varchar(128) NOT NULL,
  `created` datetime default NULL,
  `modified` datetime default NULL,
  PRIMARY KEY  (`id`)
);
groups_to_users
CREATE TABLE IF NOT EXISTS `groups_to_users` (
  `group_id` int(11) NOT NULL,
  `user_id` int(11) NOT NULL,
  PRIMARY KEY  (`group_id`,`user_id`)
);

The Models

Before we get started, lets setup the models.

I have added some validation to get you started.

app_model.php
class AppModel extends Model {
        function validUnique($value,$field){
                if (is_array($field)) {
                        foreach($field as $v) $field = $v;
                }
                $conditions = array();
                $conditions["{$this->name}.{$field}"] = $value;
                if (isset($this->id) && $this->id) {
                        $conditions["{$this->name}.{$this->primaryKey}"] = "!={$this->id}";
                }
                $this->recursive = -1;
                return !$this->hasAny($conditions);
        }
    function required($field) {
        foreach($field as $k=>$v){}
        return $v?true:false;
        }
}
models/user.php
class User extends AppModel {

    var $name = 'User';
    var $displayField = 'username';

    var $validate = array(
        'username' => array(
                        'not unique' => array(
                                'rule'=>array('validUnique','username'),
                                'required' => true,
                        ),
            'must be alpha-numeric' => array(
                                'rule' => 'alphaNumeric',
                                'required' => true,
                        ),
                        'required field' => array(
                                'rule' => 'required',
                                'required' => true,
                        ),
        ),
        'email' => array(
                        'not unique' => array(
                                'rule'=>array('validUnique','email'),
                                'required' => true,
                        ),
            'invalid email' => array(
                                'rule'=>'email',
                                'required' => true,
                        ),
                        'required field' => array(
                                'rule' => 'required',
                                'required' => true,
                        ),
        ),
        'password_confirm' => array(
            'does not match' => array(
                                'rule' => 'validPasswordConfirm',
                                'required' => false,
                        ),
        ),
    );
   
        var $hasAndBelongsToMany = array(
                'Group' => array(
                        'className' => 'Group',
                        'joinTable' => 'groups_to_users',
                        'foreignKey' => 'user_id',
                        'associationForeignKey' => 'group_id',
                        'with' => 'GroupToUser',
                ),
        );    

    function validPasswordConfirm($value){
        if ($this->data[$this->alias]['password_change'] != $this->data[$this->alias]['password_confirm']) {
            return false;
        }
        return true;
    }
}
models/group.php
class Group extends AppModel {
        var $name = 'Group';
        var $validate = array(
                'name' => array(
                        'required field' => array(
                                'rule' => 'required',
                                'required' => true,
                        ),
                        'not unique' => array(
                                'rule'=>array('validUnique','name'),
                                'required' => true,
                        ),
                ),
        );     


}

The Controllers

The app_controller will do the work of setting the related Groups into the Auth data.

app_controller.php
class AppController extends Controller {
    var $components = array('Auth');

        function beforeFilter() {
        $this->__setAuthUser();

                // allow pages to be displayed to anyone
                if ($this->name=='Pages') {
                        $this->Auth->allow('display');
                }

        parent::beforeFilter();
        }

        function __setAuthUser() {
                $this->Auth->userScope = array('User.active = 1');

                // read the auth info
                $auth = $this->Session->read('Auth');

                // set the auth users groups           
                if (!isset($auth['GroupUser']) || $auth['GroupUser']!=$auth['User']['id']) {
                        $User = ClassRegistry::init('User');
                        $auth = $User->find('first',array('conditions'=>array('User.id'=>$this->Auth->user('id'))));
                        $auth['GroupUser'] = $this->Auth->user('id');
                }

                // check permissions for admin access          
                if(isset($this->params[Configure::read('Routing.admin')])) {
                        if (isset($auth['User']['id']) && !in_group('admin',$auth)) {
                                $this->_flash(__('You do not have permission to access the administration.',true),'error');
                                $this->redirect(array('admin'=>false,'controller'=>'users','action'=>'login'));
                        }
                }

                // save and set the auth info
                $this->Session->write('Auth',$auth);
                $this->set('auth',isset($auth['User'])?$auth:false);
        }

        function _flash($message,$type='message') {
                $messages = (array)$this->Session->read('Message.multiFlash');
                $messages[] = array('message'=>$message, 'layout'=>'default', 'params'=>array('class'=>$type));
                $this->Session->write('Message.multiFlash', $messages);
        }

}

Next I have provided a fully working users_controller, complete with account register, account management and lost password email.

I have also included some extra goodies such as habtm checkbox, breadcrumbs, search and multi-actions.

controllers/users_controller.php
class UsersController extends AppController {
        var $name = 'Users';
        var $components = array('Email');
       
        var $multiActions = array(
                'multi_delete',
        );
       
        function beforeFilter() {
                parent::beforeFilter();
                $this->Auth->allow('register','password');
                if (isset($this->passedArgs['key'])) {
                        $this->Auth->allow('account');
                }
        }
       
        //
        // PUBLIC ACTIONS
        //
        function index() {

                $title = 'My Account';

                // set breadcrumb
                $breadcrumb = array();
                $breadcrumb[] = array(
                        'name' => 'Home',
                        'href' => '/',
                );
                $breadcrumb[] = array(
                        'name' => $title,
                );

                $this->set(compact('title','breadcrumb'));

        }
       
        function account() {
                // load account from key
                if (isset($this->passedArgs['key'])) {
                        $this->recursive = -1;
                        $user = $this->User->findByPassword($this->passedArgs['key']);
                        if (!$user) {
                                $this->_flash(__('Invalid Key.', true),'error');
                                $this->redirect(array('action'=>'index'));
                        }
                        $this->Auth->login($user);
                }

                // load account from auth user
                $id = $this->Auth->user('id');
                if (!$id) {
                        $this->_flash(__('Invalid User.', true),'error');
                        $this->redirect(array('action'=>'index'));
                }
               
                // process submit
                if (!empty($this->data)) {
                        $error = false;
                        $this->User->create();
                        $this->User->id = $id;
                        if ($this->data['User']['password_change']) {
                                $this->data['User']['password'] = $this->Auth->password($this->data['User']['password_change']);
                        }
                        if ($this->User->save($this->data,true,array('email','username','password','password_change','password_confirm'))) {
                                $this->_flash(__('Your Account has been saved.', true),'success');
                                $this->redirect(array('action'=>'index'));
                        }
                        $this->_flash(__('Your Account could not be saved. Please, try again.', true),'warning');
                }
                if (empty($this->data)) {
                        $this->data = $this->User->read(null, $id);
                }
               
                $title = 'Modify Details';

                // set breadcrumb
                $breadcrumb = array();
                $breadcrumb[] = array(
                        'name' => 'Home',
                        'href' => '/',
                );
                $breadcrumb[] = array(
                        'name' => 'My Account',
                        'href' => array('controller'=>'users','action'=>'index'),
                );
                $breadcrumb[] = array(
                        'name' => $title,
                );

                $this->set(compact('title','breadcrumb'));
        }
       
        function register() {
                $title = 'Register';

                // process submit
                if (!empty($this->data)) {
                        if (!$this->data['User']['password_confirm']) {
                                $this->data['User']['password_confirm']=' ';
                        }
                        $this->data['User']['password'] = $this->Auth->password($this->data['User']['password_change']);
                        $this->data['User']['active'] = 1;
                        $this->User->create();
                        if ($this->User->save($this->data,true,array('email','username','active','password','password_change','password_confirm'))) {
                                $this->Auth->login($this->data);
                                $this->_flash(__('Your Account has been created.', true),'success');
                                $this->redirect(array('action'=>'index'));
                        }
                        $this->_flash(__('Your Account could not be created. Please, try again.', true),'warning');
                }

                // set breadcrumb
                $breadcrumb = array();
                $breadcrumb[] = array(
                        'name' => 'Home',
                        'href' => '/',
                );
                $breadcrumb[] = array(
                        'name' => 'My Account',
                        'href' => array('controller'=>'users','action'=>'index'),
                );
                $breadcrumb[] = array(
                        'name' => $title,
                );

                $this->set(compact('title','breadcrumb'));
        }
       
        function login() {
                $title = 'Login';

                // set breadcrumb
                $breadcrumb = array();
                $breadcrumb[] = array(
                        'name' => 'Home',
                        'href' => '/',
                );
                $breadcrumb[] = array(
                        'name' => 'My Account',
                        'href' => array('controller'=>'users','action'=>'index'),
                );
                $breadcrumb[] = array(
                        'name' => $title,
                );

                $this->set(compact('title','breadcrumb'));
        }
       
        function logout() {
                $this->Session->delete('Auth.GroupUser');
                $this->redirect($this->Auth->logout());
        }
       
        function password() {
                $error = false;
               
                if (!$error) {
                        if (!isset($this->data['User']['lost_password']) || !$this->data['User']['lost_password']) {
                                $this->_flash(__('Please enter an email address or username.', true),'warning');
                                $error = true;
                        }
                }
               
                if (!$error) {
                        $this->User->recursive = -1;                   
                        $user = $this->User->findByUsername($this->data['User']['lost_password']);
                        if (!$user) {
                                $user = $this->User->findByEmail($this->data['User']['lost_password']);
                        }
                        if (!$user) {
                                $this->_flash(__('Account not found.', true),'error');
                                $error = true;
                        }
                }
               
                if (!$error) {
                        $this->Email->to = $user['User']['email'];
                        $this->Email->subject = 'New Password for '.Configure::read('Settings.app.name');
                        $this->Email->replyTo = Configure::read('Settings.app.email');
                        $this->Email->from = Configure::read('Settings.app.name').' <'.Configure::read('Settings.app.email').'>';
                        $this->Email->template = 'password';
                        $this->Email->sendAs = 'both';
                        $this->Email->delivery = Configure::read('Settings.smtp.delivery');
                        $this->Email->smtpOptions = array(
                                'port'=> 25,
                                'host' => 'localhost',
                                'timeout' => 30,
                                'username' => Configure::read('Settings.smtp.username'),
                                'password' => Configure::read('Settings.smtp.password'),
                        );
       
                        $this->set(compact('user'));

                        if (!$this->Email->send()) {
                                debug($this->Email);
                                $this->_flash(__('Email could not be sent.', true),'error');
                                $error = true;
                        }
                }

                if (!$error) {
                        $this->_flash(__(sprintf('New password has been sent to %s.',$user['User']['email']), true),'success');
                        $this->redirect(array('action'=>'login'));
                }              
               
                $this->login();
                $this->render('login');
        }
       
        //
        // ADMIN ACTIONS
        //

        function admin_index() {
                if (!isset($this->passedArgs['sort'])) {
                        $this->paginate['order'] = array('User.id'=>'desc');
                }

                // keywords
                if(isset($this->passedArgs['keywords'])) {
                        $keywords = $this->passedArgs['keywords'];
                        $this->paginate['conditions'][] = array(
                                'OR' => array(
                                        'User.id' => $keywords,
                                        'User.username LIKE' => "%$keywords%",
                                        'User.email LIKE' => "%$keywords%",
                                )
                        );
                        $this->data['Search']['keywords'] = $keywords;
                }

                // username
                if(isset($this->passedArgs['username'])) {
                        $this->paginate['conditions'][]['User.username LIKE'] = str_replace('*','%',$this->passedArgs['username']);
                        $this->data['Search']['username'] = $this->passedArgs['username'];
                }

                // email
                if(isset($this->passedArgs['email'])) {
                        $this->paginate['conditions'][]['User.email LIKE'] = str_replace('*','%',$this->passedArgs['email']);
                        $this->data['Search']['email'] = $this->passedArgs['email'];
                }

                // filter by group_id
                if (isset($this->passedArgs['group_id'])) {
                        $this->User->bindModel(array(
                                'hasOne' => array(
                                        'GroupToUser' => array(
                                                'className' => 'GroupToUser',
                                                'foreign_key' => 'user_id',
                                        ),
                                ),
                        ),false);
                        $this->paginate['conditions'][]['GroupToUser.group_id'] = $this->passedArgs['group_id'];
                        $this->data['Search']['group_id'] = $this->passedArgs['group_id'];
                }

                // load the data               
                $users = $this->paginate();

                // set related data
                $groups = $this->User->Group->find('list');
                $this->set(compact('users','groups'));
        }
       
        function admin_search() {
                $url =