Now that our CMS has users, we can enable them to login using the cakephp/authentication plugin. We’ll start off by ensuring passwords are stored securely in our database. Then we are going to provide a working login and logout, and enable new users to register.
Use composer to install the Authentication Plugin:
composer require "cakephp/authentication:^2.4"
You need to have created the Controller
, Table
, Entity
and
templates for the users
table in your database. You can do this manually
like you did before for the ArticlesController, or you can use the bake shell
to generate the classes for you using:
bin/cake bake all users
If you create or update a user with this setup, you might notice that the passwords are stored in plain text. This is really bad from a security point of view, so lets fix that.
This is also a good time to talk about the model layer in CakePHP. In CakePHP,
we use different classes to operate on collections of records and single records.
Methods that operate on the collection of entities are put in the Table
class,
while features belonging to a single record are put on the Entity
class.
For example, password hashing is done on the individual record, so we’ll implement this behavior on the entity object. Because we want to hash the password each time it is set, we’ll use a mutator/setter method. CakePHP will call a convention based setter method any time a property is set in one of your entities. Let’s add a setter for the password. In src/Model/Entity/User.php add the following:
<?php
namespace App\Model\Entity;
use Authentication\PasswordHasher\DefaultPasswordHasher; // Add this line
use Cake\ORM\Entity;
class User extends Entity
{
// Code from bake.
// Add this method
protected function _setPassword(string $password) : ?string
{
if (strlen($password) > 0) {
return (new DefaultPasswordHasher())->hash($password);
}
}
}
Now, point your browser to http://localhost:8765/users to see a list of users.
Remember you’ll need to have your local server running. Start a standalone PHP
server using bin/cake server
.
You can edit the default user that was created during Installation. If you change that user’s password, you should see a hashed password instead of the original value on the list or view pages. CakePHP hashes passwords with bcrypt by default. We recommend bcrypt for all new applications to keep your security standards high. This is the recommended password hash algorithm for PHP.
Note
Create a hashed password for at least one of the user accounts now! It will be needed in the next steps. After updating the password, you’ll see a long string stored in the password column. Note bcrypt will generate a different hash even for the same password saved twice.
Now it’s time to configure the Authentication Plugin. The 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.
If you remember, we used AuthComponent before to handle all these steps. Now the logic is divided into specific classes and the authentication process happens before your controller layer. First it checks if the user is authenticated (based on the configuration you provided) and injects the user and the authentication results into the request for further reference.
In src/Application.php, add the following imports:
// In src/Application.php add the following imports
use Authentication\AuthenticationService;
use Authentication\AuthenticationServiceInterface;
use Authentication\AuthenticationServiceProviderInterface;
use Authentication\Middleware\AuthenticationMiddleware;
use Cake\Routing\Router;
use Psr\Http\Message\ServerRequestInterface;
Then implement the authentication interface on your Application
class:
// in src/Application.php
class Application extends BaseApplication
implements AuthenticationServiceProviderInterface
{
Then add the following:
// src/Application.php
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
$middlewareQueue
// ... other middleware added before
->add(new RoutingMiddleware($this))
->add(new BodyParserMiddleware())
// Add the AuthenticationMiddleware. It should be after routing and body parser.
->add(new AuthenticationMiddleware($this));
return $middlewareQueue;
}
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'),
]);
return $authenticationService;
}
In your AppController
class add the following code:
// src/Controller/AppController.php
public function initialize(): void
{
parent::initialize();
$this->loadComponent('Flash');
// Add this line to check authentication result and lock your site
$this->loadComponent('Authentication.Authentication');
Now, on every request, the AuthenticationMiddleware
will inspect
the request session to look for an authenticated user. If we are loading the /users/login
page, it will also inspect the posted form data (if any) to extract the credentials.
By default the credentials will be extracted from the username
and password
fields in the request data.
The authentication result will be injected in a request attribute named
authentication
. You can inspect the result at any time using
$this->request->getAttribute('authentication')
from your controller actions.
All your pages will be restricted as the AuthenticationComponent
is checking the
result on every request. When it fails to find any authenticated user, it will redirect the
user to the /users/login
page.
Note at this point, the site won’t work as we don’t have a login page yet.
If you visit your site, you’ll get an “infinite redirect loop” so let’s fix that.
Note
If your application serves from both SSL and non-SSL protocols, then you might have problems with sessions being lost, in case your application is on non-SSL protocol. You need to enable access by setting session.cookie_secure to false in your config config/app.php or config/app_local.php. (See CakePHP’s defaults on session.cookie_secure)
In your UsersController
, add the following code:
public function beforeFilter(\Cake\Event\EventInterface $event)
{
parent::beforeFilter($event);
// Configure the login action to not require authentication, preventing
// the infinite redirect loop issue
$this->Authentication->addUnauthenticatedActions(['login']);
}
public function login()
{
$this->request->allowMethod(['get', 'post']);
$result = $this->Authentication->getResult();
// 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' => 'Articles',
'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'));
}
}
Add the template logic for your login action:
<!-- in /templates/Users/login.php -->
<div class="users form">
<?= $this->Flash->render() ?>
<h3>Login</h3>
<?= $this->Form->create() ?>
<fieldset>
<legend><?= __('Please enter your username and password') ?></legend>
<?= $this->Form->control('email', ['required' => true]) ?>
<?= $this->Form->control('password', ['required' => true]) ?>
</fieldset>
<?= $this->Form->submit(__('Login')); ?>
<?= $this->Form->end() ?>
<?= $this->Html->link("Add User", ['action' => 'add']) ?>
</div>
Now login page will allow us to correctly login into the application.
Test it by requesting any page of your site. After being redirected
to the /users/login
page, enter the email and password you
picked previously when creating your user. You should be redirected
successfully after login.
We need to add a couple more details to configure our application.
We want all view
and index
pages accessible without logging in so we’ll add this specific
configuration in AppController:
// in src/Controller/AppController.php
public function beforeFilter(\Cake\Event\EventInterface $event)
{
parent::beforeFilter($event);
// for all controllers in our application, make index and view
// actions public, skipping the authentication check
$this->Authentication->addUnauthenticatedActions(['index', 'view']);
}
Note
If you don’t have a user with a hashed password yet, comment the
$this->loadComponent('Authentication.Authentication')
line in your
AppController and all other lines where Authentication is used. Then go to
/users/add
to create a new user picking email and password. Afterward,
make sure to uncomment the lines we just temporarily commented!
Try it out by visiting /articles/add
before logging in! Since this action is not
allowed, you will be redirected to the login page. After logging in
successfully, CakePHP will automatically redirect you back to /articles/add
.
Add the logout action to the UsersController
class:
// in src/Controller/UsersController.php
public function logout()
{
$result = $this->Authentication->getResult();
// regardless of POST or GET, redirect if user is logged in
if ($result && $result->isValid()) {
$this->Authentication->logout();
return $this->redirect(['controller' => 'Users', 'action' => 'login']);
}
}
Now you can visit /users/logout
to log out. You should then be sent to the login
page.
If you try to visit /users/add without being logged in, you will be
redirected to the login page. We should fix that as we want to allow people to
sign up for our application. In the UsersController
fix the following line:
// Add to the beforeFilter method of UsersController
$this->Authentication->addUnauthenticatedActions(['login', 'add']);
The above tells AuthenticationComponent
that the add()
action of the
UsersController
does not require authentication or authorization. You may
want to take the time to clean up the Users/add.php and remove the
misleading links, or continue on to the next section. We won’t be building out
user editing, viewing or listing in this tutorial, but that is an exercise you
can complete on your own.
Now that users can log in, we’ll want to limit users to only edit articles that they created by applying authorization policies.