The Yeebase.TwoFactorAuthentication Flow package contains extensions to the Flow authentication mechanism that let you implement Two-Factor-Authentication (2FA) easily.
It provides a new Authentication Provider that can be used in addition to existing providers in order to enable 2FA via One-time Passwords (OTP).
This package can be installed via composer:
composer require yeebase/twofactorauthentication
This package requires a new database table yeebase_twofactorauthentication_secret that can be added via:
./flow doctrine:migrate
The following part describes the integration of the Two-Factor-Authentication package into an existing Flow Application. After installation Two-Factor-Authentication is considered to be disabled for all accounts in the system.
This package provides a TwoFactorAuthenticationProvider that has to be configured in addition to already existing providers.
Furthermore the authenticationStrategy has to be set to allTokens in order to make sure that both providers are taken into account.
Settings.yaml:
Neos:
Flow:
security:
authentication:
authenticationStrategy: 'allTokens'providers:
'Some.Package:Default':
# That assumes that the "PersistedUsernamePasswordProvider" is used as base authentication:provider: 'PersistedUsernamePasswordProvider''Some.Package:2FA':
provider: 'Yeebase\TwoFactorAuthentication\Security\Authentication\Provider\TwoFactorAuthenticationProvider'If a TwoFactorAuthenticationProvider
Settings.yaml:
Yeebase:
TwoFactorAuthentication:
# This is the "issuer" that will be displayed in the authenticator app like: <issuer> (<holder>)applicationName: 'Some Application'routes:
login:
'@package': 'Some.Package''@controller': 'Login''@action': 'twoFactor'Login/TwoFacor.html
...
<f:formaction="authenticate"><divclass="form-group"><labelfor="otp">2FA Code</label><f:form.textfieldname="__authentication[Yeebase][TwoFactorAuthentication][Security][Authentication][Token][OtpToken][otp]" id="otp" additionalAttributes="{autofocus: true, autocomplete: 'off'}" /></div><f:form.submitvalue="Enter" /></f:form>
...Instead of using the default UsernamePasswordProvider, adapt your settings to use the following provider instead: Yeebase\TwoFactorAuthentication\Security\Authentication\Provider\TwoFactorAuthenticationProvider
By default 2FA can be enabled per account and it is not required if it is not enabled for the account that is authenticated.
In order to require users to log in with Two-Factor Authentication the Yeebase.TwoFactorAuthentication.requireTwoFactorAuthentication flag can be set.
With that in place the One-time Password has to be specified whenever an account is authenticated.
To avoid this to leading to an exception when 2FA is not yet enabled for the given account, a setup can be configured that allows the user to initialize the 2FA.
Settings.yaml:
Yeebase:
TwoFactorAuthentication:
requireTwoFactorAuthentication: trueroutes:
# ...setup:
'@package': 'Some.Package''@controller': 'TwoFactorAuthenticationSetup''@action': 'index'And the corresponding Setup Controller (example):
TwoFactorAuthenticationSetupController.php
<?phpdeclare(strict_types=1);
namespaceSome\Package\Controller;
useNeos\Error\Messages\Message;
useNeos\Flow\AnnotationsasFlow;
useNeos\Flow\Mvc\Controller\ActionController;
useNeos\Flow\Security\Account;
useNeos\Flow\Security\Context;
useNeos\Flow\Security\Exception\AccessDeniedException;
useYeebase\TwoFactorAuthentication\Domain\ValueObjects\OneTimePassword;
useYeebase\TwoFactorAuthentication\Domain\ValueObjects\SecretWithHmac;
useYeebase\TwoFactorAuthentication\Exception\InvalidOtpException;
useYeebase\TwoFactorAuthentication\Service\TwoFactorAuthenticationService;
class TwoFactorAuthenticationSetupController extends ActionController
{
/** * @var Account */private$authenticatedAccount;
/** * @Flow\Inject * @var Context */protected$securityContext;
/** * @Flow\Inject * @var TwoFactorAuthenticationService */protected$twoFactorAuthenticationService;
protectedfunctioninitializeAction(): void
{
parent::initializeAction();
$this->authenticatedAccount = $this->securityContext->getAccountByAuthenticationProviderName('Some.Package:Default');
if ($this->authenticatedAccount === null) {
thrownewAccessDeniedException('...');
}
}
publicfunctionindexAction(): void
{
$twoFactorAuthenticationEnabled = $this->twoFactorAuthenticationService->isTwoFactorAuthenticationEnabledFor($this->authenticatedAccount);
$this->view->assign('2faEnabled', $twoFactorAuthenticationEnabled);
if (!$twoFactorAuthenticationEnabled) {
$holder = $this->authenticatedAccount->getAccountIdentifier();
$qrCode = $this->twoFactorAuthenticationService->generateActivationQrCode($holder);
$this->view->assignMultiple([
'secretWithHmac' => SecretWithHmac::fromSecret($qrCode->getSecret()),
'qrCode' => $qrCode->renderSvg(200),
]);
}
}
publicfunctionenableAction(SecretWithHmac$secretWithHmac, OneTimePassword$otp): void
{
try {
$this->twoFactorAuthenticationService->enableTwoFactorAuthentication($this->authenticatedAccount, $secretWithHmac->getSecret(), $otp);
} catch (InvalidOtpException$exception) {
$this->addFlashMessage('Invalid One-time Password', 'Invalid OTP', Message::SEVERITY_ERROR);
$this->redirect('index');
}
$this->addFlashMessage('Two-Factor-Authentication was activated!', '2FA enabled', Message::SEVERITY_OK);
$this->redirect('index');
}
publicfunctiondisableAction(): void
{
$this->twoFactorAuthenticationService->disableTwoFactorAuthentication($this->authenticatedAccount);
$this->addFlashMessage('Two-Factor-Authentication was deactivated!', '2FA disabled', Message::SEVERITY_NOTICE);
$this->redirect('index');
}
}And the corresponding Template (example):
TwoFactorAuthenticationSetup/Index.html:
<h2>Two-Factor Authentication</h2><f:ifcondition="{2faEnabled}"><f:then><ul><li>2FA is active</li></ul><f:formaction="disable"><f:form.submitvalue="disable 2FA" /></f:form></f:then><f:else><ul><li>2FA is not active</li></ul><f:formaction="enable"><div>
{qrCode -> f:format.raw()}
</div><labelfor="otp">2FA Code</label><f:form.hiddenname="secretWithHmac" value="{secretWithHmac}" /><f:form.textfieldname="otp" id="otp" additionalAttributes="{autofocus: true, pattern: '\d\d\d\d\d\d'}" required="true" title="OTP (Format: ######)" /><f:form.submitvalue="enable 2FA" /></f:form></f:else></f:if>In order to allow the user to setup 2FA initially, the corresponding actions have to be allowed to be called even if no 2FA is enabled for the account yet. This can be achieved with
the provided ExcludeTwoFactorAuthenticationSetupRequest Pattern that
disables the 2FA authentication provider for the setup route configured above:
Settings.yaml:
Neos:
Flow:
security:
authentication:
providers:
# ...'Some.Package:2FA':
requestPatterns:
'Some.Package:2FASetup':
pattern: 'Yeebase\TwoFactorAuthentication\Security\RequestPattern\ExcludeTwoFactorAuthenticationSetup'Note: The ExcludeTwoFactorAuthenticationSetup will disable 2FA for all actions of the configured controller, so the controller should not do any critical tasks without
further checks.
This package is licensed under the MIT License - see the LICENSE file for details.
This package depends on the google2fa package for generating and validating secrets/OTP and the BaconQrCode for QR Code rendering