CakePHP is a popular PHP Framework. It is essentially an open-source web framework developed using PHP. In CakePHP4, the login function is strongly supported through the Authentication 2.0 plugin. The content of this article will show you how to create a login function using the web interface.
1. Create migration, table, entity, and controller classes for users.
Before implementing the steps below, you need to create a CakePHP web application. You can refer to how to create it at: https://book.cakephp.org/4/en/installation.html
- Create migration.
We need to create a Users table to store user information such as email, password, etc.
In CakePHP, we can create tables through the Migrations plugin. Details on instructions for creating tables can be found at: https://book.cakephp.org/migrations/4/en/index.html.
Run the command below to create the migration file:
bin/cake migrations create CreateUsers
The migration file will be created in the folder config/Migrations/yyyymmddxxxxxx_CreateUsers.php.
Source code to create Users table.
<?php
use Migrations\AbstractMigration;
class CreateUsers extends AbstractMigration {
public function change()
{
$table = $this->table('users');
$table->addColumn('email', 'string', ['limit' => 255])
->addColumn('password', 'string', ['limit' => 255])
->addColumn('created_at', 'datetime')
->addColumn('updated_at', 'datetime')
->create();
}
}
Execute the migrations command below to create the table for database.
bin/cake migrations migrate
- Create table, entity, and controller classes.
We use bake shell to help generate Table, Entity, Controller classes.
bin/cake bake all users
The classes are generated and saved one by one according to the paths below:
Table: src/Model/Table/UsersTable.php
Entity: src/Model/Entity/User.php
Controller: src/Controller/UsersController.php
2, Install Authentication Plugin.
Use composer to install the Authentication plugin:
composer require "cakephp/authentication:^2.4"
To compare the password from the user and the password value saved from the database, we need to hash the password from the user input and compare it.
We need to create a mutator/setter method on the User entity.
In the file src\Model\Entity\User.php we add the _setPassword() function as below:
<?php
declare(strict_types=1);
namespace App\Model\Entity;
use Authentication\PasswordHasher\DefaultPasswordHasher;
use Cake\ORM\Entity;
class User extends Entity
{
protected function _setPassword($password)
{
return (new DefaultPasswordHasher)->hash($password);
}
}
CakePHP hashes passwords with bcrypt by default. CakePHP recommend bcrypt for all new applications to keep your security standards high. This is the recommended password hash algorithm for PHP.
Bcrypt: https://codahale.com/how-to-safely-store-a-password/
3, Create a login function using the web interface.
To understand how the login function works, you can see the diagram below:
The Authentication plugin will handle the authentication process using 3 different classes:
Application will use the Authentication Middleware and provide an AuthenticationService, holding all the configuration we want to define how are we going to check the credentials, and where to find them.
AuthenticationService will be a utility class to allow you configure the authentication process.
AuthenticationMiddleware will be executed as part of the middleware queue, this is before your Controllers are processed by the framework, and will pick the credentials and process them to check if the user is authenticated.
- Add Authentication plugin.
protected function bootstrapCli(): void
{
$this->addOptionalPlugin('Bake');
$this->addPlugin('Migrations');
$this->addPlugin('Authentication');
}
- Implement AuthenticationServiceProviderInterface.
public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
{
$service = new AuthenticationService([
'unauthenticatedRedirect' => Router::url('/login'),
'queryParam' => 'redirect',
]);
$fields = [
IdentifierInterface::CREDENTIAL_USERNAME => 'email',
IdentifierInterface::CREDENTIAL_PASSWORD => 'password'
];
$service->loadIdentifier('Authentication.Password', compact('fields'));
$service->loadAuthenticator('Authentication.Session');
$service->loadAuthenticator('Authentication.Form', [
'fields' => $fields,
'loginUrl' => [
Router::url([
'prefix' => false,
'plugin' => null,
'controller' => 'Users',
'action' => 'login',
]),
Router::url([
'prefix' => false,
'plugin' => null,
'controller' => 'Users',
'action' => 'webLogin',
])
],
]);
return $service;
}
The password identifier checks the passed credentials against a datasource.
Configuration options:
fields: The fields for the lookup. Default is ['username' => 'username', 'password' => 'password']. You can also set the username to an array. For e.g. using ['username' => ['username', 'email'], 'password' => 'password'] will allow you to match value of either username or email columns.
resolver: The identity resolver. Default is Authentication.Orm which uses CakePHP ORM.
passwordHasher: Password hasher. Default is DefaultPasswordHasher::class.
- Add AuthenticationMiddleware.
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(new RoutingMiddleware($this))
->add(new BodyParserMiddleware())
->add(new AuthenticationMiddleware($this));
return $middlewareQueue;
}
Load Authentication component.
public function initialize(): void
{
parent::initialize();
$this->loadComponent('RequestHandler');
$this->loadComponent('Flash');
$this->loadComponent('Authentication.Authentication');
}
- Create the form file LoginForm.php in the directory src/Form.
<?php
namespace App\Form;
use Cake\Form\Form;
use Cake\Form\Schema;
use Cake\Validation\Validator;
class LoginForm extends Form
{
protected function _buildSchema(Schema $schema): Schema
{
return $schema->addFields([
'email' => [
'type' => 'string',
'length' => 255
],
'password' => [
'type' => 'string',
'length' => 255
]
]);
}
public function validationDefault(Validator $validator): Validator
{
return $validator
->notEmptyString('email')
->email('email')
->maxLength('email', 50)
->notEmptyString('password')
->maxLength('password', 50);
}
}
- In src/Controller/UsersController.php.
- Create webLogin action to support login via Web interface.
public function webLogin()
{
$this->request->allowMethod(['get', 'post']);
$loginForm = new LoginForm();
$result = $this->Authentication->getResult();
if ($result && $result->isValid()) {
$this->redirect("/home");
}
if ($this->request->is('post')) {
$data = $this->request->getData();
if (!$loginForm->validate($data)) {
$this->Flash->error(__('Invalid username or password'));
}
if ($result && $result->isValid()) {
$this->redirect("/home");
} else {
$this->Flash->error(__('Invalid username or password'));
}
}
$this->set(compact('loginForm'));
}
- Override the berforeFilter function to bypass authentication for the webLogin action.
public function beforeFilter(EventInterface $event)
{
parent::beforeFilter($event);
$this->Authentication->allowUnauthenticated(['webLogin']);
}
- Create logout action to support logout function.
public function logout()
{
$this->Authentication->logout();
return $this->redirect(["action" => 'webLogin']);
}
- Create the view file web_login.php in the directory templates/Users.
<?= $this->Flash->render() ?>
<?php $this->Form->setConfig('autoSetCustomValidity', false); ?>
<?= $this->Form->create($loginForm, ['novalidate' => true]) ?>
<?= $this->Form->control('email'); ?>
<?= $this->Form->control('password'); ?>
<?= $this->Form->button('Login') ?>
<?= $this->Form->end() ?>
- Update the login URL.
We will overwrite the login page url to /login instead of using the url /users/web-login.
In config\routes.php.
$routes->scope('/', function (RouteBuilder $builder): void {
$builder->connect('/login', ['controller' => 'Users', 'action' => 'webLogin']);
$builder->fallbacks();
}
Access the /login to check login.
Successful login will be redirected to the Home page.
You can find the complete source code at: https://github.com/ivc-phampbt/cakephp-authentication/tree/web_login
Conclusion
In addition to supporting authentication using user_name and password, the authentication plugin also supports authentication via JWT Token. JWT token support makes authenticating Web APIs called from front-end frameworks (ex: VueJS, reactjs...) faster and more secure. In the future, I will have an article introducing authentication via JWT Token.
References