CakeDC / users

Users Plugin for CakePHP
https://www.cakedc.com
Other
521 stars 296 forks source link

Two Factor Authenticator Issue #916

Closed MobileSkys closed 3 years ago

MobileSkys commented 3 years ago

Versions

Users plugin: 9.0.4 CakePHP: 4.1.5 PHP: 7.4.9

The problem

Trying to change OneTimePasswordAuthenticator.login to TRUE via Application::bootstrap(). I'm using the new release of the CakeDC Users plugin, and followed the documentation to enable one-time password authenticator OneTimePasswordAuthenticator.login appears to be loading the wrong config data.

The 'OneTimePasswordAuthenticator.login' config data when changed, seems to be ignored. Considered solution outlined in #912 without success.

Investigation

users.php

This works as expected when changed manually:

'OneTimePasswordAuthenticator' => [ 
       'checker' => \CakeDC\Auth\Authentication\DefaultOneTimePasswordAuthenticationChecker::class,
        'login' => true, <------ CHANGED THIS VALUE MANUALLY AND WORKS AS EXPECTED
       'issuer' => null,
       // The number of digits the resulting codes will be
        'digits' => 6,
       // The number of seconds a code will be valid
        'period' => 30,
        // The algorithm used
        'algorithm' => 'sha1',
        // QR-code provider (more on this later)
        'qrcodeprovider' => null,
        // Random Number Generator provider (more on this later)
       'rngprovider' => null
   ],

However when using Application::bootstrap() to alter value ... it seems to be ignored.

Application::bootstrap()

   public function bootstrap(): void
    {

        // Call parent to load bootstrap from files.`
        parent::bootstrap();`

        if (PHP_SAPI === 'cli') {`
            $this->bootstrapCli();`
       }`

       /*
        * Only try to load DebugKit in development mode
        * Debug Kit should not be installed on a production system
        */
        if (Configure::read('debug')) {
           Configure::write('DebugKit.panels', ['DebugKit.Packages' => true]);
            Configure::write('DebugKit.includeSchemaReflection', true);
            $this->addPlugin('DebugKit');
        }

       // Load more plugins here

        //CakeDC Users
        Configure::write('Users.config', ['users']);
        $this->addPlugin(\CakeDC\Users\Plugin::class);
        Configure::write('OneTimePasswordAuthenticator.login', true); <------ ADDED THIS LINE BEFORE PLUGIN LOAD WITH NO SUCCESS.

    }

Checking OneTimePasswordAuthenticator.login value in various locations throughout application code as follows:

dump(Configure::read('OneTimePasswordAuthenticator.login')); <------ ALWAYS RETURNS TRUE AS EXPECTED

always returns as expected = TRUE

However when a user is successfully logged in the OneTimePasswordAuthenticator.login is being ignored and treated as FALSE

Observation

Documentation seems to be out of step with the latest code version 9.0.4.

rochamarcelo commented 3 years ago

@MobileSkys I recommend you to set your config in a custom config/users.php file. If you still want to use the code Configure::write('OneTimePasswordAuthenticator.login', true), it should be after the plugin bootstrap (Application::pluginBootstrap)

MobileSkys commented 3 years ago

Many thanks @rochamarcelo for your response.

Confirmed

Your First Option -> "I recommend you to set your config in a custom config/users.php file": Understand this option and confirm this does work. :white_check_mark:

However

Your Second Option -> "should be after the plugin bootstrap": This is our preferred option, as our client requires more control of their application with the functionality to turn 2FA on or off. To achieve this functionality, a boolean value in stored in a database and is maintained via application settings.

We cannot get this option to work Regardless if the database value is used or is set manually (as below) the value seems to be ignored when a user logs-in. :x:

Example

Here is an example of how we have tried to achieve the desired result: Using the following code anywhere in the application (for example AppController.php), it updates Users.php correctly. :white_check_mark:

 Configure::write('OneTimePasswordAuthenticator.login', true);

Image below confirms that Users.php is correct prior to login:

<h2>2Factor = <?= Configure::read('OneTimePasswordAuthenticator.login') ?></h2>

Image of login

Result

After the user adds credentials and logs-in 2FA is ignored. :question:

rochamarcelo commented 3 years ago

@MobileSkys for this user case you will need to implement a custom 'checker' logic.

First create a checker based on the default one (\CakeDC\Auth\Authentication\DefaultOneTimePasswordAuthenticationChecker) and overwrite the method isRequired or isEnabled, after this you should update users.php with

'OneTimePasswordAuthenticator.checker' => \App\Authentication\AppOneTimePasswordAuthenticationChecker::class,
'OneTimePasswordAuthenticator.login' => true,

Let me know if this works for you.

MobileSkys commented 3 years ago

Many thanks @rochamarcelo for your help.

Solution

This solution uses a dynamic value returned from a database table to switch Two Factor Authentication either ON/OFF.

Create a new file \App\Authentication\AppOneTimePasswordAuthenticationChecker.php as follows

AppOneTimePasswordAuthenticationChecker.php

<?php
declare(strict_types=1);

/**
 * 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 - 2019, Cake Development Corporation (https://www.cakedc.com)
 * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
 */
namespace App\Authentication;

use Cake\Core\Configure;
use Cake\ORM\TableRegistry;

/**
 * Default class to check if two factor authentication is enabled and required
 *
 * @package App\Authentication
 */
class OneTimePasswordAuthenticationChecker implements \CakeDC\Auth\Authentication\OneTimePasswordAuthenticationCheckerInterface
{
    /**
     * @var string
     */
    protected $enabledKey = 'OneTimePasswordAuthenticator.login';

    /**
     * DefaultTwoFactorAuthenticationChecker constructor.
     *
     * @param string $enableKey configuration key to check if enabled
     */
    public function __construct($enableKey = null)
    {
        if ($enableKey !== null) {
            $this->enabledKey = $enableKey;
        }
    }

    /**
     * Check if two factor authentication is enabled
     *
     * @return bool
     */
    public function isEnabled()
    {
        return Configure::read($this->enabledKey) !== false;
    }

    /**
     * Check if two factor authentication is enabled in database
     *
     * @return bool
     */
    public function checkDatabase()
    {
        $table = TableRegistry::getTableLocator()->get('Settings');
        $two_factor_auth = $table->findByItem('2FA')->toArray();

        define('SECURITY_2FA', (boolean)$two_factor_auth[0]['value']);

        return SECURITY_2FA;
    }

    /**
     * Check if two factor authentication is required for a user
     *
     * @param array $user user data
     *
     * @return bool
     */
    public function isRequired(?array $user = null)
    {
        return !empty($user) && $this->checkDatabase();
    }
}

Configure Application.php as follows

Application.php

   public function bootstrap(): void
    {

        // Call parent to load bootstrap from files.`
        parent::bootstrap();`

        if (PHP_SAPI === 'cli') {`
            $this->bootstrapCli();`
       }`

       /*
        * Only try to load DebugKit in development mode
        * Debug Kit should not be installed on a production system
        */
        if (Configure::read('debug')) {
           Configure::write('DebugKit.panels', ['DebugKit.Packages' => true]);
            Configure::write('DebugKit.includeSchemaReflection', true);
            $this->addPlugin('DebugKit');
        }

       // Load more plugins here

        //CakeDC Users
        Configure::write('Users.config', ['users']);
        $this->addPlugin(\CakeDC\Users\Plugin::class);

    }

Configure users.php as follows

users.php


'OneTimePasswordAuthenticator' => [ 
       'checker' =>\App\Authentication\OneTimePasswordAuthenticationChecker::class,
        'login' => true, 
       'issuer' => null,
       // The number of digits the resulting codes will be
        'digits' => 6,
       // The number of seconds a code will be valid
        'period' => 30,
        // The algorithm used
        'algorithm' => 'sha1',
        // QR-code provider (more on this later)
        'qrcodeprovider' => null,
        // Random Number Generator provider (more on this later)
       'rngprovider' => null
   ],

Just for completeness of this solution here is an example of a input form, edit or create your own:

src\templates\Settings\update.php

echo $this->Form->create($settings);
    echo $this->Form->hidden('id');
    echo $this->Form->hidden('item', ['value' => '2FA']);

echo $this->Form->checkbox('value', ['checked' => SECURITY_2FA]); //Value set in AppOneTimePasswordAuthenticationChecker.php Above

echo $this->Form->button('Update');
echo $this->Form->end();