Skip to main content

Cloudsoft Zone Build, deploy and scale with practical guides to PHP, Node.js, React, DevOps and AI automation. Explore the guides

Cloudsoft Zone
CakePHPDevelopmentPHPTutorial

CakePHP Authentication 4: build a secure login in CakePHP 5

Set up cakephp/authentication 4.x on CakePHP 5: middleware order, Session and Form authenticators, hashing, safe redirects, remember-me and tests.

Cloudsoft Zone

Updated 7 min read

Close-up of a hand typing on a dark laptop keyboard in low light
Photograph: www.Pixel.la Free Stock Photos / CC0, via Wikimedia Commons
In this article

To add login to a CakePHP 5 application in October 2026, install cakephp/authentication 4.x, add AuthenticationMiddleware after routing and body parsing, and configure a Session plus Form authenticator with a Password identifier. Version 4 changed how identifiers are configured, so most 3.x tutorials and Stack Overflow answers no longer copy-paste cleanly.

This tutorial builds a complete email-and-password login on the current release line (4.2.1, published 10 July 2026): service configuration, login and logout actions, password hashing, a safe post-login redirect, an optional “remember me” cookie and an integration test. The last section lists the 3.x to 4.x changes you need if you are upgrading an existing app.

What you need

  • A CakePHP 5 application. The current cakephp/app skeleton targets CakePHP 5.4, which needs PHP 8.2 or later. The plugin itself declares PHP 8.1+ and cakephp/http ^5.0.
  • A users table with at least id, email and password columns. The password column must be long enough for a bcrypt or Argon2 hash; VARCHAR(255) is the safe choice.
  • The CSRF middleware that ships with the skeleton left switched on. Login forms are a classic CSRF target.

Install and load the plugin from the project root:

Shell
composer require cakephp/authentication:^4.0
bin/cake plugin load Authentication

Wire the middleware in the right order

Authentication runs as PSR-15 middleware. Order matters: it must come after RoutingMiddleware (so array-style login URLs can be matched) and BodyParserMiddleware (so JSON logins are readable). The official docs call out middleware order as the first thing to check when redirects or JSON logins misbehave.

PHP
// src/Application.php
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
    $middlewareQueue
        ->add(new ErrorHandlerMiddleware(Configure::read('Error'), $this))
        ->add(new HostHeaderMiddleware())
        ->add(new AssetMiddleware(['cacheTime' => Configure::read('Asset.cacheTime')]))
        ->add(new RoutingMiddleware($this))
        ->add(new BodyParserMiddleware())
        ->add(new CsrfProtectionMiddleware(['httponly' => true]))
        ->add(new AuthenticationMiddleware($this));

    return $middlewareQueue;
}

Your Application class must also implement AuthenticationServiceProviderInterface, which the middleware calls once per request to get a configured service.

Configure the authentication service

This is where 4.x differs most from older tutorials. There is no loadIdentifier() on the service any more: each authenticator receives its own identifier through the identifier option, and the credential constants now live on PasswordIdentifier.

PHP
use Authentication\AuthenticationService;
use Authentication\AuthenticationServiceInterface;
use Authentication\AuthenticationServiceProviderInterface;
use Authentication\Identifier\PasswordIdentifier;
use Cake\Routing\Router;
use Psr\Http\Message\ServerRequestInterface;

public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
{
    $service = new AuthenticationService();
    $service->setConfig([
        'unauthenticatedRedirect' => Router::url(['prefix' => false, 'plugin' => false, 'controller' => 'Users', 'action' => 'login']),
        'queryParam' => 'redirect',
    ]);

    $fields = [
        PasswordIdentifier::CREDENTIAL_USERNAME => 'email',
        PasswordIdentifier::CREDENTIAL_PASSWORD => 'password',
    ];

    // Session first: once logged in, the identity is read from the session.
    $service->loadAuthenticator('Authentication.Session');
    $service->loadAuthenticator('Authentication.Form', [
        'identifier' => [
            'className' => 'Authentication.Password',
            'fields' => $fields,
        ],
        'fields' => $fields,
        'loginUrl' => Router::url(['prefix' => false, 'plugin' => false, 'controller' => 'Users', 'action' => 'login']),
    ]);

    return $service;
}

Authenticators run in order until one succeeds. Session reads the identity stored at login; Form only acts on the login URL and passes the posted email and password to the Password identifier, which looks the user up through the ORM and verifies the hash.

Hand holding a hardware token showing a one-time code in front of a login form with user name, password and passcode fields
Form authentication checks the posted credentials through an identifier. Photo: Brian Ronald, CC BY-SA 4.0.

Session or PrimaryKeySession?

The plain Session authenticator stores the whole user entity in the session and trusts it on later requests. If you need roles or an “is active” flag to take effect immediately, use PrimaryKeySession instead: it stores only the primary key and reloads the user on every request. In 4.x this replaces the removed identify option of Session.

