diff --git a/application/Espo/Core/Authentication/ConfigDataProvider.php b/application/Espo/Core/Authentication/ConfigDataProvider.php index aef91043fc..783ef5099e 100644 --- a/application/Espo/Core/Authentication/ConfigDataProvider.php +++ b/application/Espo/Core/Authentication/ConfigDataProvider.php @@ -37,6 +37,7 @@ use Espo\Core\Utils\Metadata; class ConfigDataProvider { private const FAILED_ATTEMPTS_PERIOD = '60 seconds'; + private const FAILED_CODE_ATTEMPTS_PERIOD = '5 minutes'; private const MAX_FAILED_ATTEMPT_NUMBER = 10; public function __construct(private Config $config, private Metadata $metadata) @@ -50,6 +51,14 @@ class ConfigDataProvider return $this->config->get('authFailedAttemptsPeriod', self::FAILED_ATTEMPTS_PERIOD); } + /** + * A period for max failed 2FA code attempts checking. + */ + public function getFailedCodeAttemptsPeriod(): string + { + return $this->config->get('authFailedCodeAttemptsPeriod', self::FAILED_CODE_ATTEMPTS_PERIOD); + } + /** * Max failed log in attempts. */ diff --git a/application/Espo/Core/Authentication/Hook/Hooks/FailedAttemptsLimit.php b/application/Espo/Core/Authentication/Hook/Hooks/FailedAttemptsLimit.php index 6e67a487ad..368f1db4bf 100644 --- a/application/Espo/Core/Authentication/Hook/Hooks/FailedAttemptsLimit.php +++ b/application/Espo/Core/Authentication/Hook/Hooks/FailedAttemptsLimit.php @@ -61,32 +61,20 @@ class FailedAttemptsLimit implements BeforeLogin { $isByTokenOnly = !$data->getMethod() && $request->getHeader(HeaderKey::AUTHORIZATION_BY_TOKEN) === 'true'; - if ($isByTokenOnly) { - return; - } - - if ($this->configDataProvider->isAuthLogDisabled()) { + if ($isByTokenOnly || $this->configDataProvider->isAuthLogDisabled()) { return; } $isSecondStep = $request->getHeader(HeaderKey::AUTHORIZATION_CODE) !== null; - $failedAttemptsPeriod = $this->configDataProvider->getFailedAttemptsPeriod(); - $maxFailedAttempts = $this->configDataProvider->getMaxFailedAttemptNumber(); - - $requestTime = intval($request->getServerParam('REQUEST_TIME_FLOAT')); - - try { - $requestTimeFrom = (new DateTime('@' . $requestTime))->modify('-' . $failedAttemptsPeriod); - } - catch (Exception $e) { - throw new RuntimeException($e->getMessage()); - } + $failedAttemptsPeriod = $isSecondStep ? + $this->configDataProvider->getFailedCodeAttemptsPeriod() : + $this->configDataProvider->getFailedAttemptsPeriod(); $ipAddress = $this->util->obtainIpFromRequest($request); $where = [ - 'requestTime>' => $requestTimeFrom->format('U'), + 'requestTime>' => $this->getTimeFrom($request, $failedAttemptsPeriod)->format('U'), 'isDenied' => true, ]; @@ -113,7 +101,7 @@ class FailedAttemptsLimit implements BeforeLogin ->where($where) ->count(); - if ($failAttemptCount <= $maxFailedAttempts) { + if ($failAttemptCount <= $this->configDataProvider->getMaxFailedAttemptNumber()) { return; } @@ -125,4 +113,18 @@ class FailedAttemptsLimit implements BeforeLogin throw new Forbidden("Max failed login attempts exceeded for IP address $ipAddress."); } + + private function getTimeFrom(Request $request, string $failedAttemptsPeriod): DateTime + { + $requestTime = intval($request->getServerParam('REQUEST_TIME_FLOAT')); + + try { + $requestTimeFrom = (new DateTime('@' . $requestTime))->modify('-' . $failedAttemptsPeriod); + } + catch (Exception $e) { + throw new RuntimeException($e->getMessage()); + } + + return $requestTimeFrom; + } } diff --git a/application/Espo/Resources/defaults/systemConfig.php b/application/Espo/Resources/defaults/systemConfig.php index e04793c937..bc9621b633 100644 --- a/application/Espo/Resources/defaults/systemConfig.php +++ b/application/Espo/Resources/defaults/systemConfig.php @@ -104,6 +104,7 @@ return [ 'authLogDisabled', 'authApiUserLogDisabled', 'authFailedAttemptsPeriod', + 'authFailedCodeAttemptsPeriod', 'authMaxFailedAttemptNumber', 'ipAddressServerParam', 'jobNoTableLocking',