@@ -16,15 +16,15 @@ enable new users to register.
1616Use 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
2424You need to have created the ` Controller ` , ` Table ` , ` Entity ` and
2525templates for the ` users ` table in your database. You can do this manually
2626like 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
3030bin/cake bake all users
@@ -34,31 +34,30 @@ If you create or update a user with this setup, you might notice that
3434the passwords are stored in plain text. This is really bad from a security point
3535of 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.
3939Methods that operate on the collection of entities are put in the ` Table ` class,
4040while 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
5150namespace App\Model\Entity;
5251
53- use Authentication\PasswordHasher\DefaultPasswordHasher; // Add this line
52+ use Authentication\PasswordHasher\DefaultPasswordHasher;
5453use Cake\ORM\Entity;
5554
5655class 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
7978bcrypt for all new applications to keep your security standards high. This
8079is 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/
9089Now it's time to configure the Authentication Plugin.
9190The 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
105100is authenticated (based on the configuration you provided) and injects the user and
106101the authentication results into the request for further reference.
102+ :::
103+
104+ ### Configuring the Application
107105
108106In ** src/Application.php** , add the following imports:
109107
110108``` php
111- // In src/Application.php add the following imports
112109use Authentication\AuthenticationService;
113110use Authentication\AuthenticationServiceInterface;
114111use Authentication\AuthenticationServiceProviderInterface;
112+ use Authentication\Identifier\AbstractIdentifier;
115113use Authentication\Middleware\AuthenticationMiddleware;
116- use Cake\Routing\Router;
117114use Psr\Http\Message\ServerRequestInterface;
118115```
119116
120117Then implement the authentication interface on your ` Application ` class:
121118
122- ``` php
119+ ``` php {3}
123120// in src/Application.php
124121class 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
133132public 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
145147public 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+
175192In your ` AppController ` class add the following code:
176193
177- ``` php
194+ ``` php {7}
178195// src/Controller/AppController.php
179196public 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
188206Now, on every request, the ` AuthenticationMiddleware ` will inspect
189207the request session to look for an authenticated user. If we are loading the ` /users/login `
190208page, it will also inspect the posted form data (if any) to extract the credentials.
191209By default, the credentials will be extracted from the ` username ` and ` password `
192210fields in the request data.
211+
193212The 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+
196216All your pages will be restricted as the ` AuthenticationComponent ` is checking the
197217result on every request. When it fails to find any authenticated user, it will redirect the
198218user 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.
200222If 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
208233In your ` UsersController ` , add the following code:
209234
210- ``` php
235+ ::: code-group
236+
237+ ``` php [UsersController.php]
211238public 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
219246public 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.
261283Test it by requesting any page of your site. After being redirected
262284to the ` /users/login ` page, enter the email and password you
263285picked previously when creating your user. You should be redirected
264286successfully after login.
265287
288+ ### Allowing Public Access
289+
266290We need to add a couple more details to configure our application.
267291We want all ` view ` and ` index ` pages accessible without logging in so we'll add this specific
268292configuration in AppController:
269293
270- ``` php
294+ ``` php {6}
271295// in src/Controller/AppController.php
272296public 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
288313Try it out by visiting ` /articles/add ` before logging in! Since this action is not
289314allowed, 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
298323public 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
315335If you try to visit ** /users/add** without being logged in, you will be
316336redirected 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
324344The above tells ` AuthenticationComponent ` that the ` add() ` action of the
0 commit comments