, (*1)
TwoFactorAuth plugin for CakePHP
This plugin provides two factor authentication functionality using RobThree/TwoFactorAuth library.
Basically, it works similar way CakePHP FormAuthenticate
does. After submitting correct username/password, if the user has secret
field set, he will be asked to enter a one-time code.
Attention: it only provides authenticate provider and component and does not take care of users signup, management etc., (*2)
Requirements
- CakePHP 5.0+ (use ^1.3 version for CakePHP <3.7, ^2.0 version for CakePHP 3.x, ^3.0 version for CakePHP 4.x)
Installation
You can install this plugin into your CakePHP application using Composer., (*3)
composer require andrej-griniuk/cakephp-two-factor-auth
Usage
First of all you need to add secret
field to your users table (field name can be changed to TwoFactorAuth.Form
authenticator configuration)., (*4)
ALTER TABLE `users` ADD `secret` VARCHAR(255) NULL;
Second, you need to load the plugin in your Application.php, (*5)
$this->addPlugin('TwoFactorAuth');
Alternatively, execute the following line:, (*6)
bin/cake plugin load TwoFactorAuth
You can see the default config values here and find out what do they mean here. To overwrite them, pass them as TwoFactorForm
authenticator values., (*7)
Then you need to set up authentication in your Application.php as you would normally do it, but using TwoFactorForm
authenticator instead of Form
, e.g.:, (*8)
class Application extends BaseApplication implements AuthenticationServiceProviderInterface
{
public function bootstrap(): void
{
// Call parent to load bootstrap from files.
parent::bootstrap();
$this->addPlugin('TwoFactorAuth');
$this->addPlugin('Authentication');
}
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
// Various other middlewares for error handling, routing etc. added here.
// Create an authentication middleware object
$authentication = new AuthenticationMiddleware($this);
// Add the middleware to the middleware queue.
// Authentication should be added *after* RoutingMiddleware.
// So that subdirectory information and routes are loaded.
$middlewareQueue->add($authentication);
return $middlewareQueue;
}
public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
{
$service = new AuthenticationService();
$service->setConfig([
'unauthenticatedRedirect' => '/users/login',
'queryParam' => 'redirect',
]);
$fields = [
'username' => 'username',
'password' => 'password'
];
// Load the authenticators, you want session first
$service->loadAuthenticator('Authentication.Session');
$service->loadAuthenticator('TwoFactorAuth.TwoFactorForm', [
'fields' => $fields,
'loginUrl' => '/users/login'
]);
// Load identifiers
$service->loadIdentifier('Authentication.Password', compact('fields'));
return $service;
}
}
Next, in your AppController load the Authentication
and TwoFactorAuth
components:, (*9)
// in src/Controller/AppController.php
public function initialize()
{
parent::initialize();
$this->loadComponent('Authentication.Authentication');
$this->loadComponent('TwoFactorAuth.TwoFactorAuth');
}
Once you have the middleware applied to your application you’ll need a way for users to login. A simplistic UsersController
would look like:, (*10)
class UsersController extends AppController
{
public function beforeFilter(\Cake\Event\EventInterface $event)
{
parent::beforeFilter($event);
$this->Authentication->allowUnauthenticated(['login', 'verify']);
}
public function login()
{
$result = $this->Authentication->getResult();
if ($result->isValid()) {
// If the user is logged in send them away.
$target = $this->Authentication->getLoginRedirect() ?? '/home';
return $this->redirect($target);
}
if ($this->request->is('post') && !$result->isValid()) {
if ($result->getStatus() == \TwoFactorAuth\Authenticator\Result::TWO_FACTOR_AUTH_FAILED) {
// One time code was entered and it's invalid
$this->Flash->error('Invalid 2FA code');
return $this->redirect(['action' => 'verify']);
} elseif ($result->getStatus() == \TwoFactorAuth\Authenticator\Result::TWO_FACTOR_AUTH_REQUIRED) {
// One time code is required and wasn't yet entered - redirect to the verify action
return $this->redirect(['action' => 'verify']);
} else {
$this->Flash->error('Invalid username or password');
}
}
}
public function logout()
{
$this->Authentication->logout();
return $this->redirect(['action' => 'login']);
}
public function verify()
{
// This action is only needed to render a vew with one time code form
}
}
And verify.php
would look like:, (*11)
<div class="users form content">
<?= $this->Form->create(null, ['url' => ['action' => 'login']]) ?>
<fieldset>
<legend><?= __('Please enter your 2FA code') ?></legend>
<?= $this->Form->control('code') ?>
</fieldset>
<?= $this->Form->button(__('Continue')); ?>
<?= $this->Form->end() ?>
</div>
Basically, it works same way CakePHP Authentication.Form
authenticator does.
After entering correct username/password combination, if the user has secret
field (can be overwritten via TwoFactorAuth.TwoFactorForm
configuration) set he will be redirected to the verify
action where he is asked to enter a one-time code.
There is no logic behind this action, it only renders the form that has to be submitted to the loginAction
again with code
field set., (*12)
You can access the RobThree\Auth\TwoFactorAuth instance from your controller via $this->TwoFactorAuth->getTfa()
or call some of the methods directly on TwoFactorAuth
component. For example, you can generate user's secret and get QR code data URI for it this way:, (*13)
$secret = $this->TwoFactorAuth->createSecret();
$secretDataUri = $this->TwoFactorAuth->getQRCodeImageAsDataUri('CakePHP:user@email.com', $secret);
Then display it in your view:, (*14)
<img src="<?= $secretDataUri ?>" />
See the library page for full documentation: https://github.com/RobThree/TwoFactorAuth, (*15)
Bugs & Feedback
https://github.com/andrej-griniuk/cakephp-two-factor-auth/issues, (*16)
Credits
https://github.com/RobThree/TwoFactorAuth, (*17)
License
Copyright (c) 2020, Andrej Griniuk and licensed under The MIT License., (*18)