Skip to content

Commit

Permalink
Added the 2FA code error in case if it's not correct.
Browse files Browse the repository at this point in the history
  • Loading branch information
Ruslan Baidan committed Nov 11, 2022
1 parent d3ffe47 commit 14affb0
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 92 deletions.
96 changes: 55 additions & 41 deletions src/Adapter/Authentication.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
<?php
/**
* @link https://github.com/monarc-project for the canonical source repository
* @copyright Copyright (c) 2016-2022 SMILE GIE Securitymadein.lu - Licensed under GNU Affero GPL v3
* @license MONARC is licensed under GNU Affero General Public License version 3
*/

namespace Monarc\Core\Adapter;

use Doctrine\ORM\EntityNotFoundException;
Expand All @@ -11,7 +17,6 @@

/**
* Class Authentication is an implementation of AbstractAdapter that takes care of authenticating an user.
* This is heavily inspired from Laminas Auth.
*
* @package Monarc\Core\Adapter
*/
Expand All @@ -26,8 +31,9 @@ class Authentication extends AbstractAdapter
/** @var ConfigService */
private $configService;

const TWO_FA_REQUIRED = 2;
const TWO_FA_TO_SET_UP = 3;
public const TWO_FA_REQUIRED = 2;
public const TWO_FA_TO_SET_UP = 3;
public const TWO_FA_FAILED = 4;

public function __construct(UserTable $userTable, ConfigService $configService)
{
Expand Down Expand Up @@ -55,10 +61,12 @@ public function getUser(): UserSuperClass

/**
* Authenticates the user from its identity and credential.
*
* @param string $token The OTP token submitted by the user or a recovery code.
*
* @return Result The authentication result
*/
public function authenticate($token = ''): Result
public function authenticate(string $token = ''): Result
{
$identity = $this->getIdentity();
$credential = $this->getCredential();
Expand All @@ -69,57 +77,63 @@ public function authenticate($token = ''): Result
}

if ($user->isActive()) {
if (password_verify($credential, $user->getPassword())) {
$this->setUser($user);

/* Validate if the user has 2FA enabled. */
if (!$user->isTwoFactorAuthEnabled()) {
/* Validate if 2FA is enforced on the platform. */
if ($this->configService->isTwoFactorAuthEnforced() && empty($token)) {
/* 2FA enforced and missing tokens in order to activate 2FA */
return new Result(self::TWO_FA_TO_SET_UP, $this->getIdentity());
} elseif ($this->configService->isTwoFactorAuthEnforced()) {
/* 2FA enforced and received tokens in order to activate 2FA */
/* tokens are the verification code and the otp secret */
$tokens = explode(":", $token);
// verify the submitted OTP token
$tfa = new TwoFactorAuth('MONARC TwoFactorAuth');
if ($tfa->verifyCode($tokens[0], $tokens[1])) {
$user->setSecretKey($tokens[0]);
$user->setTwoFactorAuthEnabled(true);
$this->userTable->saveEntity($user);
return new Result(Result::SUCCESS, $this->getIdentity());
}
return new Result(self::TWO_FA_TO_SET_UP, $this->getIdentity());
}
if (!password_verify($credential, $user->getPassword())) {
return new Result(Result::FAILURE_CREDENTIAL_INVALID, $this->getIdentity());
}
$this->setUser($user);

/* Validate if the user has 2FA enabled. */
if (!$user->isTwoFactorAuthEnabled()) {
/* Validate if 2FA is enforced on the platform. */
if (!$this->configService->isTwoFactorAuthEnforced()) {
return new Result(Result::SUCCESS, $this->getIdentity());
}

/* Validate if the 2FA token has been submitted. */
if (empty($token)) {
return new Result(static::TWO_FA_REQUIRED, $this->getIdentity());
/* 2FA enforced and missing tokens in order to activate 2FA */
return new Result(static::TWO_FA_TO_SET_UP, $this->getIdentity());
}

/* Verify the submitted OTP token. */
/* 2FA enforced and received tokens in order to activate 2FA */
/* tokens are the verification code and the otp secret */
$tokens = explode(":", $token);
// verify the submitted OTP token
$tfa = new TwoFactorAuth('MONARC TwoFactorAuth');
if ($tfa->verifyCode($user->getSecretKey(), $token)) {
if ($tokens !== false && $tfa->verifyCode($tokens[0], $tokens[1])) {
$user->setSecretKey($tokens[0]);
$user->setTwoFactorAuthEnabled(true);
$this->userTable->saveEntity($user);

return new Result(Result::SUCCESS, $this->getIdentity());
}

/* Verify the submitted recovery code. */
$recoveryCodes = $user->getRecoveryCodes();
foreach ($recoveryCodes as $key => $recoveryCode) {
if (password_verify($token, $recoveryCode)) {
unset($recoveryCodes[$key]);
$user->setRecoveryCodes($recoveryCodes);
$this->userTable->saveEntity($user);
return new Result(static::TWO_FA_TO_SET_UP, $this->getIdentity());
}

return new Result(Result::SUCCESS, $this->getIdentity());
}
/* Validate if the 2FA token has been submitted. */
if (empty($token)) {
return new Result(static::TWO_FA_REQUIRED, $this->getIdentity());
}

/* Verify the submitted OTP token. */
$tfa = new TwoFactorAuth('MONARC TwoFactorAuth');
if ($tfa->verifyCode($user->getSecretKey(), $token)) {
return new Result(Result::SUCCESS, $this->getIdentity());
}

/* Verify the submitted recovery code. */
$recoveryCodes = $user->getRecoveryCodes();
foreach ($recoveryCodes as $key => $recoveryCode) {
if (password_verify($token, $recoveryCode)) {
unset($recoveryCodes[$key]);
$user->setRecoveryCodes($recoveryCodes);
$this->userTable->saveEntity($user);

return new Result(Result::SUCCESS, $this->getIdentity());
}
}

return new Result(Result::FAILURE_CREDENTIAL_INVALID, $this->getIdentity());
return new Result(static::TWO_FA_FAILED, $this->getIdentity());
}
}
}
41 changes: 13 additions & 28 deletions src/Controller/AuthenticationController.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php
/**
* @link https://github.com/monarc-project for the canonical source repository
* @copyright Copyright (c) 2016-2020 SMILE GIE Securitymadein.lu - Licensed under GNU Affero GPL v3
* @copyright Copyright (c) 2016-2022 SMILE GIE Securitymadein.lu - Licensed under GNU Affero GPL v3
* @license MONARC is licensed under GNU Affero General Public License version 3
*/

Expand All @@ -12,25 +12,15 @@
use Laminas\Mvc\Controller\AbstractRestfulController;
use Laminas\View\Model\JsonModel;

/**
* Authentication Controller
*
* Class AuthenticationController
* @package Monarc\Core\Controller
*/
class AuthenticationController extends AbstractRestfulController
{
/** @var AuthenticationService */
private $authenticationService;
private AuthenticationService $authenticationService;

public function __construct(AuthenticationService $authenticationService)
{
$this->authenticationService = $authenticationService;
}

/**
* @inheritdoc
*/
public function create($data)
{
// If the authentication is successful, return 200 with a token and the user ID. Otherwise, return an HTTP
Expand All @@ -42,44 +32,39 @@ public function create($data)
/** @var User $user */
$user = $authenticatedData['user'];

if (in_array($authenticatedData['token'], array('2FARequired', '2FAToBeConfigured'))) {
if (\in_array($authenticatedData['token'], AuthenticationService::getAvailable2FATokens(), true)) {
$this->getResponse()->setStatusCode(401);
}

if ($authenticatedData['token'] == '2FAToBeConfigured') {
$jsonModel = new JsonModel([
if ($authenticatedData['token'] === AuthenticationService::TWO_FA_CODE_TO_BE_CONFIGURED) {
return new JsonModel([
'token' => $authenticatedData['token'],
'uid' => $user->getId(),
'language' => $user->getLanguage(),
'secret' => $authenticatedData['secret'],
'qrcode' => $authenticatedData['qrcode']
]);
} else {
$jsonModel = new JsonModel([
'token' => $authenticatedData['token'],
'uid' => $user->getId(),
'language' => $user->getLanguage(),
'qrcode' => $authenticatedData['qrcode'],
]);
}

return $jsonModel;
return new JsonModel([
'token' => $authenticatedData['token'],
'uid' => $user->getId(),
'language' => $user->getLanguage(),
]);
}

$this->getResponse()->setStatusCode(401);

return new JsonModel([]);
}

/**
* @inheritdoc
*/
public function deleteList($data)
{
$token = $this->getRequest()->getHeader('token');
$this->authenticationService->logout([
'token' => $token->getFieldValue()
'token' => $token->getFieldValue(),
]);

return new JsonModel(array());
return new JsonModel([]);
}
}
59 changes: 36 additions & 23 deletions src/Service/AuthenticationService.php
Original file line number Diff line number Diff line change
@@ -1,36 +1,33 @@
<?php
/**
* @link https://github.com/monarc-project for the canonical source repository
* @copyright Copyright (c) 2016-2020 SMILE GIE Securitymadein.lu - Licensed under GNU Affero GPL v3
* @copyright Copyright (c) 2016-2022 SMILE GIE Securitymadein.lu - Licensed under GNU Affero GPL v3
* @license MONARC is licensed under GNU Affero General Public License version 3
*/

namespace Monarc\Core\Service;

use DateTime;
use Exception;
use RobThree\Auth\TwoFactorAuth;
use RobThree\Auth\Providers\Qr\EndroidQrCodeProvider;
use Monarc\Core\Service\ConfigService;
use Laminas\Authentication\Result;
use Monarc\Core\Adapter\Authentication as AuthenticationAdapter;
use Monarc\Core\Storage\Authentication as AuthenticationStorage;
use RobThree\Auth\Providers\Qr\EndroidQrCodeProvider;
use RobThree\Auth\TwoFactorAuth;

/**
* Authentication Service
*
* Class AuthenticationService
* @package Monarc\Core\Service
*/
class AuthenticationService
{
/** @var AuthenticationStorage */
private $authenticationStorage;
public const TWO_FA_CODE_NOT_CORRECT = '2FACodeNotCorrect';
public const TWO_FA_CODE_REQUIRED = '2FARequired';
public const TWO_FA_CODE_TO_BE_CONFIGURED = '2FAToBeConfigured';

private ConfigService $configService;

/** @var AuthenticationAdapter */
private $authenticationAdapter;
private AuthenticationStorage $authenticationStorage;

/** @var TwoFactorAuth */
private $tfa;
private AuthenticationAdapter $authenticationAdapter;

private TwoFactorAuth $tfa;

public function __construct(
ConfigService $configService,
Expand All @@ -44,6 +41,15 @@ public function __construct(
$this->tfa = new TwoFactorAuth('MONARC', 6, 30, 'sha1', $qr);
}

public static function getAvailable2FATokens(): array
{
return [
self::TWO_FA_CODE_NOT_CORRECT,
self::TWO_FA_CODE_REQUIRED,
self::TWO_FA_CODE_TO_BE_CONFIGURED,
];
}

/**
* @param array $data The posted data (login/password)
*
Expand All @@ -66,32 +72,39 @@ public function authenticate($data): array

if (isset($data['verificationCode']) && isset($data['otpSecret'])) {
// activation of 2FA via login page (when user must activate 2FA on a 2FA enforced instance)
$token = $data['otpSecret'].":".$data['verificationCode'];
$token = $data['otpSecret'] . ':' . $data['verificationCode'];
}

$res = $this->authenticationAdapter
->setIdentity($data['login'])
->setCredential($data['password'])
->authenticate($token);

if ($res->isValid() && $res->getCode() == 1) {
if ($res->isValid() && $res->getCode() === Result::SUCCESS) {
$user = $this->authenticationAdapter->getUser();
$token = uniqid(bin2hex(random_bytes(random_int(20, 40))), true);
$this->authenticationStorage->addUserToken($token, $user);

return compact('token', 'user');
} elseif ($res->getCode() == 2) {
}
if (\in_array($res->getCode(), [
AuthenticationAdapter::TWO_FA_REQUIRED,
AuthenticationAdapter::TWO_FA_FAILED
], true)) {
$user = $this->authenticationAdapter->getUser();
$token = "2FARequired";
$token = $res->getCode() === AuthenticationAdapter::TWO_FA_REQUIRED
? self::TWO_FA_CODE_REQUIRED
: self::TWO_FA_CODE_NOT_CORRECT;

return compact('token', 'user');
} elseif ($res->getCode() == 3) {
}
if ($res->getCode() === AuthenticationAdapter::TWO_FA_TO_SET_UP) {
$user = $this->authenticationAdapter->getUser();
$token = "2FAToBeConfigured";
$token = self::TWO_FA_CODE_TO_BE_CONFIGURED;
// Create a new secret and generate a QRCode
$label = 'MONARC';
if ($this->configService->getInstanceName()) {
$label .= ' ('. $this->configService->getInstanceName() .')';
$label .= ' (' . $this->configService->getInstanceName() . ')';
}
$secret = $this->tfa->createSecret();
$qrcode = $this->tfa->getQRCodeImageAsDataUri($label, $secret);
Expand Down

0 comments on commit 14affb0

Please sign in to comment.