dAuth v0.3 component

By Dieter Plaetinck (Dieter_be)
the component for dAuth v0.3

controllers/components/d_auth.php

This component contains some fixes and improvements, and is compatible with v0.2 of the dAuth system.
Changes since v0.2:
  1. changed findAllByIpAdress call to findAllByIp_adress to make it php4 compatible
  2. improved the whole getting/setting system of userdata to/from the session. Also, you can now set the userdata available in the view. see the comment about $userDataInView . (PS: passing the password to the view is not necessarily dangerous. the password will never be sent to the client unless you - the programmer - explicitly code it. The default value is just to be sure... (eg debug() calls...) Check the sample element userinfo.thtml to see how you can use it.
  3. changed $this->controller = $controller; to $this->controller = &$controller; in the startup function. A fix needed for php4 users.
  4. changed ($time/500) to ($time%500)..
  5. when doing attemptLogin now checking post data first before getting records from the database. I also do some more checking on the structure of the array now
  6. gave hashing function better names (stage1Hash and stage2Hash)
  7. Added user-configurable error-messages
  8. fixed typo (successfull -> successful)
  9. Added copyright & licensing information
  10. Some changes in comments etc.
  11. Little cosmetic changes like camelcasing variables.

Component Class:

Download code <?php 
/*
 * PHP versions 4 and 5
 *
 * dAuth: A secure authentication system for the cakePHP framework.
 * Copyright (c)    2006, Dieter Plaetinck
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @author            Dieter Plaetinck
 * @copyright        Copyright (c) 2006, Dieter Plaetinck
 * @version            0.3
 * @modifiedby        Dieter@be
 * @license            http://www.opensource.org/licenses/mit-license.php The MIT License
 */

class dAuthComponent extends Object
{
    
/* ----- internal variables start here: only for devs, not users ----- */

    
var $controller;
    var 
$components =    array('Session');

    
/* Will be used for storing error messages for the user. */
    
var $error false;

    
/* ----- internal variables stop here ----- */

    /* ----- Edit the variables and function below to suit your needs ----- */

    /* set to true to include the whole array in the view, false to include nothing,
     * or use the array to leave out parts.
     */
    
var $userDataInView = array('unset'=>array('password'));

    
/* here you can specify where users must be redirected when certain events happen.  */
    
var $pages =        array(    'default' => '/',
                                
'login_success' => '/',
                                
'logout_success' => '/',
                                
'logout_failure' => '/',
                                
'login' => '/users/login',
                                
'logout' => '/users/logout',
                                
'register_success' => '/',
                                
'change_password_success' => '/'
                            
);

    
/* Here you can change the (error) messages that will be used */
    
var $messages =        array('credentials_mismatch' => 'Credentials mismatch.',
                                
'bad_username' => 'Choose a better/other username',
                                
'registration_success' => 'Registration successful.',
                                
'bad_userdata' => 'Bad user data',
                                
'unknown_error'=> 'An unknown error occurred.',
                                
'no_sessiondata' => 'Needed sessiondata not found.',
                                
'youre_hammering' => 'Hammering detected.  Your host has been blocked.  Try again later.',
                                
'youre_blocked' => 'Your host has been blocked.  Try again later'
                                
);
    
/*
     * the number of attempts in the period of time that is considered hammering (brute-force).
     */
    
var $hammerRatio = array(    'seconds' => 5,
                                
'attempts' => 4
                            
);
    
/*
     * Whether page execution should terminate when hammering is detected.
     */
    
var $dieHammer true;
    
/*
     * Whether page execution should terminate when the host is found to be blocked.
     */
    
var $dieBlocked true;
    
/*
     * The time a host should be blocked, in seconds.
     */
    
var $blockTime 1800;
    
/*
     * Whether cleartext logins should be allowed.
     */
    
var $allowClearText false;
    
/*
     * The algorithm (constant over time) that will be used to securely store passwords in the database.
     * If you change this, you have to change the stage1Hash javascript function too.
     */
    
function stage1Hash($cleartext)
    {
        return 
sha1($cleartext.$cleartext{0});
    }

    
/*
     * The algorithm (changing over time) that will be used to securely transport passwords over the network.
     * If you change this, you have to change the stage2Hash javascript function too.
     */
    
function stage2Hash($stage1,$salt)
    {
        return 
sha1($stage1.$salt);
    }

    
/* ----- Stop editing here ----- */

    
function startup(&$controller)
    {
        
$this->controller = &$controller;
        if(
$this->userDataInView$this->setUserData($this->getUserData());
    }

    function 
attemptLogin($postUser null,$ip null)
    {
        
$success false;
        
$clearText true;

        
$this->_cleanUpAttempts();
        
$this->_defineHost($ip);
        
$this->controller->LoginAttempt->create();
        
$this->controller->data['LoginAttempt'][]['host_id'] = $this->controller->data['Host']['id'];
        
$this->controller->LoginAttempt->save(end($this->controller->data['LoginAttempt']));
        
$this->controller->data array_merge($this->controller->data$this->controller->LoginAttempt->read());

        
$cleanHost $this->_checkHostBehaviour();

        if(
$cleanHost)
        {
            if(
is_array($postUser) && !empty($postUser) && isset($postUser['User']) &&
            isset(
$postUser['User']['username']) && isset($postUser['User']['password']))
            {
                   
$salt $this->Session->read('salt');
                
$dbUser $this->controller->User->findByUsername($postUser['User']['username']);

                if(!empty(
$dbUser))
                 {
                     if(isset(
$postUser['User']['hashed_pw']) && $postUser['User']['hashed_pw'] )
                    {
                        
$clearText false;
                    }
                    if(
$clearText && $this->allowClearText)
                    {
                        if(
$this->stage1Hash($postUser['User']['password']) == $dbUser['User']['password'])
                        {
                            
$success true;
                        }
                    }
                    else
                    {
                        
$real_hash $this->stage2Hash($dbUser['User']['password'],$salt);
                        
$submitted_hash $postUser['User']['hashed_pw'];
                        if(
$real_hash == $submitted_hash)
                        {
                            
$success true;
                        }
                    }
                   }
                   if(
$success)
                   {
                    
$this->_login($dbUser['User']);
                   }
                   else
                   {
                       
$this->error $this->messages['credentials_mismatch'];
                   }
               }
        }
        return 
$success;
    }

    function 
attemptRegister($postUser null,$ip null)
    {
        
$success false;
        
$clearText true;

        if (
is_array($postUser) && !empty($postUser))
        {
            if(!isset(
$postUser['User']['username']) || !$postUser['User']['username'] || $this->controller->User->findCount(array('username'=>$postUser['User']['username'])))
            {
                
$this->error $this->messages['bad_username'];
            }
            else
            {
                
$hash ='';
                if(isset(
$postUser['User']['hashed_pw']) && $postUser['User']['hashed_pw'] )
                {
                    
$clearText false;
                }
                if(
$clearText && $this->allowClearText)
                {
                    
$hash $this->stage1Hash($postUser['User']['password']);
                }
                else
                {
                    
$hash $postUser['User']['hashed_pw'];
                }
                
$this->controller->User->create();
                
$user['User']['username'] = $postUser['User']['username'];
                
$user['User']['password'] = $hash;
                 if (
$this->controller->User->save($user))
                {
                    
$success true;
                    
$this->controller->flash($this->messages['registration_success'],'/');
                }
                else
                {
                        
$this->error $this->messages['unknown_error'];
                }
            }
        }
        else
        {
            
$this->error $this->messages['bad_userdata'];
        }
        return 
$success;
    }
    function 
attemptChangePassword($postUser null,$ip null)
    {
        
$success false;
        
$clearText true;

        if(
is_array($postUser) && !empty($postUser))
         {
             
$sessionUser $this->getUserData();
             if(
$sessionUser)
             {
                 if(isset(
$postUser['User']['hashed_pw']) && $postUser['User']['hashed_pw'] )
                {
                    
$clearText false;
                }
                
$hash ='';
                if(
$clearText && $this->allowClearText)
                {
                    
$hash $this->stage1Hash($postUser['User']['password']);
                }
                else
                {
                    
$hash $postUser['User']['hashed_pw'];
                   }
                
$success $this->controller->User->changePassword($sessionUser['id'],$hash);
                   if(!
$success)
                   {
                       
$this->error $this->messages['unknown_error'];
                   }
                   else
                   {
                      
/*
                        * Update the information in the session and -possibly- the view.
                        */
                    
$this->setUserData($sessionUser);
                   }
             }
             else
             {
                 
$this->error $this->messages['no_sessiondata'];
             }
           }
           else
        {
            
$this->error $this->messages['bad_userdata'];
        }
        return 
$success;
    }
    function 
attemptLogout()
    {
        
$success $this->_logout();
        return 
$success;
    }

    function 
_login($user null)
    {
        
$success false;
           if(
$user)
           {
            
$success $this->setUserData($user);
           }
           return 
$success;
    }

    function 
_logout()
    {
        
$success $this->setUserData(null);
           return 
$success;
    }

    function 
getUserData()
    {
        
$user $this->Session->read('User');
        if(!
is_array($user) || empty($user))
        {
            
$user null;
        }
        return 
$user;
    }
    function 
setUserData($user)
    {
        if(
$user)
        {
            
$this->Session->write('User'$user);
            if(
$this->userDataInView)
            {
                if(
is_array($this->userDataInView))
                {
                    if(isset(
$this->userDataInView['not']))
                    {
                        foreach(
$this->userDataInView['not'] as $attr)
                        {
                            if(
$attr && !is_array($attr) && isset($user[$attr])) $user[$attr] = null;
                        }
                    }
                    if(isset(
$this->userDataInView['unset']))
                    {
                        foreach(
$this->userDataInView['unset'] as $attr)
                        {
                            if(
$attr && !is_array($attr)&& isset($user[$attr])) unset($user[$attr]);
                        }
                    }
                }
                
$this->controller->set('User',$user);
            }
        }
        else
        {
            
$this->Session->delete('User');
            if(
$this->userDataInView)
            {
                
$this->controller->set('User',null);
            }
        }

        return 
true;
     }

    function 
link($to)
    {
        
$path $this->pages['default'];
        if(
$to && isset($this->pages[$to]))
        {
            
$path $this->pages[$to];
        }
        return 
$path;
    }

    function 
redirect($to)
    {
        
$this->controller->redirect($this->link($to));
    }

    function 
newSalt()
    {
         
$salt crc32(time());
         
$this->controller->set('special_sauce',$salt);
         
$this->Session->write('salt'$salt);
    }

    function 
_checkHostBehaviour()
    {
        
$hammer false;
        
$blocked false;
        
$clean true;
           if(
$this->controller->data['Host']['ip_adress'])
        {
            if(
$this->controller->Host->isBlocked($this->controller->datatime() - $this->blockTime))
            {
                
$blocked true;
            }
            else
            {
                
$hammer $this->controller->Host->isHammering($this->controller->data,$this->hammerRatio);
                if(
$hammer)
                {
                    
$this->controller->Host->block($this->controller->data['Host']['id']);
                }
            }
            if((
$hammer && $this->diehammer) || ($blocked && $this->dieblocked))
            {
                die();
            }
            else if(
$hammer)
            {
                
$this->error $this->messages['youre_hammering'];
            }
            else if(
$blocked)
            {
                
$this->error $this->messages['youre_blocked'];
            }
        }
        if(
$hammer || $blocked)
        {
            
$clean false;
        }
        return 
$clean;
    }

    function 
_defineHost($ip null)
    {
        if(
$ip)
        {
            
$hosts $this->controller->Host->findAllByIp_adress($ip);
            if(
is_array($hosts)&& isset($hosts[0]))
            {
                
$this->controller->data array_merge($this->controller->data$hosts[0]);
            }
            else
            {
                
$this->controller->Host->create();
                
$this->controller->data['Host']['ip_adress'] = $ip;
                
$this->controller->Host->save($this->controller->data['Host']);
                
$this->controller->data array_merge($this->controller->data$this->controller->Host->read());
            }
        }
    }

    function 
getErrorMessage()
    {
        return 
$this->error;
    }

    function 
_cleanUpAttempts()
    {
        
$time time();
        if(!(
$time%500)) // do this about once in 500 times.
        
{
            
$this->controller->LoginAttempt->cleanUpExpired($time $this->hammerRatio['seconds'] - 1);
        }
    }
}
?>

more info about dAuth @ http://bakery.cakephp.org/articles/view/147

 

Comments 153

CakePHP Team Comments Author Comments
 

Bug

1 Changed code to fix bug

in the startup function, i changed
$this->controller = $controller;
to
$this->controller = &$controller;

The original line worked perfectly for php5 users, but not for php4 users. (sorry couldn't test that ;-)

The new line works for both! :)
Posted Nov 21, 2006 by Dieter Plaetinck
 

Comment

2 Addon if you want userdata in your presentation layer

Here is a small fix that i will probably incorporate in future releases. (but in a more tidy fashion, using a $setDataInView boolean)

if you want userdata from the session available in your presentation layer (which is very common in most applications) add this at the end of the startup function of the dAuthComponent:

$User = $this->Session->read('User');
$this->controller->set('User',$User);

Then you could put something like this in any of your views, layouts or elements:

View Template:


<?php
if($User)    echo ('Logged in as '.$User['username']);
else        echo 
'not logged in';
?>
Posted Nov 26, 2006 by Dieter Plaetinck
 

Bug

3 Another bugfix for php4 users

Php4 users should change this call:
$hosts = $this->controller->Host->findAllByIpAdress($ip);
to:
$hosts = $this->controller->Host->findAllByIp_adress($ip);

More information:
http://groups.google.com/group/cake-php/browse_thread/thread/859d121726ece828/b062a3db8c435c32 https://trac.cakephp.org/ticket/1567
Posted Dec 3, 2006 by Dieter Plaetinck
 

Bug

4 Bugfix

Change
function _cleanUpAttempts()
{
$time = time();
if(!($time/500)) // do this about once in 500 times.
{
$this->controller->LoginAttempt->cleanUpExpired($time - $this->hammerRatio['seconds'] - 1);
}
}

to

function _cleanUpAttempts()
{
$time = time();
if(!($time%500)) // do this about once in 500 times.
{
$this->controller->LoginAttempt->cleanUpExpired($time - $this->hammerRatio['seconds'] - 1);
}
}

The previous one is never been executed.
Posted Dec 12, 2006 by Jan Obratil
 

Bug

5 fixed

djeez.. can't believe i actually wrote that :D
thanks for the spotting!
it should really be % instead of /

will include this in the next version, and fix the code if the bakery lets me ;-)
Dieter
Posted Dec 13, 2006 by Dieter Plaetinck
 

Comment

6 Updated to v0.3

Fixed all reported bugs and added some features.

Have fun with it :-)

Dieter
Posted Dec 15, 2006 by Dieter Plaetinck
 

Comment

7 view

Hello,

Do you have the controller/view setup for this to see how to interract with the component ?

Thanks.
Posted Dec 21, 2006 by Salim
 

Comment

8 Re

Yes, they are here: http://bakery.cakephp.org/articles/view/152
for more information, check http://bakery.cakephp.org/articles/view/147
greets, Dieter
Posted Dec 21, 2006 by Dieter Plaetinck
 

Comment

9 Language support

I dont know if this is the right place for this info but...

I've made some changes to the dauth component to add multi lingual support. The modified version have minor changes in the dauthcomponent, views and the users_controller. A new component to handle language sessions and language files are added. The language component is in alpha version but may be used to add multi lingual support to any controller/component/view.

Please direct me to the right place to publish my stuff.

For now You can get this version from http://www.lagerbjelke.se/app_moddauth.zip
In this example set the country code in the startupfunction of dauth ($this->cccLang->setLanguage('se');)

Open and translate the language files in vendors/language, follow the naming convention from the swedish and english file.
Posted Jul 24, 2007 by Timo Lagerbjelke