DarkAuth - another way...

2 : The Component Code

By Chris Walker (theChrisWalker)
I wrote this initially for Cake 1.1 - basing it on ideas from "obAuth" by Steve Oliveira, but upgraded it to 1.2 rather than using the built in Auth component, mostly because this works how I want it to and, once setup, is really easy to use.

Main Features:

- Per action / per controller / inline access control
- Optional Group support for HABTM and User BelongsTo Group associations
- A "super-user" functionality allowing one group automatic access granted
- Optional tamper-proof Cookie support
- Custom password hashing to suit your Model
So here's the code for the component, it's quite a chunk, then on the next page I'll describe the setup.

Component Class:

<?php 
class DarkAuthComponent extends Object {

  var 
$user_model_name 'User';
  var 
$users_controller_name 'Users'
  var 
$user_name_field 'email'//e.g. email or firstname or username...
  
var $user_name_case_folding 'lower'//do you want to case fold the username before verifying? either 'lower','upper','none', to change case to lower/upper/leave it alone before matching.
  
var $user_pass_field 'password';
  var 
$user_live_field 'live'// surely you have a field in you users table to show whether the user is active or not? set to null if not.
  
var $user_live_value 1;
  var 
$group_model_name 'Group'//Group for access control if used. NB: DON'T CALL requiresAuth with Groups if no group model. it will error.
  
var $group_name_field 'name'// the name of the field used for the groups name. This will be used to check against passed groups.
  
var $HABTM true//set to false if you don use a HABTM group relationship.
  
var $superuser_group 'Root'//if you want a single group to have automatically granted access to any restriction.
  
var $login_view '/login';  //this is the login view, usually {user_controller}/login but you may have changed the routes.
  
var $deny_view '/deny';  //this is the default denied access view.
  
var $logout_page '/'// NB this is were to redirect AFTER logout by default
  
var $login_failed_message '<p class="error">Login Failed, Please check your details and try again.</p>'//This message is setFlash()'d on failed login.
  
var $allow_cookie true//Allow use of cookies to remember authenticated sessions.
  
var $cookie_expiry '+6 Months'//how long until cookies expire. format is "strtotime()" based (http://php.net/strtotime).
    //var $session_secure_key = 'sRmtVStkedAdlxBy'; //some random stuff that someone is unlikey to guess. 
    
var $session_secure_key 'sJfkgD420YsfhC2k4Abs';

    
/*
   * You can edit this function to explain how you want to hash your passwords.
   */
  
function hasher($plain_text){

    
$hashed md5('dark'.$plain_text.'cake');

    return 
$hashed;
  }

##########################################################################
 /*
  * DON'T EDIT THESE OR ANYTHING BELOW HERE UNLESS YOU KNOW WHAT YOU'RE DOING
  */
  
var $controller;
  var 
$here;
  var 
$components=array('Session');
  var 
$current_user;
  var 
$from_session;
  var 
$from_post;
  var 
$from_cookie;

  function 
startup(&$controller){
  
      
//Let's check they have changed the secure key from the default.
        
if($this->session_secure_key == 'sRmtVStkedAdlxBy'){
            die(
'<p>Please change the DarkAuth::session_secure_key value from it default.</p>');
        }
        
    
$this->controller $controller;
    
    
$this->here substr($controller->here,strlen($controller->base));
    
    
$this->controller->_login();
    
    
//now check session/cookie info.
    
$this->getUserInfoFromSessionOrCookie();

    
//now see if the calling controller wants auth (except for the users/login or logout or deny actions)
    
if( array_key_exists('DarkAuth_requiresAuth'$controller) ){
      
// We want Auth for any action here
      
if(array_key_exists('DarkAuth_ifAccessDenied',$controller)){
              
$deny $controller->DarkAuth_ifAccessDenied;
            }else{
              
$deny null;
            }
      
$this->requiresAuth($controller->DarkAuth_requiresAuth,$deny);
    }
    
//finally give the view access to the data
    
$this->controller->set('DarkAuth_User',$this->getUserInfo());
  }

    function 
secure_key(){
        static 
$key;
        if(!
$key){
            
$key md5(Configure::read('Security.salt').'!DarkAuth!'.$this->session_secure_key);
        }
        return 
$key;
    }

  function 
requiresAuth($groups=array(),$deny_redirect=null){
        if( empty(
$this->current_user) ){
            
// Still no info! render logion page!
            
if($this->from_post){
                
$this->Session->setFlash($this->login_failed_message); 
            }
      
$this->controller->render($this->login_view);
      exit();
    }else{
      if(
$this->from_post){
                
// user just authed, so redirect to avoid post data refresh.
                
$this->controller->redirect($this->here);
                exit();
      }
      
// User is authenticated, so we just need to check against the groups.
      
if( empty($groups) ){
        
// No Groups specified so we are good to go!
        
$deny false;
      }else{
        
$deny = !$this->isAllowed($groups);
      }
      if(
$deny){
        
// Current User Doesn't Have Access! DENY
        
if($deny_redirect){
                    
$this->controller->redirect($deny_redirect);
                    exit();
                }else{
                    
$this->controller->render($this->deny_view);
                    exit();
                }
      }
    }
    return 
true;
  }
 
  function 
isAllowed($groups=array()){
    if( empty(
$this->current_user) ){
      
// No information about the user! FALSE
      
return false;
    }else{
      
// User is authenticated, so we just need to check against the groups.
      
if(!is_array($groups)){ $groups[0] = $groups; }
      if( empty(
$groups) ){
        
// No Groups specified so we are good to go! TRUE
        
return true;
      }else{
                
//first check superuser access.
                
if($this->superuser_group){
                    
array_unshift($groups,$this->superuser_group);
                }
        
// Check each group.
        
if(!$this->HABTM){
          
// Single relation ship.
          
foreach($groups as $g){
            if(
              
$this->current_user[$this->group_model_name]['id'] == $g ||
              
$this->current_user[$this->group_model_name][$this->group_name_field] == $g
            
){
              
// Our Authenticated user matches a group! TRUE
              
return true;
            }
          }
        }else{
          
//HasAndBelongToMany relationship. we search the other way around...
          
foreach($this->current_user[$this->group_model_name] as $g){
            if(
              
in_array($g['id'],$groups) ||
              
in_array($g[$this->group_name_field],$groups)
            ){
              
// Our Authenticated user matches a group! TRUE
              
return true;
            }
          }
        }
        
//No Access this time. FALSE
        
return false;
      }
    }
  }

  function 
getCookieInfo(){
        if(!
array_key_exists('DarkAuth',$_COOKIE)){
            
//No cookie
            
return false;
        }
        list(
$hash,$data) = explode("|||",$_COOKIE['DarkAuth']);
        if(
$hash != md5($data.$this->secure_key())){
            
//Cookie has been tampered with
            
return false;
        }
        
$crumbs unserialize(base64_decode($data));
        if(!
array_key_exists('username',$crumbs) ||
             !
array_key_exists('password',$crumbs) ||
             !
array_key_exists('expiry'  ,$crumbs)){
            
//Cookie doesn't contain the correct info.
            
return false;
        }
        if(!isset(
$crumbs['expiry']) || $crumbs['expiry'] <= time()){
            
//Cookie is out of date!
            
return false;
        }
        
//All checks passed, cookie is genuine. remove expiry time and return
        
unset($crumbs['expiry']);
        return 
$crumbs;        
  }
  
  function 
setCookieInfo($data,$expiry=0){
      if(
$data === false){
            
//remove cookie!
            
$cookie false;
            
$expiry 100//should be in the past enough!
      
}else{
            
$serial base64_encode(serialize($data));
            
$hash md5($serial.$this->secure_key());
            
$cookie $hash."|||".$serial;
        }
        if(
$_SERVER['SERVER_NAME']=='localhost'){
          
$domain null;
        }else{
          
$domain '.'.$_SERVER['SERVER_NAME'];
        }
        return 
setcookie('DarkAuth'$cookie$expiry$this->controller->base$domain);
  }

  function 
authenticate_from_post($data){
        
$this->from_post true;
        return 
$this->authenticate($data);
  }
  function 
authenticate_from_session($data){
        
$this->from_session true;
        return 
$this->authenticate($data);
    }
    function 
authenticate_from_cookie(){
        
$this->from_cookie true;
        return 
$this->authenticate($this->getCookieInfo());
    }
    
  function 
authenticate($data){
        if(
$data === false){
            
$this->destroyData();
            return 
false;
        }
    if(
$this->from_session || $this->from_cookie){
      
$hashed_password $data['password'];
    }else{
      
$hashed_password $this->hasher($data['password']);
    }    
    switch(
$this->user_name_case_folding){
            case 
'lower':
                
$data['username'] = strtolower($data['username']);
                break;            
            case 
'upper';
                
$data['username'] = strtoupper($data['username']);
                break;
            default: break;
    }
    
$conditions = array(
      
$this->user_model_name.".".$this->user_name_field => $data['username'],
      
$this->user_model_name.".".$this->user_pass_field => $hashed_password
    
);
    if(
$this->user_live_field){
      
$field $this->user_model_name.".".$this->user_live_field;
      
$conditions[$field] = $this->user_live_value;
    };
    
$check $this->controller->{$this->user_model_name}->find($conditions);
    if(
$check){
       
$this->Session->write($this->secure_key(),$check);
       if(
                  
$this->allow_cookie && //check we're allowing cookies
                  
$this->from_post && //check this was a posted login attempt.
                  
array_key_exists('remember_me',$data) && //check they where given the option!
                  
$data['remember_me'] == true //check they WANT a cookie set
             
){
                 
// set our cookie!
                 
if(array_key_exists('cookie_expiry',$data)){
                   
$this->cookie_expiry $data['cookie_expiry'];
                 }else{
                   
$this->cookie_expiry;
                 }
                 if(
strtotime($this->cookie_expiry) <= time()){
                    
// Session cookie? might as well not set at all...
                 
}else{
                   
$expiry strtotime($this->cookie_expiry);
                   
$this->setCookieInfo(array('username'=>$data['username'], 'password'=>$hashed_password'expiry'=>$expiry), $expiry);
                 } 
             }
       
$this->current_user $check;
       return 
true;
    }else{
       
$this->destroyData();
       return 
false;
    }
  }

  function 
getUserInfo(){
    return 
$this->current_user[$this->user_model_name];
  }
  function 
getAllUserInfo(){
    return 
$this->current_user;
  }

  function 
destroyData(){
    
$this->Session->delete($this->secure_key());
    if(
$this->allow_cookie){
                
$this->setcookieInfo(false); 
        }
    
$this->current_user null;
  }

  function 
logout($redirect=false){
    
$this->destroyData();
    if(!
$redirect){
      
$redirect $this->logout_page;
    }
    
$this->controller->redirect($redirect);
    exit();
  }

  function 
getUserInfoFromSessionOrCookie(){
    if( !empty(
$this->current_user) ){ 
      return 
false
    }
    if(
$this->Session->valid() && $this->Session->check($this->secure_key()) ){
      
$this->current_user $this->Session->read($this->secure_key());
      return 
$this->authenticate_from_session(array(
        
'username' => $this->current_user[$this->user_model_name][$this->user_name_field],
        
'password' => $this->current_user[$this->user_model_name][$this->user_pass_field],
      ));
    }elseif(
$this->allow_cookie){
            return 
$this->authenticate_from_cookie();
    }
  }
}
?>

Got all that... good now let's set it up!

Page 3: 5 Steps to Setup (well, maybe 6)

Comments 617

CakePHP Team Comments Author Comments
 

Comment

1 Thanks for a great article

A very thorough and helpful article. Good job in creating it in a way to suit the most amount of people.

However the one thing that will probably keep me from using it, is that my tastes are a bit opposite to yours. I prefer to define access names and check for those. I have predefined what access the groups have and check if a user has access through it's group.

I dont know if that is clear, but in your example, I would define that the SecretsController requires "Secret level access" and either the (or one of the) group that logged in user belongs to has access to it or not.

The gain from doing it this way is that I don't have to edit my secrets controller when adding,removing or editing groups . This I feel is more inline with the cake behavior of controllers.

Anyways, just wanted to offer my 2 cents and thank you for your contribution.
Posted Feb 19, 2008 by Alexander
 

Comment

2 Thanks

Thanks for the article - haven't digested it all yet, but like you the first thing I do is user auth in my apps. I'll give this a look
Posted Feb 19, 2008 by Brit Gardner
 

Question

3 it doesnt work

Hi,

I'm newbie in cakePHP and I think this article is written in very bad way. Why you didn't create COMPLETE GUIDE ? You wrote sentenses like: "Look at the Cake Manual for how to setup the Models for these tables.", "var $uses = array ('YOUR_MODEL_FOR_USERS'); ", and so on.

I'm newbie and I don't know what you mean. I looked at manual but it still doesn't work !!!

I have errors like : "SQL Error: 1054: Unknown column 'User.email' in 'where clause'", when I go to "cake_1.2beta/users/_login" it has error "Error: UsersController::_login() cannot be accessed directly.", and so on ...

I know that I mistakely did something, BUT YOU CAN WRITE ARTICLES MORE UNDERSTAND ...

What do you recommend me ?

thx
Posted Feb 29, 2008 by greppi
 

Comment

4 RE it doesnt work

I don't think its really necessary to tell the author his article isn't written well when the problem is obviously your lack of understanding CakePHP, as you admitted. If you are unfamiliar with the framework then a good place to start would be the docs. Either http://tempdocs.cakephp.org/ for the BETA or http://manual.cakephp.org/ for the stable release.

We are all newbies once and I know exactly how you feel, when I first started I was completely lost (coming from an ASP.NET background to PHP), but it is through the docs and articles like these that I have quickly picked it up.

As far as the problems you have, the SQL error means you have not created the correct columns in your table.

"Look at the Cake Manual for how to setup the Models for these tables." - Means just that - go check out the documentation and samples that are available and you will quickly see how to implement the models for your application.

Anyways, I for one would like to say thank you to the author, I am in the middle of implementing this in my current project and it's looking promising. Great job
Posted Mar 2, 2008 by Wayne
 

Comment

5 RE RE it doesnt work

That's true, but ... Anyway, I'll study more, ...
Posted Mar 4, 2008 by greppi
 

Question

6 Redirect after login form broken

I am trying to get DarkAuth to work with Cake 1.2.0.6311. It is looking promising... but right now I am stuck.

I followed all your steps and am using your login and deny pages. DarkAuth is protecting a test controller, but after completing the login form I am not getting the page of the controller action. All I get is the Debug info, which is shown at the bottom of the page with the CakePHP default views. After reloading or loading another action manually, I get the correct behavior.

As I have by now started with freshly baked controllers/views a couple of times I begin to wonder whether this is an Apache or DarkAuth problem.

Any ideas?

Thanks for the nice component!
Posted Mar 10, 2008 by Stephan
 

Question

7 RE Redirect after login form broken

I followed all your steps and am using your login and deny pages. DarkAuth is protecting a test controller, but after completing the login form I am not getting the page of the controller action.
I had the same problem, I found a workaround by removing the line
exit();
from the _login() function in app_controller.php and sobstituting it with:
$this->redirect('./');

Hope that works
Posted Mar 15, 2008 by Stefano Pallicca
 

Question

8 DarkAuth for home.ctp

I'm trying to implement this auth system in one of my projects. Now I have a question: I'm trying to add authentication to my home page (that is home.ctp). Now, this page has no controller, so how can darkauth e aware of it?

I'll try to describe my problem more in depth: I've successfully applied darkauth to a User model, so if I try to access /cake/users I get my login screen, and everything works fine.
Now, if I want to go back to my site homepage, located in /cake, I get an error:

Undefined property: PagesController::$User [APP/controllers/components/dark_auth.php, line 248]
Line 248 says:

$check = $this->controller->{$this->user_model_name}->find($conditions);

And I can understand the error, since home.ctp has no controller associated.
home.ctp shows correctly if I logout from darkauth...
What if I'd like to show a different content on the home page for logged/unlogged users? Is it possibile?
Thanks in advance and sorry for the long post.
Posted Mar 15, 2008 by Stefano Pallicca
 

Comment

9 RE RE Redirect after login form broken

I followed all your steps and am using your login and deny pages. DarkAuth is protecting a test controller, but after completing the login form I am not getting the page of the controller action.
I had the same problem, I found a workaround by removing the line
exit();
from the _login() function in app_controller.php and sobstituting it with:
$this->redirect('./');

Hope that works


First, I would like to thank Chris Walker or this wonderful component. It is simple, straightforward and fully working.

My approach to the problem above is to include this line:

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

just before the exit();

This way, the user is redirected to the url he wants to.
Posted Mar 17, 2008 by mike stivaktakis
 

Comment

10 Re DarkAuth for home.ctp

Firstly, thanks to you all for your comments, suggestions and encouragement. Particularly the first comment by Alexander: "Good job in creating it in a way to suit the most amount of people." This is exactly what I wanted to do but you can never tell!

In response to Stefano Pallicca:
I'm trying to implement this auth system in one of my projects. Now I have a question: I'm trying to add authentication to my home page (that is home.ctp). Now, this page has no controller, so how can darkauth e aware of it?

I'll try to describe my problem more in depth: I've successfully applied darkauth to a User model, so if I try to access /cake/users I get my login screen, and everything works fine.
Now, if I want to go back to my site homepage, located in /cake, I get an error:

Undefined property: PagesController::$User [APP/controllers/components/dark_auth.php, line 248]
Line 248 says:

$check = $this->controller->{$this->user_model_name}->find($conditions);

And I can understand the error, since home.ctp has no controller associated.
home.ctp shows correctly if I logout from darkauth...
What if I'd like to show a different content on the home page for logged/unlogged users? Is it possibile?
Thanks in advance and sorry for the long post.

It looks like you haven't added the

$uses = array('User');

line to your AppController, that's what I would check first.
The controller your home.ctp view uses is the PagesController, which doesn't appear to have access to your User model. In an update I will get DarkAuth to load it automagically if needed, but for now you must add it to your AppController class.

Posted Apr 4, 2008 by Chris Walker
 

Comment

11 Re DarkAuth for home.ctp

It looks like you haven't added the
$uses = array('User');

That was the point: I added 'user' (lowercase), that's why it didn't work! Thanks for the tip and keep improving darkauth!
Posted Apr 5, 2008 by Stefano Pallicca
 

Comment

12 destroyDate() destroys all session data.

When the function destroyData() is called, it destroys all session data, while it should only delete the session data set by DarkAuth.

To fix this, replace:
$this->Session->destroy($this->secure_key());
by
$this->Session->delete($this->secure_key());
Posted Apr 26, 2008 by Tijmen
 

Comment

13 RE destroyData()

When the function destroyData() is called, it destroys all session data, while it should only delete the session data set by DarkAuth.

To fix this, replace:
$this->Session->destroy($this->secure_key());
by
$this->Session->delete($this->secure_key());

Thanks, I've updated this in the article. Also I have updated the code in a much bigger way, adding some new features and improving efficiency, and have posted a new article with it - which hasn't been moderated yet. Sometime soon it should be available at: http://bakery.cakephp.org/articles/view/darkauth-v1-3-an-auth-component
Posted Apr 26, 2008 by Chris Walker
 

Question

14 New Code

Thanks, I've updated this in the article. Also I have updated the code in a much bigger way, adding some new features and improving efficiency, and have posted a new article with it - which hasn't been moderated yet. Sometime soon it should be available at: http://bakery.cakephp.org/articles/view/darkauth-v1-3-an-auth-component
Hi Chris, any ideas when the article might be moderated? I am quite interested in the updated code for my new project :)
Posted May 2, 2008 by Wayne
 

Comment

15 RE New Code

Hi Chris, any ideas when the article might be moderated? I am quite interested in the updated code for my new project :)
I have no idea, so I put a copy of the article on my own website... http://thechriswalker.net/darkauth-1. Enjoy.
Posted May 2, 2008 by Chris Walker
 

Comment

16 I got this works

Chris May be this could help someone :

STEP [ 1 ] Create 3 tables, if you have users table just add this fields (live, username, password)
CREATE TABLE `users` (
`id` int(11) NOT NULL auto_increment,
`created` datetime default NULL,
`modified` datetime default NULL,
`live` tinyint(1) NOT NULL default 0,
`username` varchar(16) NOT NULL default '',
`passwd` varchar(32) NOT NULL default '',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

CREATE TABLE `groups` (
`id` int(11) NOT NULL auto_increment,
`created` datetime default NULL,
`modified` datetime default NULL,
`live` tinyint(1) NOT NULL default 0,
`name` varchar(32) NOT NULL default '',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

CREATE TABLE `groups_users` (
`group_id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
KEY `group_id` (`group_id`,`user_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

STEP [ 2 ] Create Group Model (/app/models/group.php)

Model Class:

<?php 

class Group extends AppModel {

    var 
$name 'Group';

    var 
$useTable 'groups';

}

?>

STEP [ 3 ] Add HABTM to your user's model (/app/models/user.php)

var $hasAndBelongsToMany = array('Group' => array('className'    => 'Group',
        'joinTable'    => 'groups_users',
        'foreignKey'   => 'user_id',
        'associationForeignKey'=> 'group_id',
        'unique'       => true
        )
    );

STEP [ 4 ] Modify your appcontroller like mine (/app/app_controller.php)

Controller Class:

<?php 
class AppController extends Controller {
    var 
$helpers = array('Form''Html''Javascript''Session');
    var 
$uses = array('User''Group');
    var 
$components = array('DarkAuth');
    
    function 
_login(){
        if(
is_array($this->data) && array_key_exists('DarkAuth',$this->data) ){
            
$this->DarkAuth->authenticate_from_post($this->data['DarkAuth']);
            
$this->data['DarkAuth']['password'] = '';
        }
    }

    function 
logout(){
        
$this->DarkAuth->logout();
    }
}
?>

STEP [ 5 ] Put DarkAuth Component as (/app/controllers/components/dark_auth.php)
You can get the code from author site http://thechriswalker.net/darkauth-2

Component Class:

<?php 
class DarkAuthComponent extends Object {
    ....
}
?>


STEP [ 6 ] Add some view (/app/views/login.ctp)

View Template:


<?
   $this
->pageTitle 'Access Restricted';
   echo 
$form->create('DarkAuth',array('url'=>substr($this->here,strlen($this->base))));
   echo 
$form->input('DarkAuth.username');
   echo 
$form->password('DarkAuth.password');
                
   echo 
$form->end('login');
?>
and (/app/views/deny.ctp)

View Template:


<?php
    $this
->pageTitle 'Access Denied!';
?>
<p>I'm sorry, but you don't have sufficient permission to access this page!</p>

and then modify (/app/controllers/components/dark_auth.php) to match your password encryption type.
find "function hasher".

  function hasher($plain_text){
    $hashed = md5('dark'.$plain_text.'cake');
    return $hashed;
  }
in this default hasher your password should be
UPDATE users SET password = MD5('dark....cake') WHERE id = ....

find "$user_name_field" and set variable as your table's field

var $user_name_field = 'email';
var $user_pass_field = 'pswd';
because we use 'username' and 'password' from CREATE TABLE above, so we have to set like this :

var $user_name_field = 'username';
var $user_pass_field = 'passwd';

STEP [ 7 ] Add new records to your table

INSERT INTO `groups` (`created`, `modified`, `live`, `name`) VALUES
(NOW(), NOW(), 1, 'Admin'),
(NOW(), NOW(), 1, 'Root');

INSERT INTO `groups_users` (`group_id`, `user_id`) VALUES
(1, 1);

INSERT INTO `users` (`live`, `username`, `passwd`) VALUES 
('1', 'YOURNAME', MD5('darkMYPASSWORDcake')); 

STEP [ 8 ] Put any rules in your controller

var $_DarkAuth = array('required'=>array('Admin','Member'));

eg : in my User controller

Controller Class:

<?php 

class UsersController extends AppController {

    var 
$name 'Users';

    var 
$helpers = array('Html''Form');
    var 
$_DarkAuth = array('required'=>array('Admin','Member'));
    
    ...
    ...
}
?>

STEP [ 9 ] Testing go to http://yourapps/users/login, type YOURNAME as username
and MYPASSWORD as password.

Happy coding...thanks to Chris
Posted May 29, 2008 by Agus Sudarmanto