Code The Pixel

CakePHP 4 Auth Using CakeDC User Plugin

Asyraf Wahi Anuar - November 26, 2021
Published in CakePHP 2177 Views Featured Email This Article
Estimated reading time: 7 minutes, 18 seconds

Authentication allows a web application developer or administrator to keep their web application content safe by allowing only authenticated users or processes to have access to their protected resources, whereas authorization allows a specified person or position to access a specific resource. Authentication and authorization are critical components in the protection of online application content. They validate the user's identification and give access to the resources. In CakePHP, there are several plugins available that enable the developer to implement authentication and authorization. In this tutorial, the CakeDC user plugin is used to implement the authentication and simple authorization procedure. Generally, the CakeDC user plugin features are:

- User registration
- Login/logout
- Social login (Facebook, Twitter, Instagram, Google, Linkedin, etc)
- Simple RBAC via https://github.com/CakeDC/auth
- Remember me (Cookie) via https://github.com/CakeDC/auth
- Manage user's profile
- Admin management
- Yubico U2F for Two-Factor Authentication
- One-Time Password for Two-Factor Authentication

Requirements
- CakePHP 4.0+
- PHP 7.2+

Download Plugin
Download using composer:

composer require cakedc/users


Load the plugin in ...src/Application.php

$this->addPlugin(\CakeDC\Users\Plugin::class);


Database Table Migration

bin/cake migrations migrate -p CakeDC/Users


or use the following schema to create a user table:

CREATE TABLE `users` (
  `id` char(36) NOT NULL,
  `username` varchar(255) NOT NULL,
  `email` varchar(255) DEFAULT NULL,
  `password` varchar(255) NOT NULL,
  `first_name` varchar(50) DEFAULT NULL,
  `last_name` varchar(50) DEFAULT NULL,
  `token` varchar(255) DEFAULT NULL,
  `token_expires` datetime DEFAULT NULL,
  `api_token` varchar(255) DEFAULT NULL,
  `activation_date` datetime DEFAULT NULL,
  `secret` varchar(32) DEFAULT NULL,
  `secret_verified` tinyint(1) DEFAULT NULL,
  `tos_date` datetime DEFAULT NULL,
  `active` tinyint(1) NOT NULL DEFAULT 0,
  `is_superuser` tinyint(1) NOT NULL DEFAULT 0,
  `role` varchar(255) DEFAULT 'user',
  `created` datetime NOT NULL,
  `modified` datetime NOT NULL,
  `additional_data` text DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


Populate superUser account:

bin/cake users addSuperuser


Authentication
At this point, when you refresh your web application page, it will redirect to the login page as shown below:

Tutorial
You can authenticate using superadmin account.

Redirect: After Login
To customize the after login redirect (eg redirect to users-index), create <link>custom event</link> UsersListener.php at ...src/Event/UsersListener.php with the following code:

<?php

namespace App\Event;

use Cake\Datasource\ModelAwareTrait;
use Cake\Event\EventListenerInterface;

class UsersListener implements EventListenerInterface
{
    use ModelAwareTrait;

    /**
     * @return string[]
     */
    public function implementedEvents(): array
    {
        return [
            \CakeDC\Users\Plugin::EVENT_AFTER_LOGIN => 'afterLogin',
        ];
    }

    /**
     * @param \Cake\Event\Event $event
     */
    public function afterLogin(\Cake\Event\Event $event)
    {
        $user = $event->getData('user');
        //your custom logic
        //$this->loadModel('SomeOptionalUserLogs')->newLogin($user);

        //If you want to use a custom redirect
        $event->setResult([
            'plugin' => false,
            'controller' => 'Users',
            'action' => 'index',
        ]);
    }
}


Then, in ...config/bootstrap.php add the following code at the end of the method (click here to check more events):

$this->getEventManager()->on(new \App\Event\UsersListener());


Extending The Templates
Use the standard CakePHP conventions to override Plugin views using your application views. Create your own design login, register, change_password, edit, index, profile, request_reset_password, resend_token_validation, verify, view and other user-related pages inside this folder.

{project_dir}/templates/plugin/CakeDC/Users/Users/{templates_in_here}


Permission Management
Copy-paste the permission file using the following console command:

cp vendor/cakedc/users/config/permissions.php  config/permissions.php


or you can create permission.php (new file) in ...config/permission.php and add the following code:

<?php
/**
 * Copyright 2010 - 2019, Cake Development Corporation (https://www.cakedc.com)
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright Copyright 2010 - 2018, Cake Development Corporation (https://www.cakedc.com)
 * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
 */

/*
 * IMPORTANT:
 * This is an example configuration file. Copy this file into your config directory and edit to
 * setup your app permissions.
 *
 * This is a quick roles-permissions implementation
 * Rules are evaluated top-down, first matching rule will apply
 * Each line define
 *      [
 *          'role' => 'role' | ['roles'] | '*'
 *          'prefix' => 'Prefix' | , (default = null)
 *          'plugin' => 'Plugin' | , (default = null)
 *          'controller' => 'Controller' | ['Controllers'] | '*',
 *          'action' => 'action' | ['actions'] | '*',
 *          'allowed' => true | false | callback (default = true)
 *      ]
 * You could use '*' to match anything
 * 'allowed' will be considered true if not defined. It allows a callable to manage complex
 * permissions, like this
 * 'allowed' => function (array $user, $role, Request $request) {}
 *
 * Example, using allowed callable to define permissions only for the owner of the Posts to edit/delete
 *
 * (remember to add the 'uses' at the top of the permissions.php file for Hash, TableRegistry and Request
   [
        'role' => ['user'],
        'controller' => ['Posts'],
        'action' => ['edit', 'delete'],
        'allowed' => function(array $user, $role, Request $request) {
            $postId = Hash::get($request->params, 'pass.0');
            $post = TableRegistry::getTableLocator()->get('Posts')->get($postId);
            $userId = Hash::get($user, 'id');
            if (!empty($post->user_id) && !empty($userId)) {
                return $post->user_id === $userId;
            }
            return false;
        }
    ],
 */

return [
    'CakeDC/Auth.permissions' => [
        //all bypass
        [
            'prefix' => false,
            'plugin' => 'CakeDC/Users',
            'controller' => 'Users',
            'action' => [
                // LoginTrait
                'socialLogin',
                'login',
                'logout',
                'socialEmail',
                'verify',
                // RegisterTrait
                'register',
                'validateEmail',
                // PasswordManagementTrait used in RegisterTrait
                'changePassword',
                'resetPassword',
                'requestResetPassword',
                // UserValidationTrait used in PasswordManagementTrait
                'resendTokenValidation',
                'linkSocial',
                //U2F actions
                'u2f',
                'u2fRegister',
                'u2fRegisterFinish',
                'u2fAuthenticate',
                'u2fAuthenticateFinish',
            ],
            'bypassAuth' => true,
        ],
        [
            'prefix' => false,
            'plugin' => 'CakeDC/Users',
            'controller' => 'SocialAccounts',
            'action' => [
                'validateAccount',
                'resendValidation',
            ],
            'bypassAuth' => true,
        ],
        //admin role allowed to all the things
        [
            'role' => 'admin',
            'prefix' => '*',
            'extension' => '*',
            'plugin' => '*',
            'controller' => '*',
            'action' => '*',
        ],
        //specific actions allowed for the all roles in Users plugin
        [
            'role' => '*',
            'plugin' => 'CakeDC/Users',
            'controller' => 'Users',
            'action' => ['profile', 'logout', 'linkSocial', 'callbackLinkSocial'],
        ],
        [
            'role' => '*',
            'plugin' => 'CakeDC/Users',
            'controller' => 'Users',
            'action' => 'resetOneTimePasswordAuthenticator',
            'allowed' => function (array $user, $role, \Cake\Http\ServerRequest $request) {
                $userId = \Cake\Utility\Hash::get($request->getAttribute('params'), 'pass.0');
                if (!empty($userId) && !empty($user)) {
                    return $userId === $user['id'];
                }

                return false;
            }
        ],
        //all roles allowed to Pages/display
        [
            'role' => '*',
            'controller' => 'Pages',
            'action' => 'display',
        ],
        [
            'role' => '*',
            'plugin' => 'DebugKit',
            'controller' => '*',
            'action' => '*',
            'bypassAuth' => true,
        ],       
    ]
];


Allow Unauthenticated Access (non-logged user)
To grant access to public operations that do not require authentication, the developer must add a new rule to .../config/permissions.php that uses the 'bypassAuth' key as shown below (allow public access to articles index, view and archive):

<?php
return [
    'CakeDC/Auth.permissions' => [
        //...... all other rules
        [
            'controller' => 'Articles',
            'action' => ['index', 'view', 'archive']
            'bypassAuth' => true,
        ],
    ],
];


Helper
To print current authenticated info, load the CakeDC helper at ...src/View/Appview.php

public function initialize(): void
{
    $this->loadHelper('CakeDC/Users.User');
}


Print welcome message with a link to profile:

<?php echo $this->User->welcome(); ?>


or use the following code without helper:

<?php echo $this->request->getAttribute('identity')->username; ?>


Logged in simple conditional check:

<?php
if($this->request->getAttribute('identity')) {
  echo 'User Logged in';
} else {
  echo 'User NOT Logged in';
}
?>


To capture the current authenticated user ID in the controller, use the following code (before save method) in the controller:

$article->user_id = $this->request->getAttribute('identity')->id; //capture id


Logout:

$this->User->logout();


AuthLinkHelper
The AuthLink Helper has certain methods that may be necessary if you want to easily update your templates and add functionality to your app. This utility provides two methods for hiding or showing links and postLinks dependent on the permissions file. There will be no more permissions checks in your views. You do not need to recreate the permissions logic in the views if the permissions file is modified.

Enable Authlink Helper in ...src/view/AppView.php:

class AppView extends View
{
    public function initialize()
    {
        parent::initialize();
        $this->loadHelper('CakeDC/Users.AuthLink');
    }
}


In your template, use the following code:

echo $this->AuthLink->link(__d('cake_d_c/users', 'List Users'), ['action' => 'index']);

echo $this->AuthLink->postLink(__d('cake_d_c/users', 'Delete'), ['action' => 'delete', $user->id], ['confirm' => __d('cake_d_c/users', 'Are you sure you want to delete # {0}?', $user->id)]);

echo $this->AuthLink->link(__d('cake_d_c/users', 'List Users'), ['action' => 'index', 'before' => '<i class="fas fa-list"></i>']);


Considering this plugin provides lots of authentication features including the social login, I recommended this plugin to be implemented in CakePHP based web application. There are still lots of features and functions that I've not mentioned in this tutorial since this tutorial is generally aimed to cover the basic authentication procedure only. Feel free to visit the CakeDC User plugin page to get more information.

That's all, happy coding :)


Cite this article (APA 6th Edition)