diff --git a/docs/en/tutorials-and-examples/cms/authentication.md b/docs/en/tutorials-and-examples/cms/authentication.md index 48bc9bc585..1cad0d1e71 100644 --- a/docs/en/tutorials-and-examples/cms/authentication.md +++ b/docs/en/tutorials-and-examples/cms/authentication.md @@ -16,15 +16,15 @@ enable new users to register. Use composer to install the Authentication Plugin: ```bash -composer require "cakephp/authentication:~3.0" +composer require "cakephp/authentication:~4.0" ``` -### Adding Password Hashing +## Adding Password Hashing You need to have created the `Controller`, `Table`, `Entity` and templates for the `users` table in your database. You can do this manually like you did before for the ArticlesController, or you can use the bake shell -to generate the classes for you using: +to generate the classes for you: ```bash bin/cake bake all users @@ -34,23 +34,22 @@ If you create or update a user with this setup, you might notice that the passwords are stored in plain text. This is really bad from a security point of view, so lets fix that. -This is also a good time to talk about the model layer in CakePHP. In CakePHP, -we use different classes to operate on collections of records and single records. +::: tip Table vs Entity +In CakePHP, we use different classes to operate on collections of records and single records. Methods that operate on the collection of entities are put in the `Table` class, while features belonging to a single record are put on the `Entity` class. +Password hashing is done on the individual record, so we'll implement this behavior on the entity object. +::: -For example, password hashing is done on the individual record, so we'll -implement this behavior on the entity object. Because we want to hash the -password each time it is set, we'll use a mutator/setter method. CakePHP will -call a convention based setter method any time a property is set in one of your -entities. Let's add a setter for the password. In **src/Model/Entity/User.php** -add the following: +Because we want to hash the password each time it is set, we'll use a mutator/setter method. +CakePHP will call a convention based setter method any time a property is set in one of your +entities. Let's add a setter for the password in **src/Model/Entity/User.php**: -```php +```php {3,12-18} 0) { return (new DefaultPasswordHasher())->hash($password); @@ -79,7 +78,7 @@ view pages. CakePHP hashes passwords with [bcrypt](https://codahale.com/how-to-s bcrypt for all new applications to keep your security standards high. This is the [recommended password hash algorithm for PHP](https://www.php.net/manual/en/function.password-hash.php). -> [!NOTE] +> [!IMPORTANT] > Create a hashed password for at least one of the user accounts now! > It will be needed in the next steps. > 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/ Now it's time to configure the Authentication Plugin. The Plugin will handle the authentication process using 3 different classes: -- `Application` will use the Authentication Middleware and provide an - AuthenticationService, holding all the configuration we want to define how are - we going to check the credentials, and where to find them. -- `AuthenticationService` will be a utility class to allow you configure the - authentication process. -- `AuthenticationMiddleware` will be executed as part of the middleware queue, - this is before your Controllers are processed by the framework, and will pick the - credentials and process them to check if the user is authenticated. - -If you remember, we used `AuthComponent` -before to handle all these steps. Now the logic is divided into specific classes and -the authentication process happens before your controller layer. First it checks if the user +| Class | Purpose | +|-------|---------| +| `Application` | Uses the Authentication Middleware and provides an AuthenticationService | +| `AuthenticationService` | Utility class to configure the authentication process | +| `AuthenticationMiddleware` | Executed as part of the middleware queue, before controllers | + +::: info How It Works +The authentication process happens before your controller layer. First it checks if the user is authenticated (based on the configuration you provided) and injects the user and the authentication results into the request for further reference. +::: + +### Configuring the Application In **src/Application.php**, add the following imports: ```php -// In src/Application.php add the following imports use Authentication\AuthenticationService; use Authentication\AuthenticationServiceInterface; use Authentication\AuthenticationServiceProviderInterface; +use Authentication\Identifier\AbstractIdentifier; use Authentication\Middleware\AuthenticationMiddleware; -use Cake\Routing\Router; use Psr\Http\Message\ServerRequestInterface; ``` Then implement the authentication interface on your `Application` class: -```php +```php {3} // in src/Application.php class Application extends BaseApplication implements AuthenticationServiceProviderInterface { ``` -Then add the following: +Then add the following methods: -```php +::: code-group + +```php [middleware()] // src/Application.php public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue { @@ -137,44 +136,62 @@ public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue ->add(new RoutingMiddleware($this)) ->add(new BodyParserMiddleware()) // Add the AuthenticationMiddleware. It should be after routing and body parser. - ->add(new AuthenticationMiddleware($this)); + ->add(new AuthenticationMiddleware($this)); // [!code ++] return $middlewareQueue; } +``` +```php [getAuthenticationService()] +// src/Application.php public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface { - $authenticationService = new AuthenticationService([ - 'unauthenticatedRedirect' => Router::url('/users/login'), + $service = new AuthenticationService(); + + // Define where users should be redirected to when they are not authenticated + $service->setConfig([ + 'unauthenticatedRedirect' => [ + 'prefix' => false, + 'plugin' => null, + 'controller' => 'Users', + 'action' => 'login', + ], 'queryParam' => 'redirect', ]); - // Load the authenticators, you want session first - $authenticationService->loadAuthenticator('Authentication.Session'); - // Configure form data check to pick email and password - $authenticationService->loadAuthenticator('Authentication.Form', [ - 'fields' => [ - 'username' => 'email', - 'password' => 'password', + $fields = [ + AbstractIdentifier::CREDENTIAL_USERNAME => 'email', + AbstractIdentifier::CREDENTIAL_PASSWORD => 'password', + ]; + + // Load the authenticators. Session should be first. + $service->loadAuthenticator('Authentication.Session'); + $service->loadAuthenticator('Authentication.Form', [ + 'fields' => $fields, + 'loginUrl' => [ + 'prefix' => false, + 'plugin' => null, + 'controller' => 'Users', + 'action' => 'login', ], - 'loginUrl' => Router::url('/users/login'), 'identifier' => [ 'Authentication.Password' => [ - 'fields' => [ - 'username' => 'email', - 'password' => 'password', - ], + 'fields' => $fields, ], ], ]); - return $authenticationService; + return $service; } ``` +::: + +### Configuring the AppController + In your `AppController` class add the following code: -```php +```php {7} // src/Controller/AppController.php public function initialize(): void { @@ -183,6 +200,7 @@ public function initialize(): void // Add this line to check authentication result and lock your site $this->loadComponent('Authentication.Authentication'); +} ``` Now, on every request, the `AuthenticationMiddleware` will inspect @@ -190,100 +208,107 @@ the request session to look for an authenticated user. If we are loading the `/u page, it will also inspect the posted form data (if any) to extract the credentials. By default, the credentials will be extracted from the `username` and `password` fields in the request data. + The authentication result will be injected in a request attribute named `authentication`. You can inspect the result at any time using `$this->request->getAttribute('authentication')` from your controller actions. + All your pages will be restricted as the `AuthenticationComponent` is checking the result on every request. When it fails to find any authenticated user, it will redirect the user to the `/users/login` page. -Note at this point, the site won't work as we don't have a login page yet. + +::: warning Infinite Redirect Loop +At this point, the site won't work as we don't have a login page yet. If you visit your site, you'll get an "infinite redirect loop" so let's fix that. +::: > [!NOTE] > If your application serves from both SSL and non-SSL protocols, then you might have problems > with sessions being lost, in case your application is on non-SSL protocol. You need to enable > access by setting session.cookie_secure to false in your config/app.php or config/app_local.php. -> (See [CakePHP’s defaults on session.cookie_secure](../../development/sessions)) +> (See [CakePHP's defaults on session.cookie_secure](../../development/sessions)) + +### Creating the Login Action In your `UsersController`, add the following code: -```php +::: code-group + +```php [UsersController.php] public function beforeFilter(\Cake\Event\EventInterface $event): void { parent::beforeFilter($event); // Configure the login action to not require authentication, preventing // the infinite redirect loop issue - $this->Authentication->addUnauthenticatedActions(['login']); + $this->Authentication->allowUnauthenticated(['login']); } public function login() { - $this->request->allowMethod(['get', 'post']); $result = $this->Authentication->getResult(); - // regardless of POST or GET, redirect if user is logged in + // If the user is logged in send them away. if ($result && $result->isValid()) { - // redirect to /articles after login success - $redirect = $this->request->getQuery('redirect', [ + $target = $this->Authentication->getLoginRedirect() ?? [ 'controller' => 'Articles', 'action' => 'index', - ]); - - return $this->redirect($redirect); + ]; + return $this->redirect($target); } - // display error if user submitted and authentication failed - if ($this->request->is('post') && !$result->isValid()) { + if ($this->request->is('post')) { $this->Flash->error(__('Invalid username or password')); } } ``` -Add the template logic for your login action: - -```php - -
+```php [templates/Users/login.php] +
Flash->render() ?>

Login

Form->create() ?>
- + Form->control('email', ['required' => true]) ?> Form->control('password', ['required' => true]) ?>
- Form->submit(__('Login')); ?> + Form->button(__('Login')); ?> Form->end() ?> Html->link("Add User", ['action' => 'add']) ?>
``` -Now login page will allow us to correctly login into the application. +::: + +Now the login page will allow us to correctly login into the application. Test it by requesting any page of your site. After being redirected to the `/users/login` page, enter the email and password you picked previously when creating your user. You should be redirected successfully after login. +### Allowing Public Access + We need to add a couple more details to configure our application. We want all `view` and `index` pages accessible without logging in so we'll add this specific configuration in AppController: -```php +```php {6} // in src/Controller/AppController.php public function beforeFilter(\Cake\Event\EventInterface $event): void { parent::beforeFilter($event); // for all controllers in our application, make index and view // actions public, skipping the authentication check - $this->Authentication->addUnauthenticatedActions(['index', 'view']); + $this->Authentication->allowUnauthenticated(['index', 'view']); } ``` -> [!NOTE] -> If you don't have a user with a hashed password yet, comment the -> `$this->loadComponent('Authentication.Authentication')` line in your -> AppController and all other lines where Authentication is used. Then go to -> `/users/add` to create a new user picking email and password. Afterward, -> make sure to uncomment the lines we just temporarily commented! +::: details Locked out? Create a user without authentication +If you don't have a user with a hashed password yet, comment the +`$this->loadComponent('Authentication.Authentication')` line in your +AppController and all other lines where Authentication is used. Then go to +`/users/add` to create a new user picking email and password. Afterward, +make sure to uncomment the lines we just temporarily commented! +::: Try it out by visiting `/articles/add` before logging in! Since this action is not allowed, you will be redirected to the login page. After logging in @@ -297,13 +322,8 @@ Add the logout action to the `UsersController` class: // in src/Controller/UsersController.php public function logout() { - $result = $this->Authentication->getResult(); - // regardless of POST or GET, redirect if user is logged in - if ($result && $result->isValid()) { - $this->Authentication->logout(); - - return $this->redirect(['controller' => 'Users', 'action' => 'login']); - } + $this->Authentication->logout(); + return $this->redirect(['controller' => 'Users', 'action' => 'login']); } ``` @@ -314,11 +334,11 @@ page. If you try to visit **/users/add** without being logged in, you will be redirected to the login page. We should fix that as we want to allow people to -sign up for our application. In the `UsersController` fix the following line: +sign up for our application. In the `UsersController` update the `beforeFilter`: -```php -// Add to the beforeFilter method of UsersController -$this->Authentication->addUnauthenticatedActions(['login', 'add']); +```php {3} +// In UsersController::beforeFilter() +$this->Authentication->allowUnauthenticated(['login', 'add']); ``` The above tells `AuthenticationComponent` that the `add()` action of the