CakePHP 4.x User Authentication Tutorial

6 October 2020

This tutorial describes how you can set up a simple user authentication for CakePHP using the officially supported authentication plugin.

Authentication in web applications deals with the identity of a user, i.e. is the user who he claims to be. Authentication usually is realized via username/password, sessions/cookies or JWT/OAuth. Whether a certain user is allowed to access a certain resource (e.g. website) is part of authorization and is described in a further article.

As boilerplate Code we can use the CakePHP Quickstart Tutorial. You can find the complete source code on github.

Installation

You can install this plugin into your CakePHP application using composer in your application root:

composer require cakephp/authentication:^2.0

Load the plugin:

// src/Application.php
public function bootstrap(): void
{
    // Call parent to load bootstrap from files.
    parent::bootstrap();

    ...

    // Load more plugins here
    $this->addPlugin('Authentication');
}

Middleware

The authentication plug-in is integrated using a PSR-7 middleware; This checks the identity of the user with each request using an authenticator (e.g. form authenticator with username/password). In order to be able to use the middleware, our application must implement the AuthenticationProviderInterface and add the middleware in the middleware() method:

// src/Application.php
use Authentication\AuthenticationServiceInterface;
use Authentication\AuthenticationServiceProviderInterface;
use Authentication\Middleware\AuthenticationMiddleware;
use Cake\Core\Configure;
use Cake\Core\Exception\MissingPluginException;
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\Routing\Middleware\AssetMiddleware;
use Cake\Routing\Middleware\RoutingMiddleware;
use Psr\Http\Message\ServerRequestInterface;

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();

        ...

        // Load more plugins here
        $this->addPlugin('Authentication');
    }

    /**
     * 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
            // load other Middleware

            ...

            // Authentication Middleware
            ->add(new AuthenticationMiddleware($this));


        return $middlewareQueue;
    }

    public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
    {
        // TODO: Implement getAuthenticationService() method.
    }
}

If you want to find out more about authentication middleware and CakePHP middleware.

Next we implement our getAuthenticationService() method:

// src/Application.php

use Authentication\AuthenticationService;
use Authentication\Identifier\IdentifierInterface;
...

/**
 * Returns a service provider instance.
 *
 * @param \Psr\Http\Message\ServerRequestInterface $request Request
 * @return \Authentication\AuthenticationServiceInterface
 */
 public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
 {
     // Create AuthenticationService
     $service = new AuthenticationService();

     // Define where users should be redirected to when they are not authenticated
     $service->setConfig([
         'unauthenticatedRedirect' => Router::url(['controller' => 'Users', 'action' => 'login']),
         'queryParam' => 'redirect',
     ]);

     // Fields in your db to match against
     $fields = [
         IdentifierInterface::CREDENTIAL_USERNAME => 'email',
         IdentifierInterface::CREDENTIAL_PASSWORD => 'password'
     ];

     // Load the authenticators. Session should be first.
     $service->loadAuthenticator('Authentication.Session');
     $service->loadAuthenticator('Authentication.Form', [
         'fields' => $fields,
         'loginUrl' => Router::url(['controller' => 'Users', 'action' => 'login'])
     ]);

     // Load identifiers
     $service->loadIdentifier('Authentication.Password', compact('fields'));

     return $service;
}

Create AuthenticationService

The AuthenticationService is created and configured here. The unauthenticatedRedirect option specifies where unauthenticated users should be redirected. You’ll find further configuration options under configuration.

Add Authenticator

Next we add a Authenticator with loadAuthenticator(). This example uses form-based authentication (form) and PHP sessions. Sessions should always be loaded first. The form fields are configured in the fields option.

Add Identifier

Finally, the identifier gets loaded with loadIdentifier(). If your data model should look different, you can use the resolver option here to set a different one. The passwordHasher option can be used to replace older password hashes with new ones. More on this at https://book.cakephp.org/authentication/2/en/identifiers.html#password

Add the Authentication Controller Component

In our AppController we then load the Authentication Component:

// src/Controller/AppController.php

public function initialize(): void
{
    parent::initialize();

    $this->loadComponent('RequestHandler');
    $this->loadComponent('Flash');
    $this->loadComponent('Authentication.Authentication');
}

Implement User Login Action

In order for the user to be able to log in, access to the login() action must be possible to unauthenticated users. For this purpose, access is allowed with ʻallowUnauthenticated in the beforeFilter()` Callback of the UsersController. The login action uses the Authentication Component and receives the result of the login attempt. If the login attempt was successful, you can forward the user to a specific page.

// src/Controller/UsersController.php

public function beforeFilter(EventInterface $event)
{
    $this->Authentication->allowUnauthenticated(['login']);

    parent::beforeFilter($event);
}

public function login()
{
    $result = $this->Authentication->getResult();

    // If the user is logged in send them away.
    if ($result->isValid()) {
        $target = $this->Authentication->getLoginRedirect() ?? '/';

        return $this->redirect($target);
    }

    if ($this->request->is('post') && !$result->isValid()) {
        $this->Flash->error('Invalid username or password');
    }
}

Next we need a login form for our login() action. We use the FormHelper for this:

// in templates/Users/login.php

<?= $this->Form->create() ?>
<?= $this->Form->control('email') ?>
<?= $this->Form->control('password') ?>
<?= $this->Form->button(__('Login')) ?>
<?= $this->Form->end() ?>

Implement User Registration Action

However, there is currently no user in our database with a corresponding Password hash. To do this, we change the setter method for our password in the user entity so that it saves the password as a hash:

// in src/Model/Entity/User.php

protected function _setPassword(string $plainPassword)
{
    $hasher = new DefaultPasswordHasher();

    return $hasher->hash($plainPassword);
}

In order to register new users we create a register() action. This action creates a new entity and the password entered is saved as a hash in the database.

// in src/Controller/UsersController.php

public function register()
{
    $user = $this->Users->newEmptyEntity();

    if ($this->request->is('post')) {
        $user = $this->Users->patchEntity($user, $this->request->getData());

        if ($this->Users->save($user)) {
            $this->Flash->success(__('Your registration was successful.'));

            return $this->redirect('/');
        }

        $this->Flash->error(__('Your registration failed.'));
    }
}

For the corresponding form, the content of the templates/Users/login.php can simply be copied into the template of the register action in templates/Users/register.php.

Via the corresponding URL (in our case http://localhost/users/register) a new user can now be created. You should see this user with the hashed password under http://localhost/users/.

You can find the complete source code on github.