AuthenticatorStores in sessionDatabase hit per requestUse when
SessionFull identityNoUser data rarely changes during a session
PrimaryKeySessionPrimary key onlyYes (one lookup)Permissions or account status must be current
PHP
$service->loadAuthenticator('Authentication.PrimaryKeySession');

Require login with the component

Load the component in AppController. From then on every action requires an authenticated identity unless you opt it out.

PHP
// src/Controller/AppController.php
public function initialize(): void
{
    parent::initialize();
    $this->loadComponent('Flash');
    $this->loadComponent('Authentication.Authentication');
}

Public actions are listed per controller in beforeFilter():

PHP
// src/Controller/UsersController.php
public function beforeFilter(\Cake\Event\EventInterface $event): void
{
    parent::beforeFilter($event);
    $this->Authentication->allowUnauthenticated(['login', 'add']);
}

Write the login and logout actions

The middleware has already tried to authenticate the request by the time your action runs, so the login action only inspects the result. Since 4.1.0 the component has redirectAfterLogin(), which sends the user to the validated redirect query parameter or to the default you pass. Use it instead of reading ?redirect= yourself; passing raw query strings to redirect() creates an open redirect.

PHP
public function login(): ?\Cake\Http\Response
{
    $this->request->allowMethod(['get', 'post']);
    $result = $this->Authentication->getResult();

    if ($result && $result->isValid()) {
        return $this->Authentication->redirectAfterLogin('/dashboard');
    }
    if ($this->request->is('post')) {
        $this->Flash->error(__('Invalid email or password'));
    }

    return null;
}

public function logout(): \Cake\Http\Response
{
    $this->Authentication->logout();

    return $this->redirect(['controller' => 'Users', 'action' => 'login']);
}

Keep the error message generic. Saying whether the email or the password was wrong lets attackers enumerate accounts. The template is an ordinary form; FormHelper adds the CSRF token automatically:

PHP
<?= $this->Form->create() ?>
<?= $this->Form->control('email', ['type' => 'email', 'autocomplete' => 'username']) ?>
<?= $this->Form->control('password', ['autocomplete' => 'current-password']) ?>
<?= $this->Form->button(__('Log in')) ?>
<?= $this->Form->end() ?>

Log out with a POST link ($this->Form->postLink()) rather than a plain GET link, so a third-party page cannot log your users out.

Hash passwords on save

The Password identifier uses DefaultPasswordHasher, which calls PHP’s password_hash() with PASSWORD_DEFAULT (bcrypt today). Hash in an entity mutator so that every save path is covered:

PHP
// src/Model/Entity/User.php
namespace App\Model\Entity;

use Authentication\PasswordHasher\DefaultPasswordHasher;
use Cake\ORM\Entity;

class User extends Entity
{
    protected array $_accessible = ['email' => true, 'password' => true];
    protected array $_hidden = ['password'];

    protected function _setPassword(string $password): string
    {
        return (new DefaultPasswordHasher())->hash($password);
    }
}

$_hidden keeps the hash out of JSON output and of any view that serialises the user. Never make id or role columns mass-assignable through $_accessible.

Migrating legacy hashes

If you are inheriting MD5 or SHA1 hashes from an older system, configure the identifier with Authentication.Fallback listing Authentication.Default first and Authentication.Legacy second. After a successful login, check $authentication->getIdentificationProvider()->needsPasswordRehash() and save the plain password again so it is re-hashed with bcrypt. The plugin documentation shows the full pattern.

Yellow key ring tag printed with the words Encryption is key
Remember-me cookies must be encrypted with EncryptedCookieMiddleware. Photo: Rohini, CC BY-SA 4.0.

Add “remember me” safely

The Cookie authenticator re-logs users in after their session expires. It must be paired with EncryptedCookieMiddleware; without encryption the cookie can be tampered with. Add the middleware with the cookie name, then load the authenticator after Form and Session:

PHP
$middlewareQueue->add(new EncryptedCookieMiddleware(['CookieAuth'], Configure::read('Security.cookieKey')));

$service->loadAuthenticator('Authentication.Cookie', [
    'identifier' => 'Authentication.Password',
    'fields' => $fields,
    'loginUrl' => '/users/login',
]);

Add a remember_me checkbox to the login form. The cookie stores the username and a hashed token, and calling logout() destroys it. Security.cookieKey is your own configuration key; store a long random value in an environment variable, never in a committed file. Release 4.2.1 also hardened how these cookie tokens are generated, which is one more reason to stay on the latest 4.x patch.

Test the protected pages

In integration tests you do not need to post the login form. Put a user into the session under the default Auth key and request the page:

PHP
use Cake\TestSuite\IntegrationTestTrait;
use Cake\TestSuite\TestCase;

