CMS Tutorial - Authentication

Now that our CMS has users, we should enable them to login, and apply some basicaccess control to the article creation & editing experiences.

Adding Password Hashing

If you were to create/update a user at this point in time, you might notice thatthe passwords are stored in plain text. This is really bad from a security pointof view, so lets fix that.

This is also a good time to talk about the model layer in CakePHP. In CakePHP,we separate the methods that operate on a collection of objects, and a singleobject into different classes. Methods that operate on the collection ofentities are put in the Table class, while features belonging to a singlerecord are put on the Entity class.

For example, password hashing is done on the individual record, so we’llimplement this behavior on the entity object. Because we want to hash thepassword each time it is set, we’ll use a mutator/setter method. CakePHP willcall convention based setter methods any time a property is set in one of yourentities. Let’s add a setter for the password. In src/Model/Entity/User.phpadd the following:

  1. <?php
  2. namespace App\Model\Entity;
  3.  
  4. use Cake\Auth\DefaultPasswordHasher; // Add this line
  5. use Cake\ORM\Entity;
  6.  
  7. class User extends Entity
  8. {
  9. // Code from bake.
  10.  
  11. // Add this method
  12. protected function _setPassword($value)
  13. {
  14. if (strlen($value)) {
  15. $hasher = new DefaultPasswordHasher();
  16.  
  17. return $hasher->hash($value);
  18. }
  19. }
  20. }

Now, point your browser to http://localhost:8765/users to see a list of users.You can edit the default user that was created duringInstallation. If you change that user’s password,you should see a hashed password instead of the original value on the list orview pages. CakePHP hashes passwords with bcrypt by default. You can alsouse SHA-1 or MD5 if you’re working with an existing database, but we recommendbcrypt for all new applications.

Note

Create a hashed password for at least one of the user accounts now!It will be needed in the next steps.

Adding Login

In CakePHP, authentication is handled by Components.Components can be thought of as ways to create reusable chunks of controllercode related to a specific feature or concept. Components can hook into thecontroller’s event life-cycle and interact with your application that way. Toget started, we’ll add the AuthComponent to our application. We’ll want thecreate, update and delete methods to require authentication, so we’ll addAuthComponent in our AppController:

  1. // In src/Controller/AppController.php
  2. namespace App\Controller;
  3.  
  4. use Cake\Controller\Controller;
  5.  
  6. class AppController extends Controller
  7. {
  8. public function initialize(): void
  9. {
  10. // Existing code
  11.  
  12. $this->loadComponent('Auth', [
  13. 'authenticate' => [
  14. 'Form' => [
  15. 'fields' => [
  16. 'username' => 'email',
  17. 'password' => 'password'
  18. ]
  19. ]
  20. ],
  21. 'loginAction' => [
  22. 'controller' => 'Users',
  23. 'action' => 'login'
  24. ],
  25. // If unauthorized, return them to page they were just on
  26. 'unauthorizedRedirect' => $this->referer()
  27. ]);
  28.  
  29. // Allow the display action so our PagesController
  30. // continues to work. Also enable the read only actions.
  31. $this->Auth->allow(['display', 'view', 'index']);
  32. }
  33.  
  34. }

We’ve just told CakePHP that we want to load the Authcomponent. We’ve customized the configuration of AuthComponent, asour users table uses email as the username. Now, if you go any protectedURL, such as /articles/add, you’ll be redirected to /users/login, whichwill show an error page as we have not written that code yet. So let’s createthe login action:

  1. // In src/Controller/UsersController.php
  2. public function login()
  3. {
  4. if ($this->request->is('post')) {
  5. $user = $this->Auth->identify();
  6. if ($user) {
  7. $this->Auth->setUser($user);
  8. return $this->redirect($this->Auth->redirectUrl());
  9. }
  10. $this->Flash->error('Your username or password is incorrect.');
  11. }
  12. }

Create a new template templates/Users/login.php and add the following:

  1. <h1>Login</h1>
  2. <?= $this->Form->create() ?>
  3. <?= $this->Form->control('email') ?>
  4. <?= $this->Form->control('password') ?>
  5. <?= $this->Form->button('Login') ?>
  6. <?= $this->Form->end() ?>

Now that we have a simple login form, we should be able to log in with one ofthe users that has a hashed password.

Note

If none of your users have hashed passwords, comment theloadComponent('Auth') block and $this->Auth->allow() calls. Then goand edit the user, saving a new password for them. After saving a newpassword for the user, make sure to uncomment the lines we just temporarilycommented!

Try it out! Before logging in, visit /articles/add. Since this action is notallowed, you will be redirected to the login page. After logging insuccessfully, CakePHP will automatically redirect you back to /articles/add.

Adding Logout

Now that people can log in, you’ll probably want to provide a way to log out aswell. Again, in the UsersController, add the following code:

  1. public function initialize(): void
  2. {
  3. parent::initialize();
  4. $this->Auth->allow(['logout']);
  5. }
  6.  
  7. public function logout()
  8. {
  9. $this->Flash->success('You are now logged out.');
  10. return $this->redirect($this->Auth->logout());
  11. }

This code adds the logout action to the list of actions that do not requireauthentication and implements the logout method. Now you can visit/users/logout to log out. You should then be sent to the login page.

Enabling Registrations

If you aren’t logged in and you try to visit /users/add you will beredirected to the login page. We should fix that as we want to allow people tosign up for our application. In the UsersController add the following:

  1. public function initialize(): void
  2. {
  3. parent::initialize();
  4. // Add the 'add' action to the allowed actions list.
  5. $this->Auth->allow(['logout', 'add']);
  6. }

The above tells AuthComponent that the add() action of theUsersController does not require authentication or authorization. You maywant to take the time to clean up the Users/add.php and remove themisleading links, or continue on to the next section. We won’t be building outuser editing, viewing or listing in this tutorial, but that is an exercise youcan complete on your own.

Restricting Article Access

Now that users can log in, we’ll want to limit users to only edit articles thatthey created. We’ll do this using an ‘authorization’ adapter. Since ourrequirements are basic, we can use a controller hook method in ourArticlesController. But before we do that, we’ll want to tell theAuthComponent how our application is going to authorize actions. Update yourAppController adding the following:

  1. public function isAuthorized($user)
  2. {
  3. // By default deny access.
  4. return false;
  5. }

Next we’ll tell AuthComponent that we want to use controller hook methodsfor authorization. Your AppController::initialize() method should now looklike:

  1. public function initialize(): void
  2. {
  3. // Existing code
  4.  
  5. $this->loadComponent('Flash');
  6. $this->loadComponent('Auth', [
  7. // Added this line
  8. 'authorize'=> 'Controller',
  9. 'authenticate' => [
  10. 'Form' => [
  11. 'fields' => [
  12. 'username' => 'email',
  13. 'password' => 'password'
  14. ]
  15. ]
  16. ],
  17. 'loginAction' => [
  18. 'controller' => 'Users',
  19. 'action' => 'login'
  20. ],
  21. // If unauthorized, return them to page they were just on
  22. 'unauthorizedRedirect' => $this->referer()
  23. ]);
  24.  
  25. // Allow the display action so our pages controller
  26. // continues to work. Also enable the read only actions.
  27. $this->Auth->allow(['display', 'view', 'index']);
  28. }

We’ll default to denying access, and incrementally grant access where it makessense. First, we’ll add the authorization logic for articles. In yourArticlesController add the following:

  1. public function isAuthorized($user)
  2. {
  3. $action = $this->request->getParam('action');
  4. // The add and tags actions are always allowed to logged in users.
  5. if (in_array($action, ['add', 'tags'])) {
  6. return true;
  7. }
  8.  
  9. // All other actions require a slug.
  10. $slug = $this->request->getParam('pass.0');
  11. if (!$slug) {
  12. return false;
  13. }
  14.  
  15. // Check that the article belongs to the current user.
  16. $article = $this->Articles->findBySlug($slug)->first();
  17.  
  18. return $article->user_id === $user['id'];
  19. }

Now if you try to edit or delete an article that does not belong to you,you should be redirected back to the page you came from. If no error message isdisplayed, add the following to your layout:

  1. // In templates/Layout/default.php
  2. <?= $this->Flash->render() ?>

Next you should add the tags action to the actions allowed forunauthenticated users, by adding the following to initialize() insrc/Controller/ArticlesController.php:

  1. $this->Auth->allow(['tags']);

While the above is fairly simplistic it illustrates how you could build morecomplex logic that combines the current user and request data to build flexibleauthorization logic.

Fixing the Add & Edit Actions

While we’ve blocked access to the edit action, we’re still open to userschanging the user_id attribute of articles during edit. Wewill solve these problems next. First up is the add action.

When creating articles, we want to fix the user_id to be the currentlylogged in user. Replace your add action with the following:

  1. // in src/Controller/ArticlesController.php
  2.  
  3. public function add()
  4. {
  5. $article = $this->Articles->newEmptyEntity();
  6. if ($this->request->is('post')) {
  7. $article = $this->Articles->patchEntity($article, $this->request->getData());
  8.  
  9. // Changed: Set the user_id from the session.
  10. $article->user_id = $this->Auth->user('id');
  11.  
  12. if ($this->Articles->save($article)) {
  13. $this->Flash->success(__('Your article has been saved.'));
  14. return $this->redirect(['action' => 'index']);
  15. }
  16. $this->Flash->error(__('Unable to add your article.'));
  17. }
  18. $this->set('article', $article);
  19. }

Next we’ll update the edit action. Replace the edit method with the following:

  1. // in src/Controller/ArticlesController.php
  2.  
  3. public function edit($slug)
  4. {
  5. $article = $this->Articles
  6. ->findBySlug($slug)
  7. ->contain('Tags') // load associated Tags
  8. ->firstOrFail();
  9.  
  10. if ($this->request->is(['post', 'put'])) {
  11. $this->Articles->patchEntity($article, $this->request->getData(), [
  12. // Added: Disable modification of user_id.
  13. 'accessibleFields' => ['user_id' => false]
  14. ]);
  15. if ($this->Articles->save($article)) {
  16. $this->Flash->success(__('Your article has been updated.'));
  17. return $this->redirect(['action' => 'index']);
  18. }
  19. $this->Flash->error(__('Unable to update your article.'));
  20. }
  21. $this->set('article', $article);
  22. }

Here we’re modifying which properties can be mass-assigned, via the optionsfor patchEntity(). See the Changing Accessible Fields section formore information. Remember to remove the user_id control fromtemplates/Articles/edit.php as we no longer need it.

Wrapping Up

We’ve built a simple CMS application that allows users to login, post articles,tag them, explore posted articles by tag, and applied basic access control toarticles. We’ve also added some nice UX improvements by leveraging theFormHelper and ORM capabilities.

Thank you for taking the time to explore CakePHP. Next, you should learn more aboutthe Database Access & ORM, or you peruse the Using CakePHP.