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.
Updated 7 min read

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/appskeleton targets CakePHP 5.4, which needs PHP 8.2 or later. The plugin itself declares PHP 8.1+ andcakephp/http ^5.0. - A
userstable with at leastid,emailandpasswordcolumns. 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:
composer require cakephp/authentication:^4.0
bin/cake plugin load AuthenticationWire 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.
// 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.
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.

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.
| Authenticator | Stores in session | Database hit per request | Use when |
|---|---|---|---|
Session | Full identity | No | User data rarely changes during a session |
PrimaryKeySession | Primary key only | Yes (one lookup) | Permissions or account status must be current |
$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.
// 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():
// 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.
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:
<?= $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:
// 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.

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:
$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:
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.x | 4.x |
|---|---|
$service->loadIdentifier(...) | Pass identifier in each loadAuthenticator() call |
IdentifierCollection | Removed; use IdentifierFactory or a single identifier |
AbstractIdentifier::CREDENTIAL_* | PasswordIdentifier::CREDENTIAL_* (or LdapIdentifier) |
Session with 'identify' => true | PrimaryKeySession |
CakeRouterUrlChecker | Now named DefaultUrlChecker; the old string checker is StringUrlChecker |
Several loginUrl values auto-detected | Set '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
fieldsmapping on both the identifier and the authenticator, and that the stored password is a hash, not plain text. - JSON logins fail.
BodyParserMiddlewaremust come beforeAuthenticationMiddleware. - Redirect loop on the login page. The
loginaction is missing fromallowUnauthenticated(). - Authorization next. Authentication only answers “who is this?”. Use the separate
cakephp/authorizationplugin to decide what that user may do.
What to do next
- Pin
cakephp/authenticationto^4.2and runcomposer updateto pick up the cookie-token hardening. - Add rate limiting or a lockout in front of the login action; the plugin does not throttle attempts.
- 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/authenticationis 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
identifieroption ofloadAuthenticator().How do I redirect users back to the page they wanted after login?
Set
queryParamon 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?
DefaultPasswordHasheruses PHP’sPASSWORD_DEFAULT, which is bcrypt in current PHP versions. You can pass a differenthashTypesuch as Argon2id in the hasher options.How do I make role changes apply without logging out?
Use the
PrimaryKeySessionauthenticator. 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.
Your next customer could start right here.
Introduce your tool, product or business to readers exploring technology.
Explore placements and request a private quote
Sources
- Authentication 4.x: Quick Start — CakePHP Book (Authentication 4.x)
- Upgrade Guide 3.x to 4.x — CakePHP Book (Authentication 4.x)
- Authenticators — CakePHP Book (Authentication 4.x)
- Authentication Component (docs source) — cakephp/authentication on GitHub
- Release 4.1.0 — cakephp/authentication on GitHub, 2026-04-22
- 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.
More from Cloudsoft Zone

Industry updateCakePHP
What's new in CakePHP 5.4: ORM changes, DTO mapping and a built-in container
CakePHP 5.4 changes default eager loading, enforces entity-table ownership and adds request-to-DTO mapping, an optional built-in DI container, typed unhydrated queries and JSON streaming. Here is what to check before upgrading.
By Cloudsoft Zone

Troubleshooting guideCodeIgniter
CodeIgniter 4 validation not working? 9 causes and fixes
Fix CodeIgniter 4 validation problems fast: wrong input source, lost errors after redirect, permit_empty, strict JSON rules, is_unique placeholders and more.
By Cloudsoft Zone

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