class DashboardControllerTest extends TestCase
{
    use IntegrationTestTrait;

    protected array $fixtures = ['app.Users'];

    public function testRedirectsGuests(): void
    {
        $this->get('/dashboard');
        $this->assertRedirectContains('/users/login');
    }

    public function testShowsDashboardToUser(): void
    {
        $user = $this->fetchTable('Users')->get(1);
        $this->session(['Auth' => $user]);
        $this->get('/dashboard');
        $this->assertResponseOk();
    }
}

If you switched to PrimaryKeySession, store the key instead: $this->session(['Auth' => 1]). For POST tests, call $this->enableCsrfToken() first.

Upgrading from Authentication 3.x

Version 4.0.0 was released on 9 February 2026. The official upgrade guide lists these breaking changes:

3.x4.x
$service->loadIdentifier(...)Pass identifier in each loadAuthenticator() call
IdentifierCollectionRemoved; use IdentifierFactory or a single identifier
AbstractIdentifier::CREDENTIAL_*PasswordIdentifier::CREDENTIAL_* (or LdapIdentifier)
Session with 'identify' => truePrimaryKeySession
CakeRouterUrlCheckerNow named DefaultUrlChecker; the old string checker is StringUrlChecker
Several loginUrl values auto-detectedSet 'urlChecker' => 'Authentication.Multi' explicitly

Most applications only need the first three rows: a search-and-replace for the constants and moving identifier config into the Form authenticator. Multi-language sites with several login URLs must add the Multi URL checker or logins on the extra URLs silently stop working.

Gotchas

  • Login always fails with correct credentials. Check the fields mapping on both the identifier and the authenticator, and that the stored password is a hash, not plain text.
  • JSON logins fail. BodyParserMiddleware must come before AuthenticationMiddleware.
  • Redirect loop on the login page. The login action is missing from allowUnauthenticated().
  • Authorization next. Authentication only answers “who is this?”. Use the separate cakephp/authorization plugin to decide what that user may do.

What to do next

  1. Pin cakephp/authentication to ^4.2 and run composer update to pick up the cookie-token hardening.
  2. Add rate limiting or a lockout in front of the login action; the plugin does not throttle attempts.
  3. Write one integration test per protected controller that asserts guests are redirected.

Frequently asked questions

  • Which Authentication plugin version works with CakePHP 5?

    Version 4.x of cakephp/authentication is the current line for CakePHP 5; 4.2.1 was the latest release in October 2026. Version 3.x also targeted CakePHP 5 but new projects should start on 4.x.

  • Why does loadIdentifier() no longer exist?

    Authentication 4.0 removed it. Each authenticator now receives its identifier through the identifier option of loadAuthenticator().

  • How do I redirect users back to the page they wanted after login?

    Set queryParam on the service and call $this->Authentication->redirectAfterLogin('/default') in the login action. It was added in 4.1.0 and only follows validated local redirect targets.

  • Which password hashing algorithm does the plugin use?

    DefaultPasswordHasher uses PHP’s PASSWORD_DEFAULT, which is bcrypt in current PHP versions. You can pass a different hashType such as Argon2id in the hasher options.

  • How do I make role changes apply without logging out?

    Use the PrimaryKeySession authenticator. It stores only the user’s primary key and reloads the user on every request.

  • Does the plugin limit login attempts?

    No. Add rate limiting yourself, for example at the web server, a reverse proxy or a custom middleware placed before authentication.

Advertise on CloudSoftZone

Your next customer could start right here.

Introduce your tool, product or business to readers exploring technology.

Advertise with us

Explore placements and request a private quote

Sources

  1. Authentication 4.x: Quick Start — CakePHP Book (Authentication 4.x)
  2. Upgrade Guide 3.x to 4.x — CakePHP Book (Authentication 4.x)
  3. Authenticators — CakePHP Book (Authentication 4.x)
  4. Authentication Component (docs source) — cakephp/authentication on GitHub
  5. Release 4.1.0 — cakephp/authentication on GitHub, 2026-04-22
  6. Release 4.2.1 — cakephp/authentication on GitHub, 2026-07-10

Written with substantial AI assistance. Facts checked against cited sources. Editorial policy.

Comments

No comments yet. Be the first to join the conversation.

Leave a comment

Every comment is read by a moderator before it appears. Your email address is never published; it is used only to contact you about this comment.

Never published. Used only to contact you about this comment.

Loading the security check…

Comments are moderated before publication.

  • Plush elePHPant toy from Heroku, a version of the PHP community mascot, photographed close up

    Laravel

    Upgrading from Laravel 12 to Laravel 13: a practical guide

    Laravel 12 lost bug-fix support in August 2026. This guide covers the PHP 8.3 requirement, Composer changes, the CSRF middleware rename, cache and session hardening, low-impact changes that often bite, and a safe rollout plan.

    By Cloudsoft Zone