A CakePHP behavior to work with passwords the easy way
Also capable of:
You can either pass those to the behavior at runtime, or globally via Configure and app.php:
$config = [
'Passwordable' => [
'passwordHasher' => ['className' => 'Fallback', 'hashers' => ['Default', 'Weak']]
]
]
In this case we use the Fallback hasher class and both Default (Blowfish, CakePHP3 default) and Weak (E.g. sha1) hashing algorithms. The latter is necessary when you try to upgrade an existing CakePHP2 application which used some weak hashing algo to Cake3. This way you can use both parallel. And new accounts will use the new hasher. Order matters!
Do NOT hard-add it in the model itself. Attach it dynamically in only those actions where you actually change the password like so:
$this->Users->addBehavior('Tools.Passwordable', $config);
as first line in any action where you want to allow the user to change his password. Also add the two form fields in the form (pwd, pwd_confirm)
The rest is CakePHP automagic :)
Also note that you can apply global settings via Configure key 'Passwordable', as well,
if you don't want to manually pass them along each time you use the behavior. This also
keeps the code clean and lean. See the app.default.php file for details.
And do NOT add any password hashing to your Table or Entity classes. That would hash the password twice.
namespace App\Controller;
use Tools\Controller\Controller;
class UsersController extends Controller {
public function register() {
$this->Users->addBehavior('Tools.Passwordable');
$user = $this->Users->newEntity($this->request->data);
if ($this->request->is(['put', 'post'])) {
$user->role_id = Configure::read('Roles.user');
if ($this->Users->save($user)) {
// Log in right away
$this->Auth->setUser($user->toArray());
// Flash message OK
return $this->redirect(array('action' => 'index'));
}
// Flash message ERROR
// Pwd should not be passed to the view again for security reasons
$user->unsetProperty('pwd');
$user->unsetProperty('pwd_repeat');
}
$this->set(compact('user'));
}
}
namespace App\Controller;
use Tools\Controller\Controller;
class UsersController extends Controller {
public function edit() {
$uid = $this->request->session()->read('Auth.User.id');
$user = $this->Users->get($uid);
$this->Users->addBehavior('Tools.Passwordable', array('require' => false));
if ($this->request->is(['put', 'post'])) {
$options = array(
'fieldList' => array(...)
);
$user = $this->Users->patchEntity($user, $this->request->data);
if ($this->Users->save($user, $options)) {
// Update session data, as well
$this->Auth->setUser($user->toArray());
// Flash message OK
return $this->redirect(array('action' => 'index'));
}
// Flash message ERROR
}
$this->set(compact('user'));
}
}
In the config example above you can see both Default and Weak hashers being used. We want to upgrade all accounts piece by piece upon login automatically. This way it can be done without the user noticing:
public function login() {
if ($this->request->is(['put', 'post'])) {
$user = $this->Auth->identify();
if ($user) {
$this->Users->addBehavior('Tools.Passwordable', array('confirm' => false));
$password = $this->request->data['password'];
$dbPassword = $this->Users->field('password', array('id' => $user['id']));
if ($this->Users->needsPasswordRehash($dbPassword)) {
$data = array(
'id' => $user['id'],
'pwd' => $password,
'modified' => false
);
$updatedUser = $this->Users->newEntity($data, ['markNew' => false]);
if (!$this->Users->save($updatedUser, ['validate' => false])) {
trigger_error(sprintf('Could not store new pwd for user %s.', $user['id']));
}
}
unset($user['password']);
$this->Auth->setUser($user);
// Flash message OK
return $this->redirect($this->Auth->redirectUrl());
}
// Flash message ERROR
}
}
Note that the passwordHasher config has been set here globabally to assert the Fallback hasher class to kick in.