Legacy Password Hasher Not Working Migrating CodeIgniter2 to Cake 3 - cakephp

I have a legacy CRM application that I am migrating from Code Igniter 2.x to Cake 3. I am attempting to implement a Legacy Password Hasher and then move everything to the Cake 3 password hasher.
I have been unable to get authentication to work. As you'll see I've had to deviate from various cake defaults, in particular all tables were created as singular names. Not sure if that's causing any issues. It's been an ordeal. If it matters, Admin exists as a plugin.
AppController
namespace Admin\Controller;
use App\Controller\AppController as BaseController;
class AppController extends BaseController
{
public function initialize() {
parent::initialize();
$this->loadComponent('Auth', [
'loginAction' => '/admin/login', // routes to /admin/user/login
'authenticate' => [
'Form' => [
'passwordHasher' => [
'className' => 'Legacy',
],
'fields' => [
'username' => ['email_address','handle'],
'password' => 'password',
],
'userModel' => 'User',
],
],
'storage' => 'Session'
]);
}
}
UserController
if( $this->request->is('post') ){
$this->loadModel('User');
$me = $this->User->find()->where([
'type IN ' => ['superuser','administrator','user','publisher'],
'status IN ' => ['active','suspended'],
'OR' => [
'email_address' => $this->request->data('username'), 'handle' => $this->request->data('username')
]
])->first();
if( $me->status == 'active' ){
$user = $this->Auth->identify();
if( $user ){
$this->Auth->setUser($user);
if( $this->Auth->authenticationProvider()->needsPasswordRehash() ){
$user = $this->User->get($this->Auth->user('id'));
$user->password = $this->request->data('password');
$this->User->save($user);
}
switch($user->type){
case 'superuser':
$url = '/admin/dashboard/super';
break;
case 'admin':
$url = '/admin/dashboard/';
break;
case 'publisher':
$url = '/admin/dashboard/publisher';
break;
case 'property':
$url = '/admin/dashboard/property';
break;
}
return $this->redirect($url);
}
}
$this->Flash->error(__('Username or password is incorrect'), [
'key' => 'auth'
]);
}
LegacyPasswordHasher
namespace App\Auth;
use Cake\Auth\AbstractPasswordHasher;
class LegacyPasswordHasher extends AbstractPasswordHasher
{
public function hash($password){
$salt_length = 16;
$salt = substr($password, 0, $salt_length);
return $salt . sha1($salt . $password);
}
public function check($password, $hashedPassword){
return $this->hash($password) === $hashedPassword;
}
}
The legacy password hasher does not even seem to be called. I did a work around to force authentication by doing my own check and then setting the auth user data. Still after that when rehashing the password using Cakes DefaultPasswordHasher authentication failed.

I figured this out. There were a few issues here. One, I needed to change the configuration to this:
$this->loadComponent('Auth', [
'loginAction' => '/admin/login',
'authenticate' => [
'Form' => [
'finder' => 'auth',
'passwordHasher' => [
'className' => 'Legacy',
],
'fields' => [
'username' => 'email_address',
'password' => 'password',
],
'userModel' => 'User',
],
],
'storage' => 'Session'
]);
Next, the field I posted needed to be email_address and not username. Last, there was a bug in my implementation of Code Igniters password hasher. It ended up needing to look like this:
public function hash($password){
$salt_length = 16;
$salt = substr($this->salt, 0, $salt_length);
return $salt . sha1($salt . $password);
}
public function check($password, $hashedPassword){
$this->salt = $hashedPassword;
return $this->hash($password) === $hashedPassword;
}
I also needed a custom finder query added to UserTable:
public function findAuth(\Cake\ORM\Query $query, array $options)
{
$query
->select(['id', 'email_address','handle','password'])
->where(['User.status' => 'active']);
return $query;
}
To handle users being able to login with a username or email_address I will just override the the posted value if its not an email address by looking up the user by handle prior to auth doing its thing.
I hope this helps someone.

Related

cakedc/users autologin after registration

I'm using the cakephp 4.2.6 with the cakedc/users plugin. I'd like to automatically login the user after registration without the need to first activate the account by clicking on the link in e-mail.
My idea is to creating an Event-Listener and listening to the Users.Global.afterRegister Event, but then I don't know how to login the user with the new Authentication.
this is what I ended up with and it seems to work so far:
<?php
namespace App\Event;
use Authentication\AuthenticationService;
use Authentication\AuthenticationServiceInterface;
use Authentication\AuthenticationServiceProviderInterface;
use Authentication\Middleware\AuthenticationMiddleware;
use Psr\Http\Message\ServerRequestInterface;
use Cake\Event\EventListenerInterface;
use Cake\Datasource\FactoryLocator;
use Cake\Log\Log;
class UserEventListener implements EventListenerInterface
{
public function implementedEvents(): array
{
return [
'Users.Global.afterRegister' => 'autoLogin',
];
}
public function autoLogin($event)
{
$user = $usersTable->get($event->getData('user')->id);
$request = $event->getSubject()->getRequest();
$response = $event->getSubject()->getResponse();
$authenticationService = $this->getAuthenticationService($request);
$authenticationService->persistIdentity($request, $response, $user);
}
public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
{
$authenticationService = new AuthenticationService([
'unauthenticatedRedirect' => \Cake\Routing\Router::url([
'controller' => 'Users',
'action' => 'login',
'plugin' => null,
'prefix' => null
]),
'queryParam' => 'redirect',
]);
// Load identifiers, ensure we check email and password fields
$authenticationService->loadIdentifier('Authentication.Password', [
'fields' => [
'username' => 'email',
'password' => 'password',
]
]);
// Load the authenticators, you want session first
$authenticationService->loadAuthenticator('Authentication.Session');
// Configure form data check to pick email and password
$authenticationService->loadAuthenticator('Authentication.Form', [
'fields' => [
'username' => 'email',
'password' => 'password',
],
'loginUrl' => \Cake\Routing\Router::url([
'controller' => 'Users',
'action' => 'login',
'plugin' => null,
'prefix' => null
]),
]);
return $authenticationService;
}
}
There is a similar use case covered in the plugin documentation https://github.com/CakeDC/users/blob/master/Docs/Documentation/Events.md
Your approach looks correct to me, using the afterRegister event to create the listener in a controller where you have access to the Authenticatication component,
// note this example is handling auto-login after the user validated the email sent (click the validation token link sent to his email address)
EventManager::instance()->on(
\CakeDC\Users\Plugin::EVENT_AFTER_EMAIL_TOKEN_VALIDATION,
function($event){
$users = $this->getTableLocator()->get('Users');
$user = $users->get($event->getData('user')->id);
$this->Authentication->setIdentity($user);
}
);
or using the request to retrieve the authentication service and set the identity like it's done here https://github.com/cakephp/authentication/blob/master/src/Controller/Component/AuthenticationComponent.php#L273

Display username after login in cakephp3

I want to display username after login in cakephp3. I write follow code but part of show username doesn't work. user after login redirect to main page.
Hi <?php print $this->request->session()->read('User',$user); ?>
login in UserController:
public function login() {
if ($this->request->session()->read('login_ok') == '1') {
$this->redirect(['controller' => 'users', 'action' => 'main']);
}
if ($this->request->is('post')) {
//$hasher = new DefaultPasswordHasher();
$count = $this->Users->find()
->where(['username' => $this->request->data['username'],
// 'password' => $this->request->data['password']])
'password' => md5($this->request->data['password'])])
->count();
if ($count > 0) {
$result = true;
$this->request->session()->write('User', $user); //store username
$this->request->session()->write('login_ok', '1');
$this->redirect(['controller' => 'users', 'action' => 'main']);
} else {
$result = false;
}
$this->set('result', $result);
}
}
public function main() {
if ($this->request->session()->read('login_ok') != '1') {
$this->redirect(['controller' => 'users', 'action' => 'login']);
}
$query = $this->request->session()->write('User',$user);
$this->set('user', $query);
}
and this main.ctp codes:
Hi <?php print $this->request->session()->read('User', $user); ?>
Can you help in making this Code OR Suggest a new code ?
You should really be using the cakephp AuthComponent and the functions that's made for this for example identifiying the user trying to login with identify().
There is really no point using a framework if you're not going to use the functions provided by the framework as N Nem pointed out in the comments.
In the snippet you posted, you seem to never set the $user variable when you write to the session. You need to set it to be able to read it.
The following code is how you're supposed to do it in Cakephp 3 i really suggest you use that approach.
AppController.php
public function initialize()
{
$this->loadComponent('Auth', [
'loginAction' => [
'controller' => 'Users',
'action' => 'login'
],
'loginRedirect' => [
'controller' => 'Start',
'action' => 'index'
],
'logoutRedirect' => [
'controller' => 'Users',
'action' => 'login'
],
'authenticate' => [
'Form'
],
]);
}
UsersController.php
public function login()
{
if ($this->request->is('post')) {
$user = $this->Auth->identify();
if ($user) {
$this->Auth->setUser($user);
return $this->redirect($this->Auth->redirectUrl());
} else {
$this->Flash->error(__('Username or password is incorrect'), [
'key' => 'auth'
]);
}
}
}
Model/Entity/User.php
protected function _setPassword($password)
{
return (new DefaultPasswordHasher)->hash($password);
}

CakePHP 3 - Login Error - Call to a member function identify on boolean

Hello I'm using CakePHP 3 to simple setup a site which some pages of it need user to login first.
It was fine when I put the loadComponent('Auth', blablabla) code in initialize() of AppController.php.
src\Controller\AdminController.php
...
public function login() {
if ($this->request->is('post')) {
$admin = $this->Auth->identify();
if ($admin) {
$this->Auth->setUser($admin);
return $this->redirect($this->Auth->redirectUrl());
}
$this->Flash->error('Your username or password is incorrect.');
}
}
...
src\Controller\AppController.php
...
public function initialize() {
parent::initialize();
$this->loadComponent('RequestHandler');
$this->loadComponent('Flash');
$this->loadComponent('Auth', [
'Authenticate' => [
'Form' => [
'userModel' => 'Admin',
'Fields' => [
'username' => 'email',
'password' => 'password'
]
]
],
'loginAction' => [
'controller' => 'Admin',
'action' => 'login',
]
]);
$this->Auth->allow(['display']);
}
...
At this point, I needed to login in order to view all other pages of the site.
But I tried to put this same authentication setup in another controller called JustController, and after I logged in, a fetal error stated
Call to a member function identify() on boolean
has been shown.
It should be possible to setup authentication in other controllers so that the site can have more than 1 set of login system instead of covering whole site by setting up in AppController, doesn't it?
Thank you.

Better approach to do this for security and code management purpose

I am working on a Cakephp 2.3 on a very big project and I'm about to launch my site worldwide.
I have a login system on my app. I am sharing my code because I want to make sure if I am coding right or not ... and also any check for any functions missing or if any advice of adding something or removing something in the code would be greatly appreciated. And also comment in security perspective too...
Do tell me some tips of making my website faster.. for example how to write faster queries or remove unwanted from this blabla
class UsersController extends AppController
{
public $components = array('Cookie');
public function beforeFilter()
{
parent::beforeFilter();
App::uses('Utility', 'Utility');
$this->Auth->allow('index');
$this->Security->requireSecure('login'); // for security
$this->Auth->authenticate = array(
'Authenticate.Cookie' => array(
'fields' => array(
'username' => 'email',
'password' => 'password'
),
'userModel' => 'User',
'scope' => array(
'User.active' => 1
)
),
'Authenticate.MultiColumn' => array(
'fields' => array(
'username' => 'email',
'password' => 'password'
),
'columns' => array(
'email',
'mobileNo'
),
'userModel' => 'User'
)
);
}
public function index()
{
$this->layout = 'logindefault';
if (!$this->Auth->login() || !$this->Auth->loggedIn()) {
$this->redirect(array(
'controller' => 'users',
'action' => 'login'
));
} else {
$this->redirect(array(
'controller' => 'users',
'action' => 'dashboard'
));
}
}
public function login()
{
$this->layout = 'logindefault';
$this->set('title_for_layout', 'Account Login');
if ($this->Auth->login() || $this->Auth->loggedIn()) {
$lastLogin = $this->Auth->User('lastLogin');
if ($lastLogin != null) {
$this->redirect($this->Auth->redirect());
} else {
$this->redirect(array(
'controller' => 'Userinfo',
'action' => 'gettingstarted'
));
}
} else {
if ($this->request->is('post')) {
$mobileNo = $this->request->data['User']['email'];
$mobileNo = Utility::addPlusToMobileNo($mobileNo);
$this->request->data['User']['email'] = $mobileNo;
if ($this->Auth->login() || $this->Auth->loggedIn()) {
if ($this->Session->check('Auth.User')) {
$this->_setCookie($this->Auth->user('idUser'));
$lastLogin = $this->Auth->User('lastLogin');
if ($lastLogin != null) {
$this->redirect(array(
'controller' => 'users',
'action' => 'dashboard'
));
} else {
$this->redirect(array(
'controller' => 'Userinfo',
'action' => 'gettingstarted'
));
}
}
} else {
$this->Session->setFlash('Incorrect Email/Password Combination');
}
}
}
}
protected function _setCookie($id)
{
if (!$this->request->data('User.remember_me')) {
return false;
}
$data = array(
'username' => $this->request->data('User.email'),
'password' => $this->request->data('User.password')
);
$this->Cookie->write('User', $data, true, '1 week');
return true;
}
public function logout()
{
$this->Cookie->delete('User');
$this->redirect($this->Auth->logout());
}
Looks like you're already using the SecurityComponent if you want to secure your app use it everywhere. For AJAX forms white list only the fields you need, dont disable the component!
Put App::uses('Utility', 'Utility'); on top of the file
$mobileNo = Utility::addPlusToMobileNo($mobileNo); should happen in the model beforeSave()
If this is supposed to be used world wide I assume you want translations, this is missing the translation method call __() setFlash('Incorrect Email/Password Combination');
Most of the code CAN and should go into the model layer
Are there unit tests? If not add unit tests, specially test validation of data and false data input. You want ~85%+ Code Coverage for unit tests.
You're not following the CakePHP coding standards
There is no way to tell you more than this without being able to access the whole app code and doing a code review (I could do that). For queries, always just query the data you need, check the generated SQL queries, use DebugKit to check the query times to find slow querys and slowly rendering pages.

Auth allow not working always redirects to login

I have this in orders_controller.php
function beforeFilter() {
$this->Auth->allow('checkout', 'checkout_confirm', 'checkout_done');
parent::beforeFilter();
}
When I try to go to orders/checkout it always redirects me to users/login
Don't know where to look for solution.
I have an app_controller.php in app/
class AppController extends Controller {
var $components = array(
'Email',
'RequestHandler',
'Session',
'Cookie',
'Auth' => array(
'fields' => array(
'username' => "email",
'password' => "password"
),
'autoRedirect' => true,
'loginAction' => array('controller' => "users", 'action' => "login", 'admin' => false), // 'loginRedirect' => array('controller'
=> "users", 'action' => "check_account") // 'loginRedirect' => array('admin' => false, 'controller' => "users", 'action' => "account_home")
),
'Acl',
'Loviu'
);
var $helpers = array('Html', 'Form', 'Paginator', 'Session', 'Image', 'Javascript', 'Time', 'Text', 'Embed', 'Loviu');
var $uses = array('User', 'Shelf');
function beforeFilter() {
if (isset($this->params['admin']) && (1 == $this->params['admin'])) {
$this->testAccess("admin");
}
if($this->params['controller'] == 'pages'){
$this->Session->write('menu.active', 'inactive');
}
$this->Auth->allow('display');
if (false == $this->Session->check('Auth.User')) {
if (empty($this->data)) {
$cookie = $this->Cookie->read('Auth.User');
if (false == is_null($cookie)) {
// login user
if ($this->Auth->login($cookie)) {
// delete auth message
$this->Session->delete('Message.auth');
}
else {
// delete invalid cookie
$this->Cookie->delete('Auth.User');
}
} elseif(!$this->Session->read('loggedOut') && $this->params['action'] != 'login_fb') {
$this->__checkFBStatus();
}
}
}
$this->set('user_id', $this->User->id);
$this->set('lng', $this->Cookie->read("language") ? $this->Cookie->read("language") : 'eng');
parent::beforeFilter();
}
I would also put the $this->Auth->allow('checkout', 'checkout_confirm', 'checkout_done');line in your app_controller. In my experience, sometimes the problem is that the system gets confused about which controller this action belongs to, depending on how your code is setup.
Here is what I use in my app_controller that has been perfect, in case it helps:
function beforeFilter() {
$this->allowAccess();
}
private function allowAccess() {
// this actually searches the URL to see what controller you're accessing, and allows actions for that controller.
if(in_array($this->name, array('Pages'))) {
$this->Auth->allow(array('home','blog','index'));
}
}
This specificity has saved me so much trouble, and calling the Auth->Allow in app_controller is where it really should be. Hope this helps!
I had the same problem and solved for my project.
My cakephp version 3. While you loadcomponent just put loginaction.
class AppController extends BaseController
{
public function initialize()
{
$this->loadComponent('Flash');
$this->loadComponent('Auth', [
'loginAction' => [
'controller' => 'Admin',
'action' => 'login',
'plugin' => 'Admin'
],
'loginRedirect' => [
'controller' => 'admin',
'action' => 'dashboard'
],
'logoutRedirect' => [
'controller' => 'admin',
'action' => 'login'
]
]);
}
}
hope helps others.
#rncrtr's answer worked for me, but I had to add the parent::beforeFilter() to the allowAccess method:
public function beforeFilter() {
parent::beforeFilter();
$this->allowAccess();
}
private function allowAccess() {
if (in_array($this->name, array('Pages'))) {
$this->Auth->allow(array('home','index','display'));
}
}
Oh yeah, I also had to add display to the allow array.
if you work on cakephp 2.x you must do like this :
function beforeFilter(){
$this->Auth->allow(array('action you want to allow1','action you want to allow2'));
}
allow(array()) instead allow()
---put that code into controller have action you want allow access without login
if you use $this->Auth->allow() you must call parent::beforeFilter(); in function beforeFilter() like this :
function beforeFilter(){
parent::beforeFilter();
$this->Auth->allow('add','view');
}

Resources