Authentication

Authentication verifies the identity of a user or service, and Authorization determines their access rights. First we need to identify a user, so we need the user to log in.

Add Authentication

We've already installed the Authentication plugin back in Application Setup. We've already added password hashing in Database and Model. And we should have added a user at the end of Controller and Views. Now 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 interface on your Application class:

class Application extends BaseApplication
   implements AuthenticationServiceProviderInterface
{

Now update the middleware() function and add the getAuthenticationService() function.

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))
      ->add(new CsrfProtectionMiddleware([
        'httponly' => true,
      ]));
    return $middlewareQueue;
  }
  
  public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
  {
    $authenticationService = new AuthenticationService([
      'unauthenticatedRedirect' => Router::url('/users/login'),
      'queryParam' => 'redirect',
    ]);
    // Load identifiers, ensure we check username and password fields
    $authenticationService->loadIdentifier('Authentication.Password', [
      'fields' => [
        'username' => 'username',
        'password' => 'password',
      ]
    ]);
    // Load the authenticators, you want session first
    $authenticationService->loadAuthenticator('Authentication.Session');
    // Configure form data check to pick username and password
    $authenticationService->loadAuthenticator('Authentication.Form', [
      'fields' => [
        'username' => 'username',
        'password' => 'password',
      ],
      'loginUrl' => Router::url('/users/login'),
    ]);
    return $authenticationService;
  }

Now in /src/Controller/AppController.php, which is the parent class of all our controllers, load the Authentication component and lock down the site. Now's a good time to uncomment the Form Protection component also.

public function initialize(): void
{
  ...
  $this->loadComponent('Flash');
  // Add this line to check authentication result and lock your site
  $this->loadComponent('Authentication.Authentication');
  /*
   * Enable the following component for recommended CakePHP form protection settings.
   * see https://book.cakephp.org/4/en/controllers/components/form-protection.html
   */
   $this->loadComponent('FormProtection');
}

We can also make global changes here, so we'll be allowing all users, and future object records, to be listed (index) and viewed anonymously, as well as allowing the display() action for the default page at http://localhost/tutorial

  public function beforeFilter(\Cake\Event\EventInterface $event)
  {
    parent::beforeFilter($event);
    $this->Authentication->addUnauthenticatedActions(['index', 'view', 'display']);
  }

Now if you check your site at http://localhost/tutorial/users/ you can view the current list of users, but if you click New User, you aren't logged in yet so do not have authentication to view that page. You'll get a infinite redirect error because the user login function and View template don't exist yet and that's where CakePHP will try to redirect you if it needs you to log in, and you also don't have authentication to access the login view.

Add Login Function and View

In /src/Controller/UsersController.php add the beforeFilter() function to allow the login, logout, reset-password, and password pages to not require authentication, and add the login() function so a user can login.

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', 'logout', 'password', 'resetPassword']);
}

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()) {
    $user = $this->Authentication->getIdentity();
    $this->Flash->success('You are logged in as ' . $user->full_name);
    // redirect to /users 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.'));
  }
}

And finally create our View for logging in at /templates/Users/login.php

<?php $this->assign('title', 'Login'); ?>
<div class="users form content">
  <h3>Login</h3>
  <?php echo $this->Form->create(); ?>
  <fieldset>
    <?php echo $this->Form->control('username', ['required' => true, 'autofocus' => true]) ?>
    <?php echo $this->Form->control('password', ['required' => true]) ?>
    <?php echo $this->Html->link(__('Forgot Password?'), ['action' => 'password']) ?>
  </fieldset>
  <?php echo $this->Form->submit(__('Login')); ?>
  <?php echo $this->Form->end() ?>
</div>

And now you can log in at http://localhost/tutorial/users/login and if you aren't logged in, page requests to /users/add will redirect you to the login page. Once you are logged in, you can once again click "New User" and get to the /users/add page.