mirror of
https://github.com/espocrm/espocrm.git
synced 2026-06-28 06:56:05 +00:00
2fa-email
This commit is contained in:
77
application/Espo/Classes/Cleanup/TwoFactorCodes.php
Normal file
77
application/Espo/Classes/Cleanup/TwoFactorCodes.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\Cleanup;
|
||||
|
||||
use Espo\Core\Cleanup\Cleanup;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\DateTime as DateTimeUtil;
|
||||
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use Espo\Entities\TwoFactorCode;
|
||||
|
||||
use DateTime;
|
||||
|
||||
class TwoFactorCodes implements Cleanup
|
||||
{
|
||||
private const PERIOD = '5 days';
|
||||
|
||||
private $config;
|
||||
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(Config $config, EntityManager $entityManager)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
public function process(): void
|
||||
{
|
||||
$period = '-' . $this->config->get('cleanupTwoFactorCodesPeriod', self::PERIOD);
|
||||
|
||||
$from = (new DateTime())
|
||||
->modify($period)
|
||||
->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT);
|
||||
|
||||
$query = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->delete()
|
||||
->from(TwoFactorCode::ENTITY_TYPE)
|
||||
->where([
|
||||
'createdAt<' => $from,
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager
|
||||
->getQueryExecutor()
|
||||
->execute($query);
|
||||
}
|
||||
}
|
||||
80
application/Espo/Controllers/TwoFactorEmail.php
Normal file
80
application/Espo/Controllers/TwoFactorEmail.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
|
||||
use Espo\Services\TwoFactorEmail as Service;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
class TwoFactorEmail
|
||||
{
|
||||
private $service;
|
||||
|
||||
public function __construct(Service $service, User $user)
|
||||
{
|
||||
$this->service = $service;
|
||||
$this->user = $user;
|
||||
|
||||
if (
|
||||
!$this->user->isAdmin() &&
|
||||
!$this->user->isRegular()
|
||||
) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
public function postActionSendCode(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$id = $data->id ?? null;
|
||||
$emailAddress = $data->emailAddress ?? null;
|
||||
|
||||
if (!$id) {
|
||||
throw new BadRequest("No 'id'.");
|
||||
}
|
||||
|
||||
if (!$emailAddress) {
|
||||
throw new BadRequest("No 'emailAddress'.");
|
||||
}
|
||||
|
||||
if (!$this->user->isAdmin() && $id !== $this->user->getId()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$this->service->sendCode($id, $emailAddress);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
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\ResultData;
|
||||
use Espo\Core\Authentication\FailReason;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
|
||||
class EmailLogin implements Login
|
||||
{
|
||||
private $entityManager;
|
||||
|
||||
private $util;
|
||||
|
||||
public function __construct(EntityManager $entityManager, Util $util)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->util = $util;
|
||||
}
|
||||
|
||||
public function login(Result $result, Request $request): Result
|
||||
{
|
||||
$code = $request->getHeader('Espo-Authorization-Code');
|
||||
|
||||
if (!$code) {
|
||||
$this->util->sendCode($result->getLoggedUser());
|
||||
|
||||
return Result::secondStepRequired($result->getUser(), $this->getLoginData());
|
||||
}
|
||||
|
||||
$loggedUser = $result->getLoggedUser();
|
||||
|
||||
if ($this->verifyCode($loggedUser, $code)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return Result::fail(FailReason::CODE_NOT_VERIFIED);
|
||||
}
|
||||
|
||||
private function getLoginData(): ResultData
|
||||
{
|
||||
return ResultData::createWithMessage('enterCodeSentInEmail');
|
||||
}
|
||||
|
||||
private function verifyCode(User $user, string $code): bool
|
||||
{
|
||||
$userData = $this->getUserDataRepository()->getByUserId($user->getId());
|
||||
|
||||
if (!$userData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$userData->get('auth2FA')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($userData->get('auth2FAMethod') !== 'Email') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->util->verifyCode($user, $code);
|
||||
}
|
||||
|
||||
private function getUserDataRepository(): UserDataRepository
|
||||
{
|
||||
return $this->entityManager->getRepository(UserData::ENTITY_TYPE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Authentication\TwoFactor\Email;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\Core\Authentication\TwoFactor\UserSetup;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class EmailUserSetup implements UserSetup
|
||||
{
|
||||
private $util;
|
||||
|
||||
public function __construct(Util $util)
|
||||
{
|
||||
$this->util = $util;
|
||||
}
|
||||
|
||||
public function getData(User $user): stdClass
|
||||
{
|
||||
return (object) [
|
||||
'emailAddressList' => $user->getEmailAddressGroup()->getAddressList(),
|
||||
];
|
||||
}
|
||||
|
||||
public function verifyData(User $user, stdClass $payloadData): bool
|
||||
{
|
||||
$code = $payloadData->code ?? null;
|
||||
|
||||
if ($code === null) {
|
||||
throw new Error("No code.");
|
||||
}
|
||||
|
||||
$codeModified = str_replace(' ', '', trim($code));
|
||||
|
||||
if (!$codeModified) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->util->verifyCode($user, $codeModified);
|
||||
}
|
||||
}
|
||||
340
application/Espo/Core/Authentication/TwoFactor/Email/Util.php
Normal file
340
application/Espo/Core/Authentication/TwoFactor/Email/Util.php
Normal file
@@ -0,0 +1,340 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Authentication\TwoFactor\Email;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
|
||||
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 const STR_PAD_LEFT;
|
||||
|
||||
class Util
|
||||
{
|
||||
/**
|
||||
* A lifetime of a code.
|
||||
*/
|
||||
private const CODE_LIFETIME_PERIOD = '10 minutes';
|
||||
|
||||
/*
|
||||
* A max number of attempts to try a single code.
|
||||
*/
|
||||
private const CODE_ATTEMPTS_COUNT = 5;
|
||||
|
||||
/**
|
||||
* A length of a code.
|
||||
*/
|
||||
private const CODE_LENGTH = 7;
|
||||
|
||||
/**
|
||||
* A max number of codes tried by a user in a period defined by `CODE_LIMIT_PERIOD`.
|
||||
*/
|
||||
private const CODE_LIMIT = 5;
|
||||
|
||||
/**
|
||||
* A period for limiting trying to too many codes.
|
||||
*/
|
||||
private const CODE_LIMIT_PERIOD = '10 minutes';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public function storeEmailAddress(User $user, string $emailAddress): void
|
||||
{
|
||||
$this->checkEmailAddressIsUsers($user, $emailAddress);
|
||||
|
||||
$userData = $this->getUserDataRepository()->getByUserId($user->getId());
|
||||
|
||||
$userData->set('auth2FAEmailAddress', $emailAddress);
|
||||
|
||||
$this->entityManager->saveEntity($userData);
|
||||
}
|
||||
|
||||
public function verifyCode(User $user, string $code): bool
|
||||
{
|
||||
$codeEntity = $this->findCodeEntity($user);
|
||||
|
||||
if (!$codeEntity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($codeEntity->getAttemptsLeft() <= 1) {
|
||||
$this->decrementAttemptsLeft($codeEntity);
|
||||
$this->inactivateExistingCodeRecords($user);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($codeEntity->getCode() !== $code) {
|
||||
$this->decrementAttemptsLeft($codeEntity);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->isCodeValidByLifetime($codeEntity)) {
|
||||
$this->inactivateExistingCodeRecords($user);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->inactivateExistingCodeRecords($user);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function sendCode(User $user, ?string $emailAddress = null): void
|
||||
{
|
||||
if ($emailAddress === null) {
|
||||
$emailAddress = $this->getEmailAddress($user);
|
||||
}
|
||||
|
||||
$this->checkEmailAddressIsUsers($user, $emailAddress);
|
||||
$this->checkCodeLimit($user);
|
||||
|
||||
$code = $this->generateCode();
|
||||
|
||||
$this->inactivateExistingCodeRecords($user);
|
||||
$this->createCodeRecord($user, $code);
|
||||
|
||||
$email = $this->createEmail($user, $code, $emailAddress);
|
||||
|
||||
$this->emailSender->send($email);
|
||||
}
|
||||
|
||||
private function isCodeValidByLifetime(TwoFactorCode $codeEntity): bool
|
||||
{
|
||||
$period = $this->config->get('auth2FAEmailCodeLifetimePeriod') ?? self::CODE_LIFETIME_PERIOD;
|
||||
|
||||
$validUntil = $codeEntity->getCreatedAt()->modify($period);
|
||||
|
||||
if (DateTime::createNow()->diff($validUntil)->invert) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function findCodeEntity(User $user): ?TwoFactorCode
|
||||
{
|
||||
return $this->entityManager
|
||||
->getRDBRepository(TwoFactorCode::ENTITY_TYPE)
|
||||
->where([
|
||||
'method' => 'Email',
|
||||
'userId' => $user->getId(),
|
||||
'isActive' => true,
|
||||
])
|
||||
->findOne();
|
||||
}
|
||||
|
||||
private function getEmailAddress(User $user): string
|
||||
{
|
||||
$userData = $this->getUserDataRepository()->getByUserId($user->getId());
|
||||
|
||||
if (!$userData) {
|
||||
throw new Error("UserData not found.");
|
||||
}
|
||||
|
||||
$emailAddress = $userData->get('auth2FAEmailAddress');
|
||||
|
||||
if ($emailAddress) {
|
||||
return $emailAddress;
|
||||
}
|
||||
|
||||
if ($user->getEmailAddressGroup()->getCount() === 0) {
|
||||
throw new Error("User does not have email address.");
|
||||
}
|
||||
|
||||
return $user->getEmailAddressGroup()->getPrimary()->getAddress();
|
||||
}
|
||||
|
||||
private function checkEmailAddressIsUsers(User $user, string $emailAddress): void
|
||||
{
|
||||
$userAddressList = array_map(
|
||||
function (string $item) {
|
||||
return strtolower($item);
|
||||
},
|
||||
$user->getEmailAddressGroup()->getAddressList()
|
||||
);
|
||||
|
||||
if (!in_array(strtolower($emailAddress), $userAddressList)) {
|
||||
throw new Forbidden("Email address is not one of user's.");
|
||||
}
|
||||
}
|
||||
|
||||
private function checkCodeLimit(User $user): void
|
||||
{
|
||||
$limit = $this->config->get('auth2FAEmailCodeLimit') ?? self::CODE_LIMIT;
|
||||
$period = $this->config->get('auth2FAEmailCodeLimitPeriod') ?? self::CODE_LIMIT_PERIOD;
|
||||
|
||||
$from = DateTime::createNow()
|
||||
->modify('-' . $period)
|
||||
->getString();
|
||||
|
||||
$count = $this->entityManager
|
||||
->getRDBRepository(TwoFactorCode::ENTITY_TYPE)
|
||||
->where(
|
||||
Cond::and(
|
||||
Cond::equal(Cond::column('method'), 'Email'),
|
||||
Cond::equal(Cond::column('userId'), $user->getId()),
|
||||
Cond::greaterOrEqual(Cond::column('createdAt'), $from),
|
||||
Cond::lessOrEqual(Cond::column('attemptsLeft'), 0),
|
||||
)
|
||||
)
|
||||
->count();
|
||||
|
||||
if ($count >= $limit) {
|
||||
throw new Forbidden("Max code count exceeded.");
|
||||
}
|
||||
}
|
||||
|
||||
private function generateCode(): string
|
||||
{
|
||||
$codeLength = $this->config->get('auth2FAEmailCodeLength') ?? self::CODE_LENGTH;
|
||||
|
||||
$max = pow(10, $codeLength) - 1;
|
||||
|
||||
return str_pad(
|
||||
(string) random_int(0, $max),
|
||||
$codeLength,
|
||||
'0',
|
||||
STR_PAD_LEFT
|
||||
);
|
||||
}
|
||||
|
||||
private function createEmail(User $user, string $code, string $emailAddress): Email
|
||||
{
|
||||
$subjectTpl = $this->templateFileManager->getTemplate('twoFactorCode', 'subject');
|
||||
$bodyTpl = $this->templateFileManager->getTemplate('twoFactorCode', 'body');
|
||||
|
||||
$htmlizer = $this->htmlizerFactory->create();
|
||||
|
||||
$data = [
|
||||
'code' => $code,
|
||||
];
|
||||
|
||||
$subject = $htmlizer->render($user, $subjectTpl, null, $data, true);
|
||||
$body = $htmlizer->render($user, $bodyTpl, null, $data, true);
|
||||
|
||||
$email = $this->emailFactory->create();
|
||||
|
||||
$email->setSubject($subject);
|
||||
$email->setBody($body);
|
||||
$email->addToAddress($emailAddress);
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
private function inactivateExistingCodeRecords(User $user): void
|
||||
{
|
||||
$query = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->update()
|
||||
->in(TwoFactorCode::ENTITY_TYPE)
|
||||
->where([
|
||||
'userId' => $user->getId(),
|
||||
'method' => 'Email',
|
||||
])
|
||||
->set([
|
||||
'isActive' => false,
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager
|
||||
->getQueryExecutor()
|
||||
->execute($query);
|
||||
}
|
||||
|
||||
private function createCodeRecord(User $user, string $code): void
|
||||
{
|
||||
$this->entityManager->createEntity(TwoFactorCode::ENTITY_TYPE, [
|
||||
'code' => $code,
|
||||
'userId' => $user->getId(),
|
||||
'method' => 'Email',
|
||||
'attemptsLeft' => $this->getCodeAttemptsCount(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function getUserDataRepository(): UserDataRepository
|
||||
{
|
||||
return $this->entityManager->getRepository(UserData::ENTITY_TYPE);
|
||||
}
|
||||
|
||||
private function decrementAttemptsLeft(TwoFactorCode $codeEntity): void
|
||||
{
|
||||
$codeEntity->decrementAttemptsLeft();
|
||||
|
||||
$this->entityManager->saveEntity($codeEntity);
|
||||
}
|
||||
|
||||
private function getCodeAttemptsCount(): int
|
||||
{
|
||||
return $this->config->get('auth2FAEmailCodeAttemptsCount') ?? self::CODE_ATTEMPTS_COUNT;
|
||||
}
|
||||
}
|
||||
67
application/Espo/Entities/TwoFactorCode.php
Normal file
67
application/Espo/Entities/TwoFactorCode.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Entities;
|
||||
|
||||
use Espo\Core\Field\DateTime;
|
||||
|
||||
class TwoFactorCode extends \Espo\Core\ORM\Entity
|
||||
{
|
||||
public const ENTITY_TYPE = 'TwoFactorCode';
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->get('isActive');
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTime
|
||||
{
|
||||
return $this->getValueObject('createdAt');
|
||||
}
|
||||
|
||||
public function getCode(): string
|
||||
{
|
||||
return $this->get('code');
|
||||
}
|
||||
|
||||
public function getAttemptsLeft(): int
|
||||
{
|
||||
return $this->get('attemptsLeft');
|
||||
}
|
||||
|
||||
public function setInactive(): void
|
||||
{
|
||||
$this->set('isActive', false);
|
||||
}
|
||||
|
||||
public function decrementAttemptsLeft(): void
|
||||
{
|
||||
$this->set('attemptsLeft', $this->getAttemptsLeft() - 1);
|
||||
}
|
||||
}
|
||||
@@ -317,6 +317,7 @@
|
||||
"readable": "Readable"
|
||||
},
|
||||
"templates": {
|
||||
"twoFactorCode": "2FA Code",
|
||||
"accessInfo": "Access Info",
|
||||
"accessInfoPortal": "Access Info for Portals",
|
||||
"assignment": "Assignment",
|
||||
|
||||
@@ -154,7 +154,8 @@
|
||||
"EmailReceived": "Received emails"
|
||||
},
|
||||
"auth2FAMethodList": {
|
||||
"Totp": "TOTP"
|
||||
"Totp": "TOTP",
|
||||
"Email": "Email"
|
||||
}
|
||||
},
|
||||
"tooltips": {
|
||||
|
||||
@@ -77,7 +77,8 @@
|
||||
"Security": "Security",
|
||||
"Reset 2FA": "Reset 2FA",
|
||||
"Code": "Code",
|
||||
"Secret": "Secret"
|
||||
"Secret": "Secret",
|
||||
"Send Code": "Send Code"
|
||||
},
|
||||
"tooltips": {
|
||||
"defaultTeam": "All records created by this user will be related to this team by default.",
|
||||
@@ -108,6 +109,8 @@
|
||||
"userNameExists": "User Name already exists",
|
||||
"wrongCode": "Wrong code",
|
||||
"codeIsRequired": "Code is required",
|
||||
"choose2FaEmailAddress": "Select an email address that will be used for 2FA. It's highly recommended to use a non-primary email address.",
|
||||
"enterCodeSentInEmail": "Enter the code sent to your email address.",
|
||||
"enterTotpCode": "Enter a code from your authenticator app.",
|
||||
"verifyTotpCode": "Scan the QR-code with your mobile authenticator app. If you have a trouble with scanning, you can enter the secret manually. After that you will see a 6-digit code in your application. Enter this code in the field below.",
|
||||
"generateAndSendNewPassword": "A new password will be generated and sent to the user's email address.",
|
||||
|
||||
@@ -6,5 +6,13 @@
|
||||
"userApplyView": "views/user-security/modals/totp",
|
||||
"loginClassName": "Espo\\Core\\Authentication\\TwoFactor\\Totp\\TotpLogin",
|
||||
"userSetupClassName": "Espo\\Core\\Authentication\\TwoFactor\\Totp\\TotpUserSetup"
|
||||
},
|
||||
"Email": {
|
||||
"settings": {
|
||||
"isAvailable": true
|
||||
},
|
||||
"userApplyView": "views/user-security/modals/two-factor-email",
|
||||
"loginClassName": "Espo\\Core\\Authentication\\TwoFactor\\Email\\EmailLogin",
|
||||
"userSetupClassName": "Espo\\Core\\Authentication\\TwoFactor\\Email\\EmailUserSetup"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,8 @@
|
||||
"webhookQueue": {
|
||||
"className": "Espo\\Classes\\Cleanup\\WebhookQueue",
|
||||
"order": 11
|
||||
},
|
||||
"twoFactorCodes": {
|
||||
"className": "Espo\\Classes\\Cleanup\\TwoFactorCodes"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,5 +25,8 @@
|
||||
},
|
||||
"passwordChangeLink": {
|
||||
"scope": "User"
|
||||
},
|
||||
"twoFactorCode": {
|
||||
"scope": "User"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"fields": {
|
||||
"code": {
|
||||
"type": "varchar",
|
||||
"maxLength": 100
|
||||
},
|
||||
"method": {
|
||||
"type": "varchar",
|
||||
"maxLength": 100
|
||||
},
|
||||
"attemptsLeft": {
|
||||
"type": "int"
|
||||
},
|
||||
"isActive": {
|
||||
"type": "bool",
|
||||
"default": true
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "datetime",
|
||||
"readOnly": true
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"user": {
|
||||
"type": "belongsTo",
|
||||
"entity": "User"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"createdAt": {
|
||||
"columns": [
|
||||
"createdAt"
|
||||
]
|
||||
},
|
||||
"userIdMethod": {
|
||||
"columns": [
|
||||
"userId",
|
||||
"method"
|
||||
]
|
||||
},
|
||||
"userIdMethodIsActive": {
|
||||
"columns": [
|
||||
"userId",
|
||||
"method",
|
||||
"isActive"
|
||||
]
|
||||
},
|
||||
"userIdMethodCreatedAt": {
|
||||
"columns": [
|
||||
"userId",
|
||||
"method",
|
||||
"createdAt"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,9 @@
|
||||
"auth2FATotpSecret": {
|
||||
"type": "varchar",
|
||||
"maxLength": 32
|
||||
},
|
||||
"auth2FAEmailAddress": {
|
||||
"type": "varchar"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<p>Enter this code to log in to EspoCRM.</p>
|
||||
|
||||
<p>Code: <strong>{{code}}</strong></p>
|
||||
@@ -0,0 +1 @@
|
||||
EspoCRM authentication code
|
||||
71
application/Espo/Services/TwoFactorEmail.php
Normal file
71
application/Espo/Services/TwoFactorEmail.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Services;
|
||||
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
|
||||
use Espo\Core\Authentication\TwoFactor\Email\Util;
|
||||
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
class TwoFactorEmail
|
||||
{
|
||||
private $util;
|
||||
|
||||
private $user;
|
||||
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(Util $util, User $user, EntityManager $entityManager)
|
||||
{
|
||||
$this->util = $util;
|
||||
$this->user = $user;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
public function sendCode(string $userId, string $emailAddress): void
|
||||
{
|
||||
if (!$this->user->isAdmin() && $userId !== $this->user->getId()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$user = $this->entityManager->getEntity(User::ENTITY_TYPE, $userId);
|
||||
|
||||
if (!$user) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
$this->util->sendCode($user, $emailAddress);
|
||||
$this->util->storeEmailAddress($user, $emailAddress);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<div class="panel no-side-margin">
|
||||
<div class="panel-body">
|
||||
<p class="p-info">
|
||||
{{translate 'choose2FaEmailAddress' category='messages' scope='User'}}
|
||||
</p>
|
||||
<p class="p-button">
|
||||
<button class="btn btn-default" data-action="sendCode">{{translate 'Send Code' scope='User'}}</button>
|
||||
</p>
|
||||
<p class="p-info-after hidden">
|
||||
{{translate 'enterCodeSentInEmail' category='messages' scope='User'}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="record">{{{record}}}</div>
|
||||
192
client/src/views/user-security/modals/two-factor-email.js
Normal file
192
client/src/views/user-security/modals/two-factor-email.js
Normal file
@@ -0,0 +1,192 @@
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
define('views/user-security/modals/two-factor-email',
|
||||
['views/modal', 'model'],
|
||||
function (Dep, Model) {
|
||||
|
||||
return Dep.extend({
|
||||
|
||||
template: 'user-security/modals/two-factor-email',
|
||||
|
||||
className: 'dialog dialog-record',
|
||||
|
||||
events: {
|
||||
'click [data-action="sendCode"]': function () {
|
||||
this.actionSendCode();
|
||||
},
|
||||
},
|
||||
|
||||
setup: function () {
|
||||
this.buttonList = [
|
||||
{
|
||||
name: 'apply',
|
||||
label: 'Apply',
|
||||
style: 'danger',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
name: 'cancel',
|
||||
label: 'Cancel',
|
||||
}
|
||||
];
|
||||
|
||||
this.headerHtml = false;
|
||||
|
||||
let codeLength = this.getConfig().get('auth2FAEmailCodeLength') || 7;
|
||||
|
||||
let model = new Model();
|
||||
|
||||
model.name = 'UserSecurity';
|
||||
|
||||
model.set('emailAddress', null);
|
||||
|
||||
model.setDefs({
|
||||
fields: {
|
||||
'code': {
|
||||
type: 'varchar',
|
||||
required: true,
|
||||
maxLength: codeLength,
|
||||
},
|
||||
'emailAddress': {
|
||||
type: 'enum',
|
||||
required: true,
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
this.internalModel = model;
|
||||
|
||||
this.wait(
|
||||
Espo.Ajax
|
||||
.postRequest('UserSecurity/action/getTwoFactorUserSetupData', {
|
||||
id: this.model.id,
|
||||
password: this.model.get('password'),
|
||||
auth2FAMethod: this.model.get('auth2FAMethod'),
|
||||
reset: this.options.reset,
|
||||
})
|
||||
.then(data => {
|
||||
this.emailAddressList = data.emailAddressList;
|
||||
|
||||
this.createView('record', 'views/record/edit-for-modal', {
|
||||
scope: 'None',
|
||||
el: this.getSelector() + ' .record',
|
||||
model: model,
|
||||
detailLayout: [
|
||||
{
|
||||
rows: [
|
||||
[
|
||||
{
|
||||
name: 'emailAddress',
|
||||
labelText: this.translate('emailAddress', 'fields', 'User'),
|
||||
},
|
||||
false
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'code',
|
||||
labelText: this.translate('Code', 'labels', 'User'),
|
||||
},
|
||||
false
|
||||
],
|
||||
]
|
||||
}
|
||||
],
|
||||
}, view => {
|
||||
view.setFieldOptionList('emailAddress', this.emailAddressList);
|
||||
|
||||
if (this.emailAddressList.length) {
|
||||
model.set('emailAddress', this.emailAddressList[0]);
|
||||
}
|
||||
|
||||
view.hideField('code');
|
||||
});
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
afterRender: function () {
|
||||
this.$sendCode = this.$el.find('[data-action="sendCode"]');
|
||||
|
||||
this.$pInfo = this.$el.find('p.p-info');
|
||||
this.$pButton = this.$el.find('p.p-button');
|
||||
this.$pInfoAfter = this.$el.find('p.p-info-after');
|
||||
},
|
||||
|
||||
actionSendCode: function () {
|
||||
this.$sendCode.attr('disabled', 'disabled').addClass('disabled');
|
||||
|
||||
Espo.Ajax
|
||||
.postRequest('TwoFactorEmail/action/sendCode', {
|
||||
id: this.model.id,
|
||||
emailAddress: this.internalModel.get('emailAddress'),
|
||||
})
|
||||
.then(() => {
|
||||
this.showButton('apply');
|
||||
|
||||
this.$pInfo.addClass('hidden');
|
||||
this.$pButton.addClass('hidden');
|
||||
this.$pInfoAfter.removeClass('hidden');
|
||||
|
||||
this.getView('record').setFieldReadOnly('emailAddress');
|
||||
this.getView('record').showField('code');
|
||||
})
|
||||
.catch(() => {
|
||||
this.$sendCode.removeAttr('disabled').removeClass('disabled');
|
||||
});
|
||||
},
|
||||
|
||||
actionApply: function () {
|
||||
let data = this.getView('record').processFetch();
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.model.set('code', data.code);
|
||||
|
||||
this.hideButton('apply');
|
||||
this.hideButton('cancel');
|
||||
|
||||
Espo.Ui.notify(this.translate('pleaseWait', 'messages'));
|
||||
|
||||
this.model
|
||||
.save()
|
||||
.then(() => {
|
||||
Espo.Ui.notify(false);
|
||||
|
||||
this.trigger('done');
|
||||
})
|
||||
.catch(() => {
|
||||
this.showButton('apply');
|
||||
this.showButton('cancel');
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user