This commit is contained in:
Yuri Kuznetsov
2023-11-18 10:29:31 +02:00
parent 0da3a590f2
commit 2bbccbd3b5
12 changed files with 96 additions and 125 deletions
@@ -30,17 +30,13 @@
namespace Espo\Core\Authentication\TwoFactor\Email;
use Espo\ORM\EntityManager;
use Espo\Entities\User;
use Espo\Entities\UserData;
use Espo\Repositories\UserData as UserDataRepository;
use Espo\Core\Authentication\TwoFactor\Login;
use Espo\Core\Authentication\Result;
use Espo\Core\Authentication\Result\Data as ResultData;
use Espo\Core\Authentication\Result\FailReason;
use Espo\Core\Api\Request;
use RuntimeException;
@@ -49,14 +45,10 @@ class EmailLogin implements Login
{
public const NAME = 'Email';
private EntityManager $entityManager;
private Util $util;
public function __construct(EntityManager $entityManager, Util $util)
{
$this->entityManager = $entityManager;
$this->util = $util;
}
public function __construct(
private EntityManager $entityManager,
private Util $util
) {}
public function login(Result $result, Request $request): Result
{
@@ -69,6 +61,7 @@ class EmailLogin implements Login
}
if (!$code) {
// @todo Catch errors, return fail with a corresponding reason. Introduce internal exceptions.
$this->util->sendCode($user);
return Result::secondStepRequired($user, $this->getResultData());
@@ -29,22 +29,20 @@
namespace Espo\Core\Authentication\TwoFactor\Email;
use Espo\Core\Exceptions\BadRequest;
use Espo\Entities\User;
use Espo\Core\Authentication\TwoFactor\UserSetup;
use Espo\Core\Exceptions\Error;
use stdClass;
/**
* @noinspection PhpUnused
*/
class EmailUserSetup implements UserSetup
{
private $util;
public function __construct(Util $util)
{
$this->util = $util;
}
public function __construct(private Util $util)
{}
public function getData(User $user): stdClass
{
@@ -58,7 +56,7 @@ class EmailUserSetup implements UserSetup
$code = $payloadData->code ?? null;
if ($code === null) {
throw new Error("No code.");
throw new BadRequest("No code.");
}
$codeModified = str_replace(' ', '', trim($code));
@@ -31,24 +31,23 @@ namespace Espo\Core\Authentication\TwoFactor\Email;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Mail\Exceptions\SendingError;
use Espo\Core\Utils\Config;
use Espo\Core\Mail\EmailSender;
use Espo\Core\Mail\EmailFactory;
use Espo\Core\Utils\TemplateFileManager;
use Espo\Core\Htmlizer\HtmlizerFactory;
use Espo\Core\Field\DateTime;
use Espo\ORM\EntityManager;
use Espo\ORM\Query\Part\Condition as Cond;
use Espo\Entities\User;
use Espo\Entities\Email;
use Espo\Entities\TwoFactorCode;
use Espo\Entities\UserData;
use Espo\Repositories\UserData as UserDataRepository;
use RuntimeException;
use const STR_PAD_LEFT;
class Util
@@ -58,7 +57,7 @@ class Util
*/
private const CODE_LIFETIME_PERIOD = '10 minutes';
/*
/**
* A max number of attempts to try a single code.
*/
private const CODE_ATTEMPTS_COUNT = 5;
@@ -78,37 +77,18 @@ class Util
*/
private const CODE_LIMIT_PERIOD = '10 minutes';
/**
* @var EntityManager
*/
private $entityManager;
private $config;
private $emailSender;
private $templateFileManager;
private $htmlizerFactory;
private $emailFactory;
public function __construct(
EntityManager $entityManager,
Config $config,
EmailSender $emailSender,
TemplateFileManager $templateFileManager,
HtmlizerFactory $htmlizerFactory,
EmailFactory $emailFactory
) {
$this->entityManager = $entityManager;
$this->config = $config;
$this->emailSender = $emailSender;
$this->templateFileManager = $templateFileManager;
$this->htmlizerFactory = $htmlizerFactory;
$this->emailFactory = $emailFactory;
}
private EntityManager $entityManager,
private Config $config,
private EmailSender $emailSender,
private TemplateFileManager $templateFileManager,
private HtmlizerFactory $htmlizerFactory,
private EmailFactory $emailFactory
) {}
/**
* @throws Forbidden
*/
public function storeEmailAddress(User $user, string $emailAddress): void
{
$this->checkEmailAddressIsUsers($user, $emailAddress);
@@ -116,7 +96,7 @@ class Util
$userData = $this->getUserDataRepository()->getByUserId($user->getId());
if (!$userData) {
throw new Error("UserData not found.");
throw new RuntimeException("UserData not found.");
}
$userData->set('auth2FAEmailAddress', $emailAddress);
@@ -156,6 +136,11 @@ class Util
return true;
}
/**
* @throws SendingError
* @throws Forbidden
* @throws Error
*/
public function sendCode(User $user, ?string $emailAddress = null): void
{
if ($emailAddress === null) {
@@ -201,12 +186,15 @@ class Util
->findOne();
}
/**
* @throws Error
*/
private function getEmailAddress(User $user): string
{
$userData = $this->getUserDataRepository()->getByUserId($user->getId());
if (!$userData) {
throw new Error("UserData not found.");
throw new RuntimeException("UserData not found.");
}
$emailAddress = $userData->get('auth2FAEmailAddress');
@@ -223,6 +211,9 @@ class Util
return $user->getEmailAddressGroup()->getPrimaryAddress();
}
/**
* @throws Forbidden
*/
private function checkEmailAddressIsUsers(User $user, string $emailAddress): void
{
$userAddressList = array_map(
@@ -237,6 +228,9 @@ class Util
}
}
/**
* @throws Forbidden
*/
private function checkCodeLimit(User $user): void
{
$limit = $this->config->get('auth2FAEmailCodeLimit') ?? self::CODE_LIMIT;
@@ -269,6 +263,7 @@ class Util
$max = pow(10, $codeLength) - 1;
/** @noinspection PhpUnhandledExceptionInspection */
return str_pad(
(string) random_int(0, $max),
$codeLength,
@@ -308,7 +303,7 @@ class Util
->in(TwoFactorCode::ENTITY_TYPE)
->where([
'userId' => $user->getId(),
'method' => 'Email',
'method' => EmailLogin::NAME,
])
->set([
'isActive' => false,
@@ -325,7 +320,7 @@ class Util
$this->entityManager->createEntity(TwoFactorCode::ENTITY_TYPE, [
'code' => $code,
'userId' => $user->getId(),
'method' => 'Email',
'method' => EmailLogin::NAME,
'attemptsLeft' => $this->getCodeAttemptsCount(),
]);
}
@@ -30,12 +30,9 @@
namespace Espo\Core\Authentication\TwoFactor\Sms;
use Espo\ORM\EntityManager;
use Espo\Entities\User;
use Espo\Entities\UserData;
use Espo\Repositories\UserData as UserDataRepository;
use Espo\Core\Authentication\TwoFactor\Login;
use Espo\Core\Authentication\Result;
use Espo\Core\Authentication\Result\Data as ResultData;
@@ -48,14 +45,10 @@ class SmsLogin implements Login
{
public const NAME = 'Sms';
private EntityManager $entityManager;
private Util $util;
public function __construct(EntityManager $entityManager, Util $util)
{
$this->entityManager = $entityManager;
$this->util = $util;
}
public function __construct(
private EntityManager $entityManager,
private Util $util
) {}
public function login(Result $result, Request $request): Result
{
@@ -74,6 +67,7 @@ class SmsLogin implements Login
throw new RuntimeException("No user.");
}
// @todo Catch errors, return fail with a corresponding reason. Introduce internal exceptions.
$this->util->sendCode($user);
return Result::secondStepRequired($user, $this->getResultData());
@@ -29,21 +29,19 @@
namespace Espo\Core\Authentication\TwoFactor\Sms;
use Espo\Core\Exceptions\BadRequest;
use Espo\Entities\User;
use Espo\Core\Authentication\TwoFactor\UserSetup;
use Espo\Core\Exceptions\Error;
use stdClass;
/**
* @noinspection PhpUnused
*/
class SmsUserSetup implements UserSetup
{
private $util;
public function __construct(Util $util)
{
$this->util = $util;
}
public function __construct(private Util $util)
{}
public function getData(User $user): stdClass
{
@@ -57,7 +55,7 @@ class SmsUserSetup implements UserSetup
$code = $payloadData->code ?? null;
if ($code === null) {
throw new Error("No code.");
throw new BadRequest("No code.");
}
$codeModified = str_replace(' ', '', trim($code));
@@ -50,7 +50,7 @@ use const STR_PAD_LEFT;
class Util
{
private const METHOD = 'Sms';
private const METHOD = SmsLogin::NAME;
/**
* A lifetime of a code.
@@ -30,33 +30,28 @@
namespace Espo\Core\Authentication\TwoFactor\Totp;
use Espo\ORM\EntityManager;
use Espo\Entities\User;
use Espo\Entities\UserData;
use Espo\Repositories\UserData as UserDataRepository;
use Espo\Core\Authentication\TwoFactor\Login;
use Espo\Core\Authentication\Result;
use Espo\Core\Authentication\Result\Data as ResultData;
use Espo\Core\Authentication\Result\FailReason;
use Espo\Core\Api\Request;
use RuntimeException;
/**
* @noinspection PhpUnused
*/
class TotpLogin implements Login
{
public const NAME = 'Totp';
private EntityManager $entityManager;
private Util $totp;
public function __construct(EntityManager $entityManager, Util $totp)
{
$this->entityManager = $entityManager;
$this->totp = $totp;
}
public function __construct(
private EntityManager $entityManager,
private Util $totp
) {}
public function login(Result $result, Request $request): Result
{
@@ -29,34 +29,27 @@
namespace Espo\Core\Authentication\TwoFactor\Totp;
use Espo\Core\Exceptions\BadRequest;
use Espo\Entities\UserData;
use Espo\Entities\User;
use Espo\Repositories\UserData as UserDataRepository;
use Espo\ORM\EntityManager;
use Espo\Core\Authentication\TwoFactor\UserSetup;
use Espo\Core\Utils\Config;
use Espo\Core\Exceptions\Error;
use RuntimeException;
use stdClass;
/**
* @noinspection PhpUnused
*/
class TotpUserSetup implements UserSetup
{
private $totp;
private $config;
private $entityManager;
public function __construct(Util $totp, Config $config, EntityManager $entityManager)
{
$this->totp = $totp;
$this->config = $config;
$this->entityManager = $entityManager;
}
public function __construct(
private Util $totp,
private Config $config,
private EntityManager $entityManager
) {}
public function getData(User $user): stdClass
{
@@ -79,7 +72,7 @@ class TotpUserSetup implements UserSetup
$code = $payloadData->code ?? null;
if ($code === null) {
throw new Error("No code.");
throw new BadRequest("No code.");
}
$codeModified = str_replace(' ', '', trim($code));
@@ -91,7 +84,7 @@ class TotpUserSetup implements UserSetup
$userData = $this->getUserDataRepository()->getByUserId($user->getId());
if (!$userData) {
throw new Error("User not found.");
throw new RuntimeException("User not found.");
}
$secret = $userData->get('auth2FATotpSecret');
@@ -104,7 +97,7 @@ class TotpUserSetup implements UserSetup
$userData = $this->getUserDataRepository()->getByUserId($user->getId());
if (!$userData) {
throw new Error();
throw new RuntimeException();
}
$userData->set('auth2FATotpSecret', $secret);
@@ -30,6 +30,8 @@
namespace Espo\Core\Authentication\TwoFactor\Totp;
use RobThree\Auth\TwoFactorAuth;
use RobThree\Auth\TwoFactorAuthException;
use RuntimeException;
class Util
{
@@ -44,6 +46,11 @@ class Util
{
$impl = new TwoFactorAuth();
return $impl->createSecret();
try {
return $impl->createSecret();
}
catch (TwoFactorAuthException $e) {
throw new RuntimeException($e->getMessage());
}
}
}
@@ -29,6 +29,7 @@
namespace Espo\Core\Authentication\TwoFactor;
use Espo\Core\Exceptions\BadRequest;
use Espo\Entities\User;
use stdClass;
@@ -45,6 +46,8 @@ interface UserSetup
/**
* Verify input data before making 2FA enabled for a user.
*
* @throws BadRequest
*/
public function verifyData(User $user, stdClass $payloadData): bool;
}
@@ -32,18 +32,15 @@ namespace Espo\Core\Authentication\TwoFactor;
use Espo\Core\InjectableFactory;
use Espo\Core\Utils\Metadata;
use Espo\Core\Exceptions\Error;
use RuntimeException;
class UserSetupFactory
{
private InjectableFactory $injectableFactory;
private Metadata $metadata;
public function __construct(InjectableFactory $injectableFactory, Metadata $metadata)
{
$this->injectableFactory = $injectableFactory;
$this->metadata = $metadata;
}
public function __construct(
private InjectableFactory $injectableFactory,
private Metadata $metadata
) {}
public function create(string $method): UserSetup
{
@@ -51,7 +48,7 @@ class UserSetupFactory
$className = $this->metadata->get(['app', 'authentication2FAMethods', $method, 'userSetupClassName']);
if (!$className) {
throw new Error("No user-setup class for '{$method}'.");
throw new RuntimeException("No user-setup class for '$method'.");
}
return $this->injectableFactory->create($className);
@@ -29,7 +29,6 @@
namespace Espo\Tools\UserSecurity;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Exceptions\BadRequest;
@@ -95,7 +94,6 @@ class Service
/**
* @throws BadRequest
* @throws Error
* @throws Forbidden
* @throws NotFound
*/
@@ -171,9 +169,9 @@ class Service
}
/**
* @throws Error
* @throws Forbidden
* @throws NotFound
* @throws BadRequest
*/
public function update(string $id, stdClass $data): stdClass
{