CakePHP 5 custom authentication, PasswordHasher

Hey,

I installed CakePHP 5:
composer create-project --prefer-dist cakephp/app:~5.0 app

I installed Authentication:
composer require "cakephp/authentication:^3.0"

In app_local.php config the MySQL database connect successfully.

Added this:

CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  user_login VARCHAR(255) NOT NULL,
  user_pass VARCHAR(255) NOT NULL,
  created DATETIME,
  modified DATETIME
);

I use user_login (not email) and user_pass (not password) fields.

Run: bin/cake bake all users

I changed the files email->user_login and password->user_pass.

I read and do: CMS Tutorial - Authentication - 5.x

I read more articles and forums from internet, but the auth works with CakePHP 2, 3, 4, but does not works with 5.

Please help me how to.

My file:

<?php
class PhpassPasswordHasher extends AbstractPasswordHasher {
  public function hash($password) {
    return 'xyz';
  }

  public function check($password, $hashedPassword) {
    return true;
  }
}

Please help me.

1. How to set password?

src/Model/Entity/User.php

use Authentication\PasswordHasher\DefaultPasswordHasher; // how to use my file instead this?
class User extends Entity
{
  protected function _setUserPass(string $password) : ?string
  {
    if (strlen($password) > 0) {
      return (new DefaultPasswordHasher())->hash($password); // how to use my pw hasher?
    }
    return null;
  }
}

2. How to use my class for login?

3. Where do I save my file?

4. In my file, what need use/namespace etc?

Thanks.

In CakePHP 2 (PHP 5 and 7) I can create own PhpassPasswordHasher. This PHP 8.3 and CakePHP 5 it’s complicated for me.

did you follow the steps in CMS tutorial?

Dear Rods,

yes, but the CMS tutorial use only DefaultPasswordHasher, but I need custom/own password hasher and the tutorial this is not included. I need help. I read articles/forums for custom/own password hasher, but these works only CakePHP 2, 3 and 4, with CakePHP 5 does not works.

What does “does not work” mean? You get an error? Fail to authenticate? Lets you log in but just for one page and then asks for a login again?

Dear Zuluru,

It works with DefaultPasswordHasher: CMS Tutorial - Authentication - 5.x

… but I would like use custom/own password hasher. I tried for several hours yesterday, but it didn’t work at all. Can anyone help?

It would be urgent. If I can’t solve it today, I’ll have to switch to CakePHP 2 or other framework.

This help would be useful for others, not just me. Yesterday I read that no one else has succeeded either with CakePHP 5.

I read GitHub repositories too, issues, forums, articles…

The CakePHP version 5 has changed too much in my opinion. I think it has to be configured differently for this version and the files must be somewhere else or I don’t know, I don’t understand. This CakePHP 5 is too difficult first look. CakePHP 2, easy, CakePHP 5, hard (for me).

I would try something like this:

create: src/Authentication/PasswordHasher/PhpassPasswordHasher.php

<?php
namespace App\Authentication\PasswordHasher;

use Authentication\PasswordHasher\AbstractPasswordHasher;

class PhpassPasswordHasher extends AbstractPasswordHasher
{
    public function hash($password)
    {
        // your code
        return 'xyz';
    }

    public function check($password, $hashedPassword)
    {
        // your code
        return true;
    }
}

@ User Entity:

namespace App\Model\Entity;

use Cake\ORM\Entity;
use App\Authentication\PasswordHasher\PhpassPasswordHasher;

class User extends Entity
{
    protected function _setUserPass(string $password) : ?string
    {
        if (strlen($password) > 0) {
            return (new PhpassPasswordHasher())->hash($password);
        }
        return null;
    }
}

@ src/Application.php

public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
{
    $service = new AuthenticationService();

    // Load identifiers
    $service->loadIdentifier('Authentication.Password', [
        'fields' => [
            'username' => 'user_login',
            'password' => 'user_pass'
        ]
    ]);

    // Load the authenticators
    $service->loadAuthenticator('Authentication.Session');
    $service->loadAuthenticator('Authentication.Form', [
        'fields' => [
            'username' => 'user_login',
            'password' => 'user_pass'
        ],
        'loginUrl' => '/users/login'
    ]);

    // Using the custom password hasher
    $service->setConfig([
        'passwordHasher' => [
            'className' => PhpassPasswordHasher::class
        ]
    ]);

    return $service;
}

PS, I haven’t tested the code.

2 Likes

Dear Salines,

Thank you very much, it works. You are my hero! :slight_smile:

Who come after me (CakePHP 5 and PHP 8.3):

<?php
namespace App\Authentication\PasswordHasher;
use Authentication\PasswordHasher\AbstractPasswordHasher;

class PhpassPasswordHasher extends AbstractPasswordHasher {

  public function hash(string $password): string
  {
    return 'xyz';
  }

  public function check(string $password, string $hashedPassword): bool
  {
    return true;
  }

}
1 Like

How to import 1 PHP class file in my src/Authentication/PasswordHasher/PhpassPasswordHasher.php file?

My file what I would like import: vendor/phpass/phpass.php

This phpass.php file:

<?php
class Phpass {
  public function hash(string $password): string
  {
    return 'xyz';
  }
  public function check(string $password, string $hashedPassword): bool
  {
    return true;
  }
}

The PhpassPasswordHasher.php file, where I would like use the Phpass class:

...
public function hash(string $password): string
{
  return Phpass::hash($password);
}
...

I assume you used Composer to install the PHP package. In that case, including other PHP classes is done via the autoloader. I recommend using a good code editor with advanced features such as code suggestions while typing. For example, VSCode with the PHP Intelephense extension (including a one-time paid license) or PhpStorm.

From your example, including “Phpass” might look like this, considering it has no static methods:

<?php
declare(strict_types=1);

namespace App\Authentication\PasswordHasher;

use Authentication\PasswordHasher\AbstractPasswordHasher;
use Phpass; // <--------------------- include

class PhpassPasswordHasher extends AbstractPasswordHasher {
{
    public function hash(string $password): string
    {
        $phpass = new Phpass(); // <---------- implementation
        return $phpass->hash($password);
    }

    public function check(string $password, string $hashedPassword): bool
    {
        $phpass = new Phpass();
        return $phpass->check($password, $hashedPassword);
    }
}

Thanks, but doesn’t works. Error: Class “Phpass” not found

I would like know how to use it.

With Composer, it works, but I would like know how to use it without Composer.

Solution:

cd app
composer require "bordoni/phpass"

The file:

<?php
namespace App\Authentication\PasswordHasher;

use Authentication\PasswordHasher\AbstractPasswordHasher;
use Hautelook\Phpass\PasswordHash;

class PhpassPasswordHasher extends AbstractPasswordHasher {

  public function hash(string $password): string
  {
    $hasher = new PasswordHash( 8, true );
    return $hasher->HashPassword($password);
  }

  public function check(string $password, string $hashedPassword): bool
  {
    $hasher = new PasswordHash( 8, true );
    return $hasher->CheckPassword($password, $hashedPassword);
  }

}

The WordPress use this Phpass, that’s why I need it for my users table. It was a difficult birth. I have to study a lot. This PHP 8.3 with composer, “use”, “namespace” etc…

You can include non-composer files using the composer.json “files” property. Actually I think I learnt this from @ishan in the asset-mix repo


<?php

// vendor/phpass/phpass.php
// example of non-namespaced non-composer class

class Phpass
{
    public function hash($text = "Hi")
    {
        return md5($text)
    }
}

Edit autoload to include the file.

{ 
//... reset of composer
    "autoload": {
        "files": [
            "vendor/phpass/phpass.php"
        ],
        "psr-4": {
            "App\\": "src/"
        }
    }
}
# remember to do this or it won't work
composer dumpautoload

If it’s a non-composer PHP file then you won’t need a use Phpass; statement

You can then use the class in CakePHP e.g:

  <p><?= (new Phpass)->hash('This is a test'); ?></p>
<p>ce114e4501d2f4e2dcea3e17b546f339</p>
1 Like

It doesn’t work on a fresh install. :smiling_face_with_tear:

First install, it works:

  1. Create table (email and password):
CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  email VARCHAR(255) NOT NULL,
  password VARCHAR(255) NOT NULL,
  created DATETIME,
  modified DATETIME
);
  1. Do this: CMS Tutorial - Authentication - 5.x
  2. Not do this: CMS Tutorial - Authorization - 5.x
  3. Change the email → user_login and password → user_pass
  4. Do this forum codes and it works.

Second install, it doesn’t work:

  1. Create table (user_login and user_pass):
CREATE TABLE users (
  id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  user_login varchar(60) not null,
  user_pass varchar(64) not null,
  user_email varchar(100) not null,
  user_registered timestamp default current_timestamp,
  user_activation_key varchar(60) not null,
  user_admin tinyint(1) not null default 0,
  user_status int(11) not null default 0
);
  1. Do this: CMS Tutorial - Authentication - 5.x
  2. Do this: CMS Tutorial - Authorization - 5.x
  3. Change the username → user_login and password → user_pass
  4. Do this forum codes and it doesn’t work.

Problem:

What is the problem, what do you think? User add is okay, but the login is not okay: “Invalid username or password”.

public function login()
  {
    $this->Authorization->skipAuthorization();
    $this->request->allowMethod(['get', 'post']);
    $result = $this->Authentication->getResult();
    var_dump($result);
    // regardless of POST or GET, redirect if user is logged in
    if ($result && $result->isValid()) {
      // redirect to /articles after login success
      $redirect = $this->request->getQuery('redirect', [
        'controller' => 'Users',
        'action' => 'index',
      ]);

      return $this->redirect($redirect);
    }
    // display error if user submitted and authentication failed
    if ($this->request->is('post') && !$result->isValid()) {
      $this->Flash->error(__('Invalid username or password'));
    }
}

The var_dump:

object(Authentication\Authenticator\Result)#203 (3) { ["_status":protected]=> string(26) "FAILURE_IDENTITY_NOT_FOUND" ["_data":protected]=> NULL ["_errors":protected]=> array(1) { ["Password"]=> array(0) { } } }

I no longer have a working first installation. :sob:

Thanks! :pray:

Mission impossible. There are two options:

  1. Login never worked.
  2. It worked, but I have no idea how.

What I do:

  1. Create users table: CMS Tutorial - Creating the Database - 5.x
  2. Do this: CMS Tutorial - Authentication - 5.x
  3. Do this: CakePHP 5 custom authentication, PasswordHasher - #6 by Salines
  4. Do this: CakePHP 5 custom authentication, PasswordHasher - #11 by only4r

Adding user works but login why not?

Is it possible that the problem is caused by the fact that new versions have been released?

Or could i have used these versions? I don’t know. Please help me.

post_data


:sneezing_face:

please show your getAuthenticationService() in your src/Application.php

also if you have no idea what namespaces and use statements are, I would recommend watching this:

So instead of relying on composer to autoload your wordpress PHP file you can also just manually require it. It’s just industry standard that you use composer to manage PHP dependencies.

Wordpress is just years behind to follow that standard.

1 Like

This is the file:

<?php
declare(strict_types=1);

/**
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @link      https://cakephp.org CakePHP(tm) Project
 * @since     3.3.0
 * @license   https://opensource.org/licenses/mit-license.php MIT License
 */
namespace App;

use Cake\Core\Configure;
use Cake\Core\ContainerInterface;
use Cake\Datasource\FactoryLocator;
use Cake\Error\Middleware\ErrorHandlerMiddleware;
use Cake\Http\BaseApplication;
use Cake\Http\Middleware\BodyParserMiddleware;
use Cake\Http\Middleware\CsrfProtectionMiddleware;
use Cake\Http\MiddlewareQueue;
use Cake\ORM\Locator\TableLocator;
use Cake\Routing\Middleware\AssetMiddleware;
use Cake\Routing\Middleware\RoutingMiddleware;
use Authentication\AuthenticationService;
use Authentication\AuthenticationServiceInterface;
use Authentication\AuthenticationServiceProviderInterface;
use Authentication\Middleware\AuthenticationMiddleware;
use Cake\Routing\Router;
use Psr\Http\Message\ServerRequestInterface;

/**
 * Application setup class.
 *
 * This defines the bootstrapping logic and middleware layers you
 * want to use in your application.
 *
 * @extends \Cake\Http\BaseApplication<\App\Application>
 */
class Application extends BaseApplication implements AuthenticationServiceProviderInterface
{
  /**
   * Load all the application configuration and bootstrap logic.
   *
   * @return void
   */
  public function bootstrap(): void
  {
    // Call parent to load bootstrap from files.
    parent::bootstrap();

    if (PHP_SAPI !== 'cli') {
      FactoryLocator::add(
        'Table',
        (new TableLocator())->allowFallbackClass(false)
      );
    }
  }

  /**
   * Setup the middleware queue your application will use.
   *
   * @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to setup.
   * @return \Cake\Http\MiddlewareQueue The updated middleware queue.
   */
  public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
  {
    $middlewareQueue
      // Catch any exceptions in the lower layers,
      // and make an error page/response
      ->add(new ErrorHandlerMiddleware(Configure::read('Error'), $this))

      // Handle plugin/theme assets like CakePHP normally does.
      ->add(new AssetMiddleware([
        'cacheTime' => Configure::read('Asset.cacheTime'),
      ]))

      // Add routing middleware.
      // If you have a large number of routes connected, turning on routes
      // caching in production could improve performance.
      // See https://github.com/CakeDC/cakephp-cached-routing
      ->add(new RoutingMiddleware($this))

      // Parse various types of encoded request bodies so that they are
      // available as array through $request->getData()
      // https://book.cakephp.org/5/en/controllers/middleware.html#body-parser-middleware
      ->add(new BodyParserMiddleware())

      // Cross Site Request Forgery (CSRF) Protection Middleware
      // https://book.cakephp.org/5/en/security/csrf.html#cross-site-request-forgery-csrf-middleware
      ->add(new CsrfProtectionMiddleware([
        'httponly' => true,
      ]))

      ->add(new AuthenticationMiddleware($this));

    return $middlewareQueue;
  }

  /**
   * Register application container services.
   *
   * @param \Cake\Core\ContainerInterface $container The Container to update.
   * @return void
   * @link https://book.cakephp.org/5/en/development/dependency-injection.html#dependency-injection
   */
  public function services(ContainerInterface $container): void
  {
  }

  public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
  {
    $authenticationService = new AuthenticationService([
      'unauthenticatedRedirect' => Router::url('/users/login'),
      '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' => Router::url('/users/login'),
    ]);

    $authenticationService->setConfig([
      'passwordHasher' => [
        'className' => PhpassPasswordHasher::class
      ]
    ]);

    return $authenticationService;
  }
}

Thanks! :pray:

Adjust your password identifier to specify the password hasher like so.

    $authenticationService->loadIdentifier('Authentication.Password', [
      'fields' => [
        'username' => 'email',
        'password' => 'password',
      ],
      'passwordHasher' => 'Phpass'
    ]);

This requires your PhpassPasswordHasher to be located in src/PasswordHasher/PhpassPasswordHasher so that cake can find it.

And it of course needs to have the correct namespace, so

<?php
declare(strict_types=1);

namespace App\PasswordHasher;

use Authentication\PasswordHasher\AbstractPasswordHasher;

class PhpassPasswordHasher extends AbstractPasswordHasher {

    public function hash(string $password): string
    {
        return 'xyz';
    }

    public function check(string $password, string $hashedPassword): bool
    {
        return true;
    }

}

then this password hasher is at least used with the password identifier

1 Like