DarkAuth v1.3 - the component

» Page: 1 - the intro, 2 - the component, 3 - the setup

Update (26/06/2008): Extra line added to ensure $_DarkAuth variable is set on "deny". New method added getUserId(), does what it says on the tin!
Update: I have ammended the component code, as it was not working with the RC of 1.2 only a small fix but necessary. Thanks to those of you who pointed the bug out to me!

Here's the v1.3b Code. (new changes highlighted)

Component Class:

  1. <?php
  2. class DarkAuthComponent extends Object {
  3.   //user model name
  4.   var $user_model_name = 'User';
  5.  
  6.   //user model fields for user and password.
  7.   var $user_name_field = 'email';
  8.   var $user_pass_field = 'pswd';
  9.  
  10.   //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.
  11.   var $user_name_case_folding = 'lower';
  12.    
  13.   //surely you have a field in you users table to show whether the user is active or not? set to null if not.
  14.   var $user_live_field = 'live';
  15.   var $user_live_value = 1;
  16.  
  17.   //Group for access control if used, and the field for matching the name.
  18.   var $group_model_name = 'Group';
  19.   var $group_name_field = 'name';
  20.  
  21.   //set to false if you don use a HABTM group relationship.
  22.   var $HABTM = true;
  23.  
  24.   //if you want a single group to have automatically granted access to any restriction.
  25.   var $superuser_group = 'Root';
  26.  
  27.   //this is the login and deny views (I usually suggest keeping this in the root of your views folder)
  28.   var $login_view = '/login';  
  29.   var $deny_view = '/deny';  
  30.  
  31.   // NB this is were to **redirect** AFTER logout by default
  32.   var $logout_page = '/';
  33.  
  34.   //This message is setFlash()'d on failed login.
  35.   var $login_failed_message = '<p class="error">Login Failed, Please check your details and try again.</p>';
  36.  
  37.   //Message to setFlash after logout.
  38.   var $logout_message = '<p class="success">You have been succesfully logged out.</p>';
  39.  
  40.   //Allow use of cookies to remember authenticated sessions.
  41.   var $allow_cookie = false;
  42.  
  43.   //how long until cookies expire (by default). format is "strtotime()" based (http://php.net/strtotime).
  44.   var $cookie_expiry = '+6 Months';
  45.  
  46.   //some random stuff that someone is unlikey to guess.
  47.   var $session_secure_key = 'sRmtVStkedAdlxBy';
  48.  
  49.   /**
  50.    * You can edit this function to explain how you want to hash your passwords.
  51.    * Also you can use it as a static function in your controller to hash passwords beforeSave
  52.    */
  53.   function hasher($plain_text){
  54.     $hashed = md5('dark'.$plain_text.'cake');
  55.     return $hashed;
  56.   }
  57.  
  58. ##########################################################################
  59.  /*
  60.   * DON'T EDIT THESE OR ANYTHING BELOW HERE UNLESS YOU KNOW WHAT YOU'RE DOING
  61.   */
  62.   var $controller;
  63.   var $here;
  64.   var $components=array('Session');
  65.   var $current_user;
  66.   var $from_session;
  67.   var $from_post;
  68.   var $from_cookie;
  69.  
  70.  
  71.   function startup(&$controller){
  72.  
  73.       //Let's check they have changed the secure key from the default.
  74.         if($this->session_secure_key == 'sRmtVStkedAdlxBy'){
  75.             die('<p>Please change the DarkAuth::session_secure_key value from it default.</p>');
  76.         }
  77.        
  78.     $this->controller = $controller;
  79.    
  80.     $this->here = substr($this->controller->here,strlen($this->controller->base));
  81.      
  82.     $this->controller->_login();
  83.    
  84.     //now check session/cookie info.
  85.     $this->getUserInfoFromSessionOrCookie();
  86.  
  87.     //now see if the calling controller wants auth
  88.     if( array_key_exists('_DarkAuth', $this->controller) ){
  89.       // We want Auth for any action here
  90.       if(!empty($this->controller->_DarkAuth['onDeny'])){
  91.               $deny = $this->controller->_DarkAuth['onDeny'];
  92.             }else{
  93.               $deny = null;
  94.             }
  95.             if(!empty($this->controller->_DarkAuth['required'])){
  96.               $this->requiresAuth($this->controller->_DarkAuth['required'],$deny);
  97.             }else{
  98.         $this->requiresAuth(null,$deny);
  99.       }
  100.     }
  101.     //finally give the view access to the data
  102.     $this->_giveViewData();
  103.   }
  104.  
  105.   function _giveViewData(){
  106.     $DA = array(
  107.       'User'=>$this->getUserInfo(),
  108.       'Access'=>$this->getAccessList()
  109.     );
  110.     $this->controller->set('_DarkAuth',$DA);
  111.   }
  112.  
  113.     function secure_key(){
  114.         static $key;
  115.         if(!$key){
  116.             $key = md5(Configure::read('Security.salt').'!DarkAuth!'.$this->session_secure_key);
  117.         }
  118.         return $key;
  119.     }
  120.  
  121.   function requiresAuth($groups=array(),$deny_redirect=null){
  122.         if( empty($this->current_user) ){
  123.             // Still no info! render login page!
  124.             if($this->from_post){
  125.                 $this->Session->setFlash($this->login_failed_message);
  126.             }
  127.       echo $this->controller->render($this->login_view);
  128.       exit();
  129.     }else{
  130.       if($this->from_post){
  131.                 // user just authed, so redirect to avoid post data refresh.
  132.                 $this->controller->redirect($this->here,null,null,true);
  133.                 exit();
  134.       }
  135.       // User is authenticated, so we just need to check against the groups.
  136.       if( empty($groups) ){
  137.         // No Groups specified so we are good to go!
  138.         $deny = false;
  139.       }else{
  140.         $deny = !$this->isAllowed($groups);
  141.       }
  142.       if($deny){
  143.         // Current User Doesn't Have Access! DENY
  144.         if($deny_redirect){
  145.                     $this->controller->redirect($deny_redirect);
  146.                     exit();
  147.                 }else{
  148.                     $this->_giveViewData(); // so this view has it, otherwise the exit stopps it being set.
  149.                     echo $this->controller->render($this->deny_view);
  150.                     exit();
  151.                 }
  152.       }
  153.     }
  154.     return true;
  155.   }
  156.  
  157.   function isAllowed($groups=array()){
  158.     if( empty($this->current_user) ){
  159.       // No information about the user! FALSE
  160.       return false;
  161.     }else{
  162.       // User is authenticated, so we just need to check against the groups.
  163.       if( empty($groups) ){
  164.         // No Groups specified so we are good to go! TRUE
  165.         return true;
  166.       }
  167.      
  168.       if(!is_array($groups)){
  169.         //if a string passed, turn to an array with one element
  170.         $groups = array(0 => $groups);
  171.       }
  172.      
  173.       $access = $this->getAccessList();
  174.            
  175.       foreach($groups as $g){
  176.         if(array_key_exists($g,$access) && $access[$g]){
  177.           return true;
  178.         }
  179.       }
  180.     }
  181.   }
  182.  
  183.   function getCookieInfo(){
  184.         if(!array_key_exists('DarkAuth',$_COOKIE)){
  185.             //No cookie
  186.             return false;
  187.         }
  188.         list($hash,$data) = explode("|||",$_COOKIE['DarkAuth']);
  189.         if($hash != md5($data.$this->secure_key())){
  190.             //Cookie has been tampered with
  191.             return false;
  192.         }
  193.         $crumbs = unserialize(base64_decode($data));
  194.         if(!array_key_exists('username',$crumbs) ||
  195.              !array_key_exists('password',$crumbs) ||
  196.              !array_key_exists('expiry'  ,$crumbs)){
  197.             //Cookie doesn't contain the correct info.
  198.             return false;
  199.         }
  200.         if(!isset($crumbs['expiry']) || $crumbs['expiry'] <= time()){
  201.             //Cookie is out of date!
  202.             return false;
  203.         }
  204.         //All checks passed, cookie is genuine. remove expiry time and return
  205.         unset($crumbs['expiry']);
  206.         return $crumbs;        
  207.   }
  208.  
  209.   function setCookieInfo($data,$expiry=0){
  210.       if($data === false){
  211.             //remove cookie!
  212.             $cookie = false;
  213.             $expiry = 100; //should be in the past enough!
  214.       }else{
  215.             $serial = base64_encode(serialize($data));
  216.             $hash = md5($serial.$this->secure_key());
  217.             $cookie = $hash."|||".$serial;
  218.         }
  219.         if($_SERVER['SERVER_NAME']=='localhost'){
  220.           $domain = null;
  221.         }else{
  222.           $domain = '.'.$_SERVER['SERVER_NAME'];
  223.         }
  224.         return setcookie('DarkAuth', $cookie, $expiry, $this->controller->base, $domain);
  225.   }
  226.  
  227.   function authenticate_from_post($data){
  228.         $this->from_post = true;
  229.         return $this->authenticate($data);
  230.   }
  231.   function authenticate_from_session($data){
  232.         $this->from_session = true;
  233.         return $this->authenticate($data);
  234.     }
  235.     function authenticate_from_cookie(){
  236.         $this->from_cookie = true;
  237.         return $this->authenticate($this->getCookieInfo());
  238.     }
  239.    
  240.   function authenticate($data){
  241.         if($data === false){
  242.             $this->destroyData();
  243.             return false;
  244.         }
  245.     if($this->from_session || $this->from_cookie){
  246.       $hashed_password = $data['password'];
  247.     }else{
  248.       $hashed_password = $this->hasher($data['password']);
  249.     }    
  250.     switch($this->user_name_case_folding){
  251.             case 'lower':
  252.                 $data['username'] = strtolower($data['username']);
  253.                 break;            
  254.             case 'upper';
  255.                 $data['username'] = strtoupper($data['username']);
  256.                 break;
  257.             default: break;
  258.     }
  259.     $conditions = array(
  260.       $this->user_model_name.".".$this->user_name_field => $data['username'],
  261.       $this->user_model_name.".".$this->user_pass_field => $hashed_password
  262.     );
  263.     if($this->user_live_field){
  264.       $field = $this->user_model_name.".".$this->user_live_field;
  265.       $conditions[$field] = $this->user_live_value;
  266.     };
  267.     $check = $this->controller->{$this->user_model_name}->find($conditions);
  268.     if($check){
  269.        $this->Session->write($this->secure_key(),$check);
  270.        if(
  271.                   $this->allow_cookie && //check we're allowing cookies
  272.                   $this->from_post && //check this was a posted login attempt.
  273.                   array_key_exists('remember_me',$data) && //check they where given the option!
  274.                   $data['remember_me'] == true //check they WANT a cookie set
  275.              ){
  276.                  // set our cookie!
  277.                  if(array_key_exists('cookie_expiry',$data)){
  278.                    $this->cookie_expiry = $data['cookie_expiry'];
  279.                  }else{
  280.                    $this->cookie_expiry;
  281.                  }
  282.                  if(strtotime($this->cookie_expiry) <= time()){
  283.                     // Session cookie? might as well not set at all...
  284.                  }else{
  285.                    $expiry = strtotime($this->cookie_expiry);
  286.                    $this->setCookieInfo(array('username'=>$data['username'], 'password'=>$hashed_password, 'expiry'=>$expiry), $expiry);
  287.                  }
  288.              }
  289.        $this->current_user = $check;
  290.        return true;
  291.     }else{
  292.         if($this->from_post){
  293.           $this->Session->setFlash($this->login_failed_message);
  294.             }
  295.       $this->destroyData();
  296.       return false;
  297.     }
  298.   }
  299.  
  300.   function getUserInfo(){
  301.     return $this->current_user[$this->user_model_name];
  302.   }
  303.   function getUserId(){
  304.     return $this->current_user[$this->user_model_name]['id'];
  305.   }
  306.   function getAllUserInfo(){
  307.     return $this->current_user;
  308.   }
  309.   function getAccessList(){
  310.     static $access_list = false;
  311.     if(!$access_list){
  312.       $access_list = $this->_generateAccessList();
  313.     }
  314.     return $access_list;
  315.   }
  316.   function _generateAccessList(){
  317.     if(!$this->group_model_name){
  318.       return array();
  319.     }
  320.     $all_groups = $this->controller->{$this->user_model_name}->{$this->group_model_name}->find('list');
  321.     if(!count($all_groups)){  return array(); }
  322.     $access = array_combine($all_groups,array_fill(0,count($all_groups),0)); //create empty array.
  323.    
  324.     if(empty($this->current_user)){
  325.       // NO AUTHENTICATION, SO EMTPY ARRAY!
  326.       return $access;
  327.     }
  328.     if($this->HABTM){
  329.       // could be many groups
  330.       $ugroups = Set::combine($this->current_user[$this->group_model_name],'{n}.id','{n}.'.$this->group_name_field);
  331.       foreach($all_groups as $id => $role){
  332.         if(in_array($role,$ugroups)){
  333.           $access[$role] = 1;
  334.         }else{
  335.           $access[$role] = 0;
  336.         }
  337.       }
  338.     }else{
  339.       // single group assoc, id = user.group_id
  340.       $foreign_key = $this->controller{$this->user_model_name}->belongsTo[$this->group_model_name]['foreignKey'];
  341.       foreach($all_groups as $id => $role){
  342.         if($this->current_user[$this->user_model_name][$foreign_key] == $id){
  343.           $access[$role] = 1;
  344.         }else{
  345.           $access[$role] = 0;
  346.         }
  347.       }
  348.     }
  349.     if($this->superuser_group && $access[$this->superuser_group]){
  350.       return array_combine($all_groups,array_fill(0,count($all_groups),1));
  351.     }else{
  352.       return $access;
  353.     }
  354.   }
  355.  
  356.   function destroyData(){
  357.     $this->Session->delete($this->secure_key());
  358.     if($this->allow_cookie){
  359.       $this->setcookieInfo(false);
  360.     }
  361.     $this->current_user = null;
  362.   }
  363.  
  364.   function logout($redirect=false){
  365.     $this->destroyData();
  366.     if(!$redirect){
  367.       $redirect = $this->logout_page;
  368.     }
  369.         $this->Session->setFlash($this->logout_message);
  370.     $this->controller->redirect($redirect,null,true);
  371.     exit();
  372.   }
  373.  
  374.   function getUserInfoFromSessionOrCookie(){
  375.     if( !empty($this->current_user) ){
  376.       return false;
  377.     }
  378.     if($this->Session->valid() && $this->Session->check($this->secure_key()) ){
  379.       $this->current_user = $this->Session->read($this->secure_key());
  380.       return $this->authenticate_from_session(array(
  381.         'username' => $this->current_user[$this->user_model_name][$this->user_name_field],
  382.         'password' => $this->current_user[$this->user_model_name][$this->user_pass_field],
  383.       ));
  384.     }elseif($this->allow_cookie){
  385.             return $this->authenticate_from_cookie();
  386.     }
  387.   }
  388. }
  389. ?>

Now on to the Setup and the default Views for Login and Deny.

» Page: 1 - the intro, 2 - the component, 3 - the setup