Guest User

CakePHP PasswordableBehavior

a guest
Jul 3rd, 2013
208
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. <!-- File: /app/Controller/UsersController.php -->
  2. <?php
  3.  
  4. class UsersController extends AppController {
  5.  
  6.     public function passchange($id = null) {
  7.         if ($this->request->is('post') || $this->request->is('put')) {
  8.             // attach the behavior and force the user to enter the current password for security purposes
  9.             $this->User->Behaviors->attach('Tools.Passwordable', array('current'=>true));
  10.             $this->request->data['User']['id'] = $this->Session->read('Auth.User.id');
  11.             if ($this->User->save($this->request->data, true, array('pwd', 'pwd_repeat', 'id'))) {
  12.                 //SUCCESS flash/redirect
  13.                 $this->Session->setFlash(__('user password has been saved'));
  14.                 $this->redirect(array('action' => 'index'));
  15.             }
  16.             $this->Session->setFlash(__('Err saving password.'));
  17.          
  18.             // pw should not be passed to the view again for security reasons
  19.             unset($this->request->data['User']['pwd']);
  20.             unset($this->request->data['User']['pwd_repeat']);
  21.         }
  22.     }
  23.  
  24. (...)
  25.  
  26. #################################################################
  27. <!-- File: /app/View/Users/passchange.ctp -->
  28.  
  29. <div class="reveal-modal" style="visibility: visible !important" >
  30. <h1>Change password</h1>
  31. <?php echo $this->Form->create(); ?>
  32.  
  33. <?php
  34. echo $this->Form->input('id',array('type'=>'hidden'));
  35. echo $this->Form->input('pwd_current',array('type'=>'password', 'value'=>'', 'autocomplete'=>'off','label'=>array('text'=>'Enter your password')));
  36. echo $this->Form->input('pwd',array('type'=>'password', 'value'=>'', 'autocomplete'=>'off','label'=>array('text'=>'Enter your new password')));
  37. echo $this->Form->input('pwd_repeat',array('type'=>'password', 'value'=>'', 'autocomplete'=>'off','label'=>array('text'=>'Confirm your new password')));
  38. ?>
  39.  
  40. <button type="submit">Save</button>
  41.  
  42. <?php echo $this->Form->end(); ?>
  43. <a class="close-reveal-modal" id="cancela">&#215;</a>
  44. </div>
  45. <div id="loading">Loading...</div>
  46. <div id="respostaAjax"></div>
  47.  
  48. #################################################################
  49. <!-- File: /app/Model/User.php -->
  50.  
  51. <?php
  52. App::uses('PasswordableBehavior', 'Tools.Model/Behavior');
  53. class User extends AppModel {
  54.     public $validate = array(
  55.         'username' => array(
  56.             'required' => array(
  57.                 'rule' => array('notEmpty'),
  58.                 'message' => 'A username is required'
  59.             )
  60.         ),
  61.         'password' => array(
  62.             'required' => array(
  63.                 'rule' => array('notEmpty'),
  64.                 'message' => 'A password is required'
  65.             )
  66.         ),
  67.         'role' => array(
  68.             'valid' => array(
  69.                 'rule' => array('inList', array('admin', 'user')),
  70.                 'message' => 'Please enter a valid role',
  71.                 'allowEmpty' => false
  72.             )
  73.         ),
  74.     );
  75.  
  76.      public function beforeSave($options = array()) {
  77.         parent::beforeSave($options);
  78.         if(isset($this->data[$this->alias]['pwd'])) {
  79.             $this->data[$this->alias]['password'] = Security::hash($this->data[$this->alias]['pwd'], 'blowfish');
  80.             $this->request->data[$this->alias]['password'] = '';
  81.         }
  82.         return true;
  83.     }  
  84. }
  85.  
  86. <!-- Passwordablebehavior.php -->
  87.  
  88. <?php
  89. App::uses('ModelBehavior', 'Model');
  90. App::uses('Router', 'Routing');
  91. App::uses('CakeRequest', 'Network');
  92. App::uses('CakeResponse', 'Network');
  93. App::uses('Security', 'Utility');
  94.  
  95. if (!defined('PWD_MIN_LENGTH')) {
  96.     define('PWD_MIN_LENGTH', 3);
  97. }
  98. if (!defined('PWD_MAX_LENGTH')) {
  99.     define('PWD_MAX_LENGTH', 20);
  100. }
  101.  
  102. /**
  103.  * A cakephp2 behavior to work with passwords the easy way
  104.  * - complete validation
  105.  * - hashing of password
  106.  * - requires fields (no tempering even without security component)
  107.  * - usable for edit forms (allowEmpty=>true for optional password update)
  108.  *
  109.  * usage: do NOT add it via $actAs = array()
  110.  * attach it dynamically in only those actions where you actually change the password like so:
  111.  * $this->User->Behaviors->load('Tools.Passwordable', array(SETTINGSARRAY));
  112.  * as first line in any action where you want to allow the user to change his password
  113.  * also add the two form fields in the form (pwd, pwd_confirm)
  114.  * the rest is cake automagic :)
  115.  *
  116.  * now also is capable of:
  117.  * - require current password prior to altering it (current=>true)
  118.  * - don't allow the same password it was before (allowSame=>false)
  119.  *
  120.  * TODO: allowEmpty and nonEmptyToEmpty - maybe with checkbox "set_new_pwd"
  121.  * feel free to help me out
  122.  *
  123.  * @version 1.6 (renamed from ChangePassword to Passwordable)
  124.  * @author Mark Scherer
  125.  * @link http://www.dereuromark.de/2011/08/25/working-with-passwords-in-cakephp
  126.  * @license MIT
  127.  * 2012-08-18 ms
  128.  */
  129. class PasswordableBehavior extends ModelBehavior {
  130.  
  131.     /**
  132.      * @access protected
  133.      */
  134.     protected $_defaults = array(
  135.         'field' => 'password',
  136.         'confirm' => true, # set to false if in admin view and no confirmation (pwd_repeat) is required
  137.         'allowEmpty' => false, # if password must be provided or be changed (set to true for update sites)
  138.         'current' => true, # expect the current password for security purposes
  139.         'formField' => 'pwd',
  140.         'formFieldRepeat' => 'pwd_repeat',
  141.         'formFieldCurrent' => 'pwd_current',
  142.         'hashType' => 'blowfish',
  143.         'hashSalt' => false,
  144.         'auth' => null, # which component (defaults to AuthComponent),
  145.         'allowSame' => true, # dont allow the old password on change,
  146.         'minLength' => PWD_MIN_LENGTH,
  147.         'maxLength' => PWD_MAX_LENGTH
  148.     );
  149.  
  150.     /**
  151.      * @access protected
  152.      */
  153.     protected $_validationRules = array(
  154.         'formField' => array(
  155.             'between' => array(
  156.                 'rule' => array('between', PWD_MIN_LENGTH, PWD_MAX_LENGTH),
  157.                 'message' => array('valErrBetweenCharacters %s %s', PWD_MIN_LENGTH, PWD_MAX_LENGTH),
  158.                 'allowEmpty' => null,
  159.                 'last' => true,
  160.             )
  161.         ),
  162.         'formFieldRepeat' => array(
  163.             'between' => array(
  164.                 'rule' => array('between', PWD_MIN_LENGTH, PWD_MAX_LENGTH),
  165.                 'message' => array('valErrBetweenCharacters %s %s', PWD_MIN_LENGTH, PWD_MAX_LENGTH),
  166.                 'allowEmpty' => null,
  167.                 'last' => true,
  168.             ),
  169.             'validateIdentical' => array(
  170.                 'rule' => array('validateIdentical', 'formField'),
  171.                 'message' => 'valErrPwdNotMatch',
  172.                 'allowEmpty' => null,
  173.                 'last' => true,
  174.             )
  175.         ),
  176.         'formFieldCurrent' => array(
  177.             'notEmpty' => array(
  178.                 'rule' => array('notEmpty'),
  179.                 'message' => 'valErrProvideCurrentPwd',
  180.                 'allowEmpty' => null,
  181.                 'last' => true,
  182.             ),
  183.             'validateCurrentPwd' => array(
  184.                 'rule' => 'validateCurrentPwd',
  185.                 'message' => 'valErrCurrentPwdIncorrect',
  186.                 'allowEmpty' => null,
  187.                 'last' => true,
  188.             )
  189.         )
  190.     );
  191.  
  192.     /**
  193.      * if not implemented in AppModel
  194.      * @throws CakeException
  195.      * @return bool $success
  196.      * 2011-07-22 ms
  197.      */
  198.     public function validateCurrentPwd(Model $Model, $data) {
  199.  
  200.         debug(func_get_args());
  201.  
  202.         if (is_array($data)) {
  203.             $pwd = array_shift($data);
  204.         } else {
  205.             $pwd = $data;
  206.         }
  207.  
  208.         $uid = null;
  209.         if ($Model->id) {
  210.             $uid = $Model->id;
  211.         } elseif (!empty($Model->data[$Model->alias]['id'])) {
  212.             $uid = $Model->data[$Model->alias]['id'];
  213.         } else {
  214.             trigger_error('No user id given');
  215.             return false;
  216.         }
  217.  
  218.         $auth = 'Auth';
  219.         if (empty($this->settings[$Model->alias]['auth']) && class_exists('AuthExtComponent')) {
  220.             $auth = 'AuthExt';
  221.         } elseif ($this->settings[$Model->alias]['auth']) {
  222.             $auth = $this->settings[$Model->alias]['auth'];
  223.         }
  224.         $authClass = $auth . 'Component';
  225.         if (!class_exists($authClass)) {
  226.             throw new CakeException('No Authentication class found (' . $authClass. ')');
  227.         }
  228.  
  229.         $this->Auth = new $authClass(new ComponentCollection());
  230.  
  231.         # easiest authenticate method via form and (id + pwd)
  232.         $this->Auth->authenticate = array(
  233.             'Form' => array(
  234.                 'fields' => array('username' => 'id', 'password' => $this->settings[$Model->alias]['field'])
  235.             )
  236.         );
  237.         $request = Router::getRequest();
  238.         $request->data['User'] = array('id' => $uid, 'password' => $pwd);
  239.         $response = new CakeResponse();
  240.         return (bool)$this->Auth->identify($request, $response);
  241.     }
  242.  
  243.     /**
  244.      * if not implemented in AppModel
  245.      * @return bool $success
  246.      * 2011-07-22 ms
  247.      */
  248.     public function validateIdentical(Model $Model, $data, $compareWith = null) {
  249.  
  250.  
  251.         if (is_array($data)) {
  252.             $value = array_shift($data);
  253.         } else {
  254.             $value = $data;
  255.         }
  256.         $compareValue = $Model->data[$Model->alias][$compareWith];
  257.         return ($compareValue === $value);
  258.     }
  259.  
  260.     /**
  261.      * if not implemented in AppModel
  262.      * @return bool $success
  263.      * 2011-11-10 ms
  264.      */
  265.     public function validateNotSame(Model $Model, $data, $field1, $field2) {
  266.         $value1 = $Model->data[$Model->alias][$field1];
  267.         $value2 = $Model->data[$Model->alias][$field2];
  268.         return ($value1 !== $value2);
  269.     }
  270.  
  271.     /**
  272.      * if not implemented in AppModel
  273.      * @return bool $success
  274.      * 2011-11-10 ms
  275.      */
  276.     public function validateNotSameHash(Model $Model, $data, $formField) {
  277.         $field = $this->settings[$Model->alias]['field'];
  278.         $type = $this->settings[$Model->alias]['hashType'];
  279.         $salt = $this->settings[$Model->alias]['hashSalt'];
  280.  
  281.         if (!isset($Model->data[$Model->alias][$Model->primaryKey])) {
  282.             return true;
  283.         }
  284.         $primaryKey = $Model->data[$Model->alias][$Model->primaryKey];
  285.         $value = Security::hash($Model->data[$Model->alias][$formField], $type, $salt);
  286.         $dbValue = $Model->field($field, array($Model->primaryKey => $primaryKey));
  287.         if (!$dbValue) {
  288.             return true;
  289.         }
  290.         return ($value !== $dbValue);
  291.     }
  292.  
  293.     /**
  294.      * Adding validation rules
  295.      * also adds and merges config settings (direct + configure)
  296.      *
  297.      * @return void
  298.      * 2011-08-24 ms
  299.      */
  300.     public function setup(Model $Model, $config = array()) {
  301.         $defaults = $this->_defaults;
  302.         if ($configureDefaults = Configure::read('Passwordable')) {
  303.             $defaults = Set::merge($defaults, $configureDefaults);
  304.         }
  305.         $this->settings[$Model->alias] = Set::merge($defaults, $config);
  306.  
  307.         $formField = $this->settings[$Model->alias]['formField'];
  308.         $formFieldRepeat = $this->settings[$Model->alias]['formFieldRepeat'];
  309.         $formFieldCurrent = $this->settings[$Model->alias]['formFieldCurrent'];
  310.  
  311.         $rules = $this->_validationRules;
  312.  
  313.         # add the validation rules if not already attached
  314.         if (!isset($Model->validate[$formField])) {
  315.             $Model->validator()->add($formField, $rules['formField']);
  316.         }
  317.         if (!isset($Model->validate[$formFieldRepeat])) {
  318.             $ruleSet = $rules['formFieldRepeat'];
  319.             $ruleSet['validateIdentical']['rule'][1] = $formField;
  320.             $Model->validator()->add($formFieldRepeat, $ruleSet);
  321.         }
  322.  
  323.         if ($this->settings[$Model->alias]['current'] && !isset($Model->validate[$formFieldCurrent])) {
  324.             $Model->validator()->add($formFieldCurrent, $rules['formFieldCurrent']);
  325.  
  326.             if (!$this->settings[$Model->alias]['allowSame']) {
  327.                 $Model->validator()->add($formField, 'validateNotSame', array(
  328.                     'rule' => array('validateNotSame', $formField, $formFieldCurrent),
  329.                     'message' => 'valErrPwdSameAsBefore',
  330.                     'allowEmpty' => $this->settings[$Model->alias]['allowEmpty'],
  331.                     'last' => true,
  332.                 ));
  333.             }
  334.         } elseif (!isset($Model->validate[$formFieldCurrent])) {
  335.             # try to match the password against the hash in the DB
  336.             if (!$this->settings[$Model->alias]['allowSame']) {
  337.                 $Model->validator()->add($formField, 'validateNotSame', array(
  338.                     'rule' => array('validateNotSameHash', $formField),
  339.                     'message' => 'valErrPwdSameAsBefore',
  340.                     'allowEmpty' => $this->settings[$Model->alias]['allowEmpty'],
  341.                     'last' => true,
  342.                 ));
  343.             }
  344.         }
  345.     }
  346.  
  347.     /**
  348.      * Preparing the data
  349.      *
  350.      * @return bool $success
  351.      * 2011-07-22 ms
  352.      */
  353.     public function beforeValidate(Model $Model) {
  354.         $formField = $this->settings[$Model->alias]['formField'];
  355.         $formFieldRepeat = $this->settings[$Model->alias]['formFieldRepeat'];
  356.         $formFieldCurrent = $this->settings[$Model->alias]['formFieldCurrent'];
  357.  
  358.         # make sure fields are set and validation rules are triggered - prevents tempering of form data
  359.         if (!isset($Model->data[$Model->alias][$formField])) {
  360.             $Model->data[$Model->alias][$formField] = '';
  361.         }
  362.         if ($this->settings[$Model->alias]['confirm'] && !isset($Model->data[$Model->alias][$formFieldRepeat])) {
  363.             $Model->data[$Model->alias][$formFieldRepeat] = '';
  364.         }
  365.         if ($this->settings[$Model->alias]['current'] && !isset($Model->data[$Model->alias][$formFieldCurrent])) {
  366.             $Model->data[$Model->alias][$formFieldCurrent] = '';
  367.         }
  368.  
  369.         # check if we need to trigger any validation rules
  370.         if ($this->settings[$Model->alias]['allowEmpty']) {
  371.             $current = !empty($Model->data[$Model->alias][$formFieldCurrent]);
  372.             $new = !empty($Model->data[$Model->alias][$formField]) || !empty($Model->data[$Model->alias][$formFieldRepeat]);
  373.             if (!$new && !$current) {
  374.                 //$Model->validator()->remove($formField); // tmp only!
  375.                 //unset($Model->validate[$formField]);
  376.                 unset($Model->data[$Model->alias][$formField]);
  377.                 if ($this->settings[$Model->alias]['confirm']) {
  378.                     //$Model->validator()->remove($formFieldRepeat); // tmp only!
  379.                     //unset($Model->validate[$formFieldRepeat]);
  380.                     unset($Model->data[$Model->alias][$formFieldRepeat]);
  381.                 }
  382.                 if ($this->settings[$Model->alias]['current']) {
  383.                     //$Model->validator()->remove($formFieldCurrent); // tmp only!
  384.                     //unset($Model->validate[$formFieldCurrent]);
  385.                     unset($Model->data[$Model->alias][$formFieldCurrent]);
  386.                 }
  387.                 return true;
  388.             }
  389.         }
  390.  
  391.         # add fields to whitelist!
  392.         $whitelist = array($this->settings[$Model->alias]['formField'], $this->settings[$Model->alias]['formFieldRepeat']);
  393.         if ($this->settings[$Model->alias]['current']) {
  394.             $whitelist[] = $this->settings[$Model->alias]['formFieldCurrent'];
  395.         }
  396.         if (!empty($Model->whitelist)) {
  397.             $Model->whitelist = array_merge($Model->whitelist, $whitelist);
  398.         }
  399.  
  400.         return true;
  401.     }
  402.  
  403.  
  404.     /**
  405.      * Hashing the password and whitelisting
  406.      *
  407.      * @return bool $success
  408.      * 2011-07-22 ms
  409.      */
  410.     public function beforeSave(Model $Model) {
  411.  
  412.         //debug(func_get_args());
  413.  
  414.         $formField = $this->settings[$Model->alias]['formField'];
  415.         $field = $this->settings[$Model->alias]['field'];
  416.         $type = $this->settings[$Model->alias]['hashType'];
  417.         $salt = $this->settings[$Model->alias]['hashSalt'];
  418.  
  419.         if (isset($Model->data[$Model->alias][$formField])) {
  420.             $Model->data[$Model->alias][$field] = Security::hash($Model->data[$Model->alias][$formField], $type, $salt);
  421.             unset($Model->data[$Model->alias][$formField]);
  422.             if ($this->settings[$Model->alias]['confirm']) {
  423.                 $formFieldRepeat = $this->settings[$Model->alias]['formFieldRepeat'];
  424.                 unset($Model->data[$Model->alias][$formFieldRepeat]);
  425.             }
  426.             if ($this->settings[$Model->alias]['current']) {
  427.                 $formFieldCurrent = $this->settings[$Model->alias]['formFieldCurrent'];
  428.                 unset($Model->data[$Model->alias][$formFieldCurrent]);
  429.             }
  430.             # update whitelist
  431.             if (!empty($Model->whitelist)) {
  432.                 $Model->whitelist = array_merge($Model->whitelist, array($field));
  433.             }
  434.         }
  435.  
  436.         return true;
  437.     }
  438.  
  439. }
RAW Paste Data