Skip to content

Commit dd97e57

Browse files
committed
Update CMS authentication tutorial for Authentication plugin v4
Updates for v4 API: - Update version requirement to ~4.0 - Use allowUnauthenticated() instead of deprecated addUnauthenticatedActions() - Use array URLs instead of Router::url() strings - Use setConfig() method for service configuration - Use AbstractIdentifier constants for field mapping - Use getLoginRedirect() for handling post-login redirects - Simplify login/logout actions VitePress enhancements for better readability: - Add code-group tabs for related code (middleware/service, controller/template) - Add line highlighting {3,12-18} for important additions - Add ::: tip, ::: info, ::: warning, ::: details containers - Add summary table for authentication classes - Use [!code ++] diff annotation for new code - Restructure with clearer subsections
1 parent e882d9e commit dd97e57

File tree

1 file changed

+109
-89
lines changed

1 file changed

+109
-89
lines changed

docs/en/tutorials-and-examples/cms/authentication.md

Lines changed: 109 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ enable new users to register.
1616
Use composer to install the Authentication Plugin:
1717

1818
```bash
19-
composer require "cakephp/authentication:~3.0"
19+
composer require "cakephp/authentication:~4.0"
2020
```
2121

22-
### Adding Password Hashing
22+
## Adding Password Hashing
2323

2424
You need to have created the `Controller`, `Table`, `Entity` and
2525
templates for the `users` table in your database. You can do this manually
2626
like you did before for the ArticlesController, or you can use the bake shell
27-
to generate the classes for you using:
27+
to generate the classes for you:
2828

2929
```bash
3030
bin/cake bake all users
@@ -34,31 +34,30 @@ If you create or update a user with this setup, you might notice that
3434
the passwords are stored in plain text. This is really bad from a security point
3535
of view, so lets fix that.
3636

37-
This is also a good time to talk about the model layer in CakePHP. In CakePHP,
38-
we use different classes to operate on collections of records and single records.
37+
::: tip Table vs Entity
38+
In CakePHP, we use different classes to operate on collections of records and single records.
3939
Methods that operate on the collection of entities are put in the `Table` class,
4040
while features belonging to a single record are put on the `Entity` class.
41+
Password hashing is done on the individual record, so we'll implement this behavior on the entity object.
42+
:::
4143

42-
For example, password hashing is done on the individual record, so we'll
43-
implement this behavior on the entity object. Because we want to hash the
44-
password each time it is set, we'll use a mutator/setter method. CakePHP will
45-
call a convention based setter method any time a property is set in one of your
46-
entities. Let's add a setter for the password. In **src/Model/Entity/User.php**
47-
add the following:
44+
Because we want to hash the password each time it is set, we'll use a mutator/setter method.
45+
CakePHP will call a convention based setter method any time a property is set in one of your
46+
entities. Let's add a setter for the password in **src/Model/Entity/User.php**:
4847

49-
```php
48+
```php {3,12-18}
5049
<?php
5150
namespace App\Model\Entity;
5251

53-
use Authentication\PasswordHasher\DefaultPasswordHasher; // Add this line
52+
use Authentication\PasswordHasher\DefaultPasswordHasher;
5453
use Cake\ORM\Entity;
5554

5655
class User extends Entity
5756
{
5857
// Code from bake.
5958

6059
// Add this method
61-
protected function _setPassword(string $password) : ?string
60+
protected function _setPassword(string $password): ?string
6261
{
6362
if (mb_strlen($password) > 0) {
6463
return (new DefaultPasswordHasher())->hash($password);
@@ -79,7 +78,7 @@ view pages. CakePHP hashes passwords with [bcrypt](https://codahale.com/how-to-s
7978
bcrypt for all new applications to keep your security standards high. This
8079
is the [recommended password hash algorithm for PHP](https://www.php.net/manual/en/function.password-hash.php).
8180

82-
> [!NOTE]
81+
> [!IMPORTANT]
8382
> Create a hashed password for at least one of the user accounts now!
8483
> It will be needed in the next steps.
8584
> After updating the password, you'll see a long string stored in the password column.
@@ -90,45 +89,45 @@ is the [recommended password hash algorithm for PHP](https://www.php.net/manual/
9089
Now it's time to configure the Authentication Plugin.
9190
The Plugin will handle the authentication process using 3 different classes:
9291

93-
- `Application` will use the Authentication Middleware and provide an
94-
AuthenticationService, holding all the configuration we want to define how are
95-
we going to check the credentials, and where to find them.
96-
- `AuthenticationService` will be a utility class to allow you configure the
97-
authentication process.
98-
- `AuthenticationMiddleware` will be executed as part of the middleware queue,
99-
this is before your Controllers are processed by the framework, and will pick the
100-
credentials and process them to check if the user is authenticated.
101-
102-
If you remember, we used `AuthComponent`
103-
before to handle all these steps. Now the logic is divided into specific classes and
104-
the authentication process happens before your controller layer. First it checks if the user
92+
| Class | Purpose |
93+
|-------|---------|
94+
| `Application` | Uses the Authentication Middleware and provides an AuthenticationService |
95+
| `AuthenticationService` | Utility class to configure the authentication process |
96+
| `AuthenticationMiddleware` | Executed as part of the middleware queue, before controllers |
97+
98+
::: info How It Works
99+
The authentication process happens before your controller layer. First it checks if the user
105100
is authenticated (based on the configuration you provided) and injects the user and
106101
the authentication results into the request for further reference.
102+
:::
103+
104+
### Configuring the Application
107105

108106
In **src/Application.php**, add the following imports:
109107

110108
```php
111-
// In src/Application.php add the following imports
112109
use Authentication\AuthenticationService;
113110
use Authentication\AuthenticationServiceInterface;
114111
use Authentication\AuthenticationServiceProviderInterface;
112+
use Authentication\Identifier\AbstractIdentifier;
115113
use Authentication\Middleware\AuthenticationMiddleware;
116-
use Cake\Routing\Router;
117114
use Psr\Http\Message\ServerRequestInterface;
118115
```
119116

120117
Then implement the authentication interface on your `Application` class:
121118

122-
```php
119+
```php {3}
123120
// in src/Application.php
124121
class Application extends BaseApplication
125122
implements AuthenticationServiceProviderInterface
126123
{
127124
```
128125

129-
Then add the following:
126+
Then add the following methods:
130127

131-
```php
128+
::: code-group
129+
130+
```php [middleware()]
132131
// src/Application.php
133132
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
134133
{
@@ -137,44 +136,62 @@ public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
137136
->add(new RoutingMiddleware($this))
138137
->add(new BodyParserMiddleware())
139138
// Add the AuthenticationMiddleware. It should be after routing and body parser.
140-
->add(new AuthenticationMiddleware($this));
139+
->add(new AuthenticationMiddleware($this)); // [!code ++]
141140

142141
return $middlewareQueue;
143142
}
143+
```
144144

145+
```php [getAuthenticationService()]
146+
// src/Application.php
145147
public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
146148
{
147-
$authenticationService = new AuthenticationService([
148-
'unauthenticatedRedirect' => Router::url('/users/login'),
149+
$service = new AuthenticationService();
150+
151+
// Define where users should be redirected to when they are not authenticated
152+
$service->setConfig([
153+
'unauthenticatedRedirect' => [
154+
'prefix' => false,
155+
'plugin' => null,
156+
'controller' => 'Users',
157+
'action' => 'login',
158+
],
149159
'queryParam' => 'redirect',
150160
]);
151161

152-
// Load the authenticators, you want session first
153-
$authenticationService->loadAuthenticator('Authentication.Session');
154-
// Configure form data check to pick email and password
155-
$authenticationService->loadAuthenticator('Authentication.Form', [
156-
'fields' => [
157-
'username' => 'email',
158-
'password' => 'password',
162+
$fields = [
163+
AbstractIdentifier::CREDENTIAL_USERNAME => 'email',
164+
AbstractIdentifier::CREDENTIAL_PASSWORD => 'password',
165+
];
166+
167+
// Load the authenticators. Session should be first.
168+
$service->loadAuthenticator('Authentication.Session');
169+
$service->loadAuthenticator('Authentication.Form', [
170+
'fields' => $fields,
171+
'loginUrl' => [
172+
'prefix' => false,
173+
'plugin' => null,
174+
'controller' => 'Users',
175+
'action' => 'login',
159176
],
160-
'loginUrl' => Router::url('/users/login'),
161177
'identifier' => [
162178
'Authentication.Password' => [
163-
'fields' => [
164-
'username' => 'email',
165-
'password' => 'password',
166-
],
179+
'fields' => $fields,
167180
],
168181
],
169182
]);
170183

171-
return $authenticationService;
184+
return $service;
172185
}
173186
```
174187

188+
:::
189+
190+
### Configuring the AppController
191+
175192
In your `AppController` class add the following code:
176193

177-
```php
194+
```php {7}
178195
// src/Controller/AppController.php
179196
public function initialize(): void
180197
{
@@ -183,107 +200,115 @@ public function initialize(): void
183200

184201
// Add this line to check authentication result and lock your site
185202
$this->loadComponent('Authentication.Authentication');
203+
}
186204
```
187205

188206
Now, on every request, the `AuthenticationMiddleware` will inspect
189207
the request session to look for an authenticated user. If we are loading the `/users/login`
190208
page, it will also inspect the posted form data (if any) to extract the credentials.
191209
By default, the credentials will be extracted from the `username` and `password`
192210
fields in the request data.
211+
193212
The authentication result will be injected in a request attribute named
194213
`authentication`. You can inspect the result at any time using
195214
`$this->request->getAttribute('authentication')` from your controller actions.
215+
196216
All your pages will be restricted as the `AuthenticationComponent` is checking the
197217
result on every request. When it fails to find any authenticated user, it will redirect the
198218
user to the `/users/login` page.
199-
Note at this point, the site won't work as we don't have a login page yet.
219+
220+
::: warning Infinite Redirect Loop
221+
At this point, the site won't work as we don't have a login page yet.
200222
If you visit your site, you'll get an "infinite redirect loop" so let's fix that.
223+
:::
201224

202225
> [!NOTE]
203226
> If your application serves from both SSL and non-SSL protocols, then you might have problems
204227
> with sessions being lost, in case your application is on non-SSL protocol. You need to enable
205228
> access by setting session.cookie_secure to false in your config/app.php or config/app_local.php.
206-
> (See [CakePHP’s defaults on session.cookie_secure](../../development/sessions))
229+
> (See [CakePHP's defaults on session.cookie_secure](../../development/sessions))
230+
231+
### Creating the Login Action
207232

208233
In your `UsersController`, add the following code:
209234

210-
```php
235+
::: code-group
236+
237+
```php [UsersController.php]
211238
public function beforeFilter(\Cake\Event\EventInterface $event): void
212239
{
213240
parent::beforeFilter($event);
214241
// Configure the login action to not require authentication, preventing
215242
// the infinite redirect loop issue
216-
$this->Authentication->addUnauthenticatedActions(['login']);
243+
$this->Authentication->allowUnauthenticated(['login']);
217244
}
218245

219246
public function login()
220247
{
221-
$this->request->allowMethod(['get', 'post']);
222248
$result = $this->Authentication->getResult();
223-
// regardless of POST or GET, redirect if user is logged in
249+
// If the user is logged in send them away.
224250
if ($result && $result->isValid()) {
225-
// redirect to /articles after login success
226-
$redirect = $this->request->getQuery('redirect', [
251+
$target = $this->Authentication->getLoginRedirect() ?? [
227252
'controller' => 'Articles',
228253
'action' => 'index',
229-
]);
230-
231-
return $this->redirect($redirect);
254+
];
255+
return $this->redirect($target);
232256
}
233-
// display error if user submitted and authentication failed
234-
if ($this->request->is('post') && !$result->isValid()) {
257+
if ($this->request->is('post')) {
235258
$this->Flash->error(__('Invalid username or password'));
236259
}
237260
}
238261
```
239262

240-
Add the template logic for your login action:
241-
242-
```php
243-
<!-- in /templates/Users/login.php -->
244-
<div class="users form">
263+
```php [templates/Users/login.php]
264+
<div class="users form content">
245265
<?= $this->Flash->render() ?>
246266
<h3>Login</h3>
247267
<?= $this->Form->create() ?>
248268
<fieldset>
249-
<legend><?= __('Please enter your username and password') ?></legend>
269+
<legend><?= __('Please enter your email and password') ?></legend>
250270
<?= $this->Form->control('email', ['required' => true]) ?>
251271
<?= $this->Form->control('password', ['required' => true]) ?>
252272
</fieldset>
253-
<?= $this->Form->submit(__('Login')); ?>
273+
<?= $this->Form->button(__('Login')); ?>
254274
<?= $this->Form->end() ?>
255275

256276
<?= $this->Html->link("Add User", ['action' => 'add']) ?>
257277
</div>
258278
```
259279

260-
Now login page will allow us to correctly login into the application.
280+
:::
281+
282+
Now the login page will allow us to correctly login into the application.
261283
Test it by requesting any page of your site. After being redirected
262284
to the `/users/login` page, enter the email and password you
263285
picked previously when creating your user. You should be redirected
264286
successfully after login.
265287

288+
### Allowing Public Access
289+
266290
We need to add a couple more details to configure our application.
267291
We want all `view` and `index` pages accessible without logging in so we'll add this specific
268292
configuration in AppController:
269293

270-
```php
294+
```php {6}
271295
// in src/Controller/AppController.php
272296
public function beforeFilter(\Cake\Event\EventInterface $event): void
273297
{
274298
parent::beforeFilter($event);
275299
// for all controllers in our application, make index and view
276300
// actions public, skipping the authentication check
277-
$this->Authentication->addUnauthenticatedActions(['index', 'view']);
301+
$this->Authentication->allowUnauthenticated(['index', 'view']);
278302
}
279303
```
280304

281-
> [!NOTE]
282-
> If you don't have a user with a hashed password yet, comment the
283-
> `$this->loadComponent('Authentication.Authentication')` line in your
284-
> AppController and all other lines where Authentication is used. Then go to
285-
> `/users/add` to create a new user picking email and password. Afterward,
286-
> make sure to uncomment the lines we just temporarily commented!
305+
::: details Locked out? Create a user without authentication
306+
If you don't have a user with a hashed password yet, comment the
307+
`$this->loadComponent('Authentication.Authentication')` line in your
308+
AppController and all other lines where Authentication is used. Then go to
309+
`/users/add` to create a new user picking email and password. Afterward,
310+
make sure to uncomment the lines we just temporarily commented!
311+
:::
287312

288313
Try it out by visiting `/articles/add` before logging in! Since this action is not
289314
allowed, you will be redirected to the login page. After logging in
@@ -297,13 +322,8 @@ Add the logout action to the `UsersController` class:
297322
// in src/Controller/UsersController.php
298323
public function logout()
299324
{
300-
$result = $this->Authentication->getResult();
301-
// regardless of POST or GET, redirect if user is logged in
302-
if ($result && $result->isValid()) {
303-
$this->Authentication->logout();
304-
305-
return $this->redirect(['controller' => 'Users', 'action' => 'login']);
306-
}
325+
$this->Authentication->logout();
326+
return $this->redirect(['controller' => 'Users', 'action' => 'login']);
307327
}
308328
```
309329

@@ -314,11 +334,11 @@ page.
314334

315335
If you try to visit **/users/add** without being logged in, you will be
316336
redirected to the login page. We should fix that as we want to allow people to
317-
sign up for our application. In the `UsersController` fix the following line:
337+
sign up for our application. In the `UsersController` update the `beforeFilter`:
318338

319-
```php
320-
// Add to the beforeFilter method of UsersController
321-
$this->Authentication->addUnauthenticatedActions(['login', 'add']);
339+
```php {3}
340+
// In UsersController::beforeFilter()
341+
$this->Authentication->allowUnauthenticated(['login', 'add']);
322342
```
323343

324344
The above tells `AuthenticationComponent` that the `add()` action of the

0 commit comments

Comments
 (0)