PasswordableBehavior.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. <?php
  2. App::uses('ModelBehavior', 'Model');
  3. App::uses('Router', 'Routing');
  4. App::uses('CakeRequest', 'Network');
  5. App::uses('CakeResponse', 'Network');
  6. App::uses('Security', 'Utility');
  7. // @deprecated Use Configure settings instead.
  8. if (!defined('PWD_MIN_LENGTH')) {
  9. define('PWD_MIN_LENGTH', 6);
  10. }
  11. if (!defined('PWD_MAX_LENGTH')) {
  12. define('PWD_MAX_LENGTH', 20);
  13. }
  14. /**
  15. * A CakePHP2 behavior to work with passwords the easy way
  16. * - complete validation
  17. * - hashing of password
  18. * - requires fields (no tempering even without security component)
  19. * - usable for edit forms (require=>false for optional password update)
  20. *
  21. * Usage: Do NOT add it via $actAs = array()
  22. * attach it dynamically in only those actions where you actually change the password like so:
  23. * $this->User->Behaviors->load('Tools.Passwordable', array(SETTINGSARRAY));
  24. * as first line in any action where you want to allow the user to change his password
  25. * also add the two form fields in the form (pwd, pwd_confirm)
  26. * the rest is cake automagic :)
  27. *
  28. * Also note that you can apply global settings via Configure key 'Passwordable', as well,
  29. * if you don't want to manually pass them along each time you use the behavior. This also
  30. * keeps the code clean and lean.
  31. *
  32. * Now also is capable of:
  33. * - require current password prior to altering it (current=>true)
  34. * - don't allow the same password it was before (allowSame=>false)
  35. * - supporting different auth types and password hashing algorythms
  36. *
  37. * @version 1.7 (Now CakePHP2.4/2.5 ready - with passwordHasher support)
  38. * @author Mark Scherer
  39. * @link http://www.dereuromark.de/2011/08/25/working-with-passwords-in-cakephp
  40. * @license MIT
  41. */
  42. class PasswordableBehavior extends ModelBehavior {
  43. /**
  44. * @var array
  45. */
  46. protected $_defaultConfig = array(
  47. 'field' => 'password',
  48. 'confirm' => true, // Set to false if in admin view and no confirmation (pwd_repeat) is required
  49. 'require' => true, // If a password change is required (set to false for edit forms, leave it true for pure password update forms)
  50. 'allowEmpty' => false, // Deprecated, do NOT use anymore! Use require instead!
  51. 'current' => false, // Enquire the current password for security purposes
  52. 'formField' => 'pwd',
  53. 'formFieldRepeat' => 'pwd_repeat',
  54. 'formFieldCurrent' => 'pwd_current',
  55. 'userModel' => null, // Defaults to User
  56. 'hashType' => null, // Only for authType Form [cake2.3]
  57. 'hashSalt' => true, // Only for authType Form [cake2.3]
  58. 'auth' => null, // Which component (defaults to AuthComponent),
  59. 'authType' => 'Form', // Which type of authenticate (Form, Blowfish, ...) [cake2.4]
  60. 'passwordHasher' => null, // If a custom pwd hasher is been used [cake2.4]
  61. 'allowSame' => true, // Don't allow the old password on change
  62. 'minLength' => PWD_MIN_LENGTH,
  63. 'maxLength' => PWD_MAX_LENGTH
  64. );
  65. /**
  66. * @var array
  67. */
  68. protected $_validationRules = array(
  69. 'formField' => array(
  70. 'between' => array(
  71. 'rule' => array('between', PWD_MIN_LENGTH, PWD_MAX_LENGTH),
  72. 'message' => array('valErrBetweenCharacters %s %s', PWD_MIN_LENGTH, PWD_MAX_LENGTH),
  73. 'allowEmpty' => null,
  74. 'last' => true,
  75. )
  76. ),
  77. 'formFieldRepeat' => array(
  78. 'between' => array(
  79. 'rule' => array('between', PWD_MIN_LENGTH, PWD_MAX_LENGTH),
  80. 'message' => array('valErrBetweenCharacters %s %s', PWD_MIN_LENGTH, PWD_MAX_LENGTH),
  81. 'allowEmpty' => null,
  82. 'last' => true,
  83. ),
  84. 'validateIdentical' => array(
  85. 'rule' => array('validateIdentical', 'formField'),
  86. 'message' => 'valErrPwdNotMatch',
  87. 'allowEmpty' => null,
  88. 'last' => true,
  89. ),
  90. ),
  91. 'formFieldCurrent' => array(
  92. 'notEmpty' => array(
  93. 'rule' => array('notEmpty'),
  94. 'message' => 'valErrProvideCurrentPwd',
  95. 'allowEmpty' => null,
  96. 'last' => true,
  97. ),
  98. 'validateCurrentPwd' => array(
  99. 'rule' => 'validateCurrentPwd',
  100. 'message' => 'valErrCurrentPwdIncorrect',
  101. 'allowEmpty' => null,
  102. 'last' => true,
  103. )
  104. ),
  105. );
  106. /**
  107. * If not implemented in AppModel
  108. *
  109. * Note: requires the used Auth component to be App::uses() loaded.
  110. * It also reqires the same Auth setup as in your AppController's beforeFilter().
  111. * So if you set up any special passwordHasher or auth type, you need to provide those
  112. * with the settings passed to the behavior:
  113. *
  114. * 'authType' => 'Blowfish', 'passwordHasher' => array(
  115. * 'className' => 'Simple',
  116. * 'hashType' => 'sha256'
  117. * )
  118. *
  119. * @throws CakeException
  120. * @param Model $Model
  121. * @param array $data
  122. * @return bool Success
  123. */
  124. public function validateCurrentPwd(Model $Model, $data) {
  125. if (is_array($data)) {
  126. $pwd = array_shift($data);
  127. } else {
  128. $pwd = $data;
  129. }
  130. $uid = null;
  131. if ($Model->id) {
  132. $uid = $Model->id;
  133. } elseif (!empty($Model->data[$Model->alias]['id'])) {
  134. $uid = $Model->data[$Model->alias]['id'];
  135. } else {
  136. trigger_error('No user id given');
  137. return false;
  138. }
  139. $auth = 'Auth';
  140. if (empty($this->settings[$Model->alias]['auth']) && class_exists('AuthExtComponent')) {
  141. $auth = 'AuthExt';
  142. } elseif ($this->settings[$Model->alias]['auth']) {
  143. $auth = $this->settings[$Model->alias]['auth'];
  144. }
  145. $authClass = $auth . 'Component';
  146. if (!class_exists($authClass)) {
  147. throw new CakeException('No Authentication class found (' . $authClass . ')');
  148. }
  149. $this->Auth = new $authClass(new ComponentCollection());
  150. // Easiest authenticate method via form and (id + pwd)
  151. $authConfig = array(
  152. 'fields' => array('username' => 'id', 'password' => $this->settings[$Model->alias]['field']),
  153. 'userModel' => $this->settings[$Model->alias]['userModel'] ? $this->settings[$Model->alias]['userModel'] : $Model->alias
  154. );
  155. if (!empty($this->settings[$Model->alias]['passwordHasher'])) {
  156. $authConfig['passwordHasher'] = $this->settings[$Model->alias]['passwordHasher'];
  157. }
  158. $this->Auth->authenticate = array(
  159. $this->settings[$Model->alias]['authType'] => $authConfig
  160. );
  161. $request = Router::getRequest();
  162. $request->data[$Model->alias] = array('id' => $uid, 'password' => $pwd);
  163. $response = new CakeResponse();
  164. return (bool)$this->Auth->identify($request, $response);
  165. }
  166. /**
  167. * If not implemented in AppModel
  168. *
  169. * @param Model $Model
  170. * @param array $data
  171. * @param string $compareWith String to compare field value with
  172. * @return bool Success
  173. */
  174. public function validateIdentical(Model $Model, $data, $compareWith = null) {
  175. if (is_array($data)) {
  176. $value = array_shift($data);
  177. } else {
  178. $value = $data;
  179. }
  180. $compareValue = $Model->data[$Model->alias][$compareWith];
  181. return ($compareValue === $value);
  182. }
  183. /**
  184. * If not implemented in AppModel
  185. *
  186. * @return bool Success
  187. */
  188. public function validateNotSame(Model $Model, $data, $field1, $field2) {
  189. $value1 = $Model->data[$Model->alias][$field1];
  190. $value2 = $Model->data[$Model->alias][$field2];
  191. return ($value1 !== $value2);
  192. }
  193. /**
  194. * If not implemented in AppModel
  195. *
  196. * @return bool Success
  197. */
  198. public function validateNotSameHash(Model $Model, $data, $formField) {
  199. $field = $this->settings[$Model->alias]['field'];
  200. $type = $this->settings[$Model->alias]['hashType'];
  201. $salt = $this->settings[$Model->alias]['hashSalt'];
  202. if ($this->settings[$Model->alias]['authType'] === 'Blowfish') {
  203. $type = 'blowfish';
  204. $salt = false;
  205. }
  206. if (!isset($Model->data[$Model->alias][$Model->primaryKey])) {
  207. return true;
  208. }
  209. $primaryKey = $Model->data[$Model->alias][$Model->primaryKey];
  210. $value = Security::hash($Model->data[$Model->alias][$formField], $type, $salt);
  211. $dbValue = $Model->field($field, array($Model->primaryKey => $primaryKey));
  212. if (!$dbValue) {
  213. return true;
  214. }
  215. return ($value !== $dbValue);
  216. }
  217. /**
  218. * Adding validation rules
  219. * also adds and merges config settings (direct + configure)
  220. *
  221. * @return void
  222. */
  223. public function setup(Model $Model, $config = array()) {
  224. $defaults = $this->_defaultConfig;
  225. if ($configureDefaults = Configure::read('Passwordable')) {
  226. $defaults = array_merge($defaults, $configureDefaults);
  227. }
  228. $this->settings[$Model->alias] = array_merge($defaults, $config);
  229. // BC comp
  230. if ($this->settings[$Model->alias]['allowEmpty']) {
  231. $this->settings[$Model->alias]['require'] = false;
  232. }
  233. $formField = $this->settings[$Model->alias]['formField'];
  234. $formFieldRepeat = $this->settings[$Model->alias]['formFieldRepeat'];
  235. $formFieldCurrent = $this->settings[$Model->alias]['formFieldCurrent'];
  236. $rules = $this->_validationRules;
  237. foreach ($rules as $field => $fieldRules) {
  238. foreach ($fieldRules as $key => $rule) {
  239. $rule['allowEmpty'] = !$this->settings[$Model->alias]['require'];
  240. if ($key === 'between') {
  241. $rule['rule'][1] = $this->settings[$Model->alias]['minLength'];
  242. $rule['message'][1] = $this->settings[$Model->alias]['minLength'];
  243. $rule['rule'][2] = $this->settings[$Model->alias]['maxLength'];
  244. $rule['message'][2] = $this->settings[$Model->alias]['maxLength'];
  245. }
  246. $fieldRules[$key] = $rule;
  247. }
  248. $rules[$field] = $fieldRules;
  249. }
  250. // Add the validation rules if not already attached
  251. if (!isset($Model->validate[$formField])) {
  252. $Model->validator()->add($formField, $rules['formField']);
  253. }
  254. if (!isset($Model->validate[$formFieldRepeat])) {
  255. $ruleSet = $rules['formFieldRepeat'];
  256. $ruleSet['validateIdentical']['rule'][1] = $formField;
  257. $Model->validator()->add($formFieldRepeat, $ruleSet);
  258. }
  259. if ($this->settings[$Model->alias]['current'] && !isset($Model->validate[$formFieldCurrent])) {
  260. $Model->validator()->add($formFieldCurrent, $rules['formFieldCurrent']);
  261. if (!$this->settings[$Model->alias]['allowSame']) {
  262. $Model->validator()->add($formField, 'validateNotSame', array(
  263. 'rule' => array('validateNotSame', $formField, $formFieldCurrent),
  264. 'message' => 'valErrPwdSameAsBefore',
  265. 'allowEmpty' => !$this->settings[$Model->alias]['require'],
  266. 'last' => true,
  267. ));
  268. }
  269. } elseif (!isset($Model->validate[$formFieldCurrent])) {
  270. // Try to match the password against the hash in the DB
  271. if (!$this->settings[$Model->alias]['allowSame']) {
  272. $Model->validator()->add($formField, 'validateNotSame', array(
  273. 'rule' => array('validateNotSameHash', $formField),
  274. 'message' => 'valErrPwdSameAsBefore',
  275. 'allowEmpty' => !$this->settings[$Model->alias]['require'],
  276. 'last' => true,
  277. ));
  278. }
  279. }
  280. }
  281. /**
  282. * Preparing the data
  283. *
  284. * @return bool Success
  285. */
  286. public function beforeValidate(Model $Model, $options = array()) {
  287. $formField = $this->settings[$Model->alias]['formField'];
  288. $formFieldRepeat = $this->settings[$Model->alias]['formFieldRepeat'];
  289. $formFieldCurrent = $this->settings[$Model->alias]['formFieldCurrent'];
  290. // Make sure fields are set and validation rules are triggered - prevents tempering of form data
  291. if (!isset($Model->data[$Model->alias][$formField])) {
  292. $Model->data[$Model->alias][$formField] = '';
  293. }
  294. if ($this->settings[$Model->alias]['confirm'] && !isset($Model->data[$Model->alias][$formFieldRepeat])) {
  295. $Model->data[$Model->alias][$formFieldRepeat] = '';
  296. }
  297. if ($this->settings[$Model->alias]['current'] && !isset($Model->data[$Model->alias][$formFieldCurrent])) {
  298. $Model->data[$Model->alias][$formFieldCurrent] = '';
  299. }
  300. // Check if we need to trigger any validation rules
  301. if (!$this->settings[$Model->alias]['require']) {
  302. $current = !empty($Model->data[$Model->alias][$formFieldCurrent]);
  303. $new = !empty($Model->data[$Model->alias][$formField]) || !empty($Model->data[$Model->alias][$formFieldRepeat]);
  304. if (!$new && !$current) {
  305. //$Model->validator()->remove($formField); // tmp only!
  306. //unset($Model->validate[$formField]);
  307. unset($Model->data[$Model->alias][$formField]);
  308. if ($this->settings[$Model->alias]['confirm']) {
  309. //$Model->validator()->remove($formFieldRepeat); // tmp only!
  310. //unset($Model->validate[$formFieldRepeat]);
  311. unset($Model->data[$Model->alias][$formFieldRepeat]);
  312. }
  313. if ($this->settings[$Model->alias]['current']) {
  314. //$Model->validator()->remove($formFieldCurrent); // tmp only!
  315. //unset($Model->validate[$formFieldCurrent]);
  316. unset($Model->data[$Model->alias][$formFieldCurrent]);
  317. }
  318. return true;
  319. }
  320. // Make sure we trigger validation if allowEmpty is set but we have the password field set
  321. if ($new) {
  322. if ($this->settings[$Model->alias]['confirm'] && empty($Model->data[$Model->alias][$formFieldRepeat])) {
  323. $Model->invalidate($formFieldRepeat, __('valErrPwdNotMatch'));
  324. }
  325. }
  326. }
  327. // Update whitelist
  328. $this->_modifyWhitelist($Model);
  329. return true;
  330. }
  331. /**
  332. * Hashing the password and whitelisting
  333. *
  334. * @param Model $Model
  335. * @return bool Success
  336. */
  337. public function beforeSave(Model $Model, $options = array()) {
  338. $formField = $this->settings[$Model->alias]['formField'];
  339. $field = $this->settings[$Model->alias]['field'];
  340. $type = $this->settings[$Model->alias]['hashType'];
  341. $salt = $this->settings[$Model->alias]['hashSalt'];
  342. if ($this->settings[$Model->alias]['authType'] === 'Blowfish') {
  343. $type = 'blowfish';
  344. $salt = false;
  345. }
  346. if (isset($Model->data[$Model->alias][$formField])) {
  347. $Model->data[$Model->alias][$field] = Security::hash($Model->data[$Model->alias][$formField], $type, $salt);
  348. unset($Model->data[$Model->alias][$formField]);
  349. if ($this->settings[$Model->alias]['confirm']) {
  350. $formFieldRepeat = $this->settings[$Model->alias]['formFieldRepeat'];
  351. unset($Model->data[$Model->alias][$formFieldRepeat]);
  352. }
  353. if ($this->settings[$Model->alias]['current']) {
  354. $formFieldCurrent = $this->settings[$Model->alias]['formFieldCurrent'];
  355. unset($Model->data[$Model->alias][$formFieldCurrent]);
  356. }
  357. }
  358. // Update whitelist
  359. $this->_modifyWhitelist($Model, true);
  360. return true;
  361. }
  362. /**
  363. * Modify the model's whitelist.
  364. *
  365. * Since 2.5 behaviors can also modify the whitelist for validate, thus this behavior can now
  366. * (>= CakePHP 2.5) add the form fields automatically, as well (not just the password field itself).
  367. *
  368. * @param Model $Model
  369. * @return void
  370. */
  371. protected function _modifyWhitelist(Model $Model, $onSave = false) {
  372. $fields = array();
  373. if ($onSave) {
  374. $fields[] = $this->settings[$Model->alias]['field'];
  375. } else {
  376. $fields[] = $this->settings[$Model->alias]['formField'];
  377. if ($this->settings[$Model->alias]['confirm']) {
  378. $fields[] = $this->settings[$Model->alias]['formFieldRepeat'];
  379. }
  380. if ($this->settings[$Model->alias]['current']) {
  381. $fields[] = $this->settings[$Model->alias]['formFieldCurrent'];
  382. }
  383. }
  384. foreach ($fields as $field) {
  385. if (!empty($Model->whitelist) && !in_array($field, $Model->whitelist)) {
  386. $Model->whitelist = array_merge($Model->whitelist, array($field));
  387. }
  388. }
  389. }
  390. }