From ed99a62f784d59679da1901b6b055395b822fc83 Mon Sep 17 00:00:00 2001 From: Marcin Maruszewski <marcin.maruszewski@isobar.com> Date: Tue, 11 Feb 2025 17:45:15 +0100 Subject: [PATCH] Issue #3505844: Flood service implementation --- access_code.install | 2 ++ src/Controller/UseCodeController.php | 51 ++++++++++++++++++++++------ src/Form/LoginForm.php | 44 +++++++++++++++++++----- src/Form/SettingsForm.php | 23 +++++++++++++ 4 files changed, 101 insertions(+), 19 deletions(-) diff --git a/access_code.install b/access_code.install index 37cf927..2e28872 100644 --- a/access_code.install +++ b/access_code.install @@ -48,5 +48,7 @@ function access_code_uninstall() { $config->clear('auto_code_format'); $config->clear('expiration_default'); $config->clear('display_input'); + $config->clear('login_attempts_limit'); + $config->clear('login_attempts_window'); $config->save(TRUE); } diff --git a/src/Controller/UseCodeController.php b/src/Controller/UseCodeController.php index a3af158..51fb4c7 100644 --- a/src/Controller/UseCodeController.php +++ b/src/Controller/UseCodeController.php @@ -5,6 +5,7 @@ namespace Drupal\access_code\Controller; use Drupal\access_code\Service\AccessCodeManager; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Database\Connection; +use Drupal\Core\Flood\FloodInterface; use Drupal\Core\Logger\LoggerChannelFactoryInterface; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Url; @@ -39,37 +40,67 @@ class UseCodeController extends ControllerBase { */ protected $accessCodeManager; + /** + * The flood service. + * + * @var \Drupal\Core\Flood\FloodInterface + */ + protected $flood; + /** * Constructor. */ - public function __construct(LoggerChannelFactoryInterface $logger_factory, Connection $database, MessengerInterface $messenger, AccessCodeManager $manager) { + public function __construct( + LoggerChannelFactoryInterface $logger_factory, + Connection $database, + MessengerInterface $messenger, + AccessCodeManager $manager, + FloodInterface $flood, + ) { $this->logger = $logger_factory->get('access_code'); $this->database = $database; $this->messenger = $messenger; $this->accessCodeManager = $manager; + $this->flood = $flood; } /** * @inheritdoc */ public static function create(ContainerInterface $container) { - return new static($container->get('logger.factory'), $container->get('database'), $container->get('messenger'), $container->get('access_code.manager')); + return new static( + $container->get('logger.factory'), + $container->get('database'), + $container->get('messenger'), + $container->get('access_code.manager'), + $container->get('flood'), + ); } /** * Page callback for the use code link. */ public function useCode($access_code, Request $request) { - $uid = $this->accessCodeManager->validateAccessCode($access_code); + $ip_address = $request->getClientIp(); + $limit = $this->config('access_code.settings')->get('login_attempts_limit') ?: 5; + $window = $this->config('access_code.settings')->get('login_attempts_window') ?: 3600; - if ($uid) { - $user = User::load($uid); + if ($this->flood->isAllowed('access_code_login', $limit, $window, $ip_address)) { + $uid = $this->accessCodeManager->validateAccessCode($access_code); - $url = $this->accessCodeManager->processLogin($user); - return new RedirectResponse($url->toString()); - } - else { - throw new AccessDeniedHttpException(); + if ($uid) { + $user = User::load($uid); + + $this->flood->clear('access_code_login', $ip_address); + + $url = $this->accessCodeManager->processLogin($user); + return new RedirectResponse($url->toString()); + } else { + $this->flood->register('access_code_login', $window, $ip_address); + throw new AccessDeniedHttpException(); + } + } else { + throw new AccessDeniedHttpException('Too many failed attempts. Please try again later.'); } } diff --git a/src/Form/LoginForm.php b/src/Form/LoginForm.php index 0396b5a..ebea5d1 100644 --- a/src/Form/LoginForm.php +++ b/src/Form/LoginForm.php @@ -5,6 +5,7 @@ namespace Drupal\access_code\Form; use Drupal\access_code\Service\AccessCodeManager; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Flood\FloodInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\user\Entity\User; @@ -30,13 +31,26 @@ class LoginForm extends FormBase { */ private $config; + /** + * The flood service. + * + * @var \Drupal\Core\Flood\FloodInterface + */ + protected $flood; + /** * {@inheritdoc} */ - public function __construct(ModuleHandlerInterface $handler, AccessCodeManager $manager, ConfigFactoryInterface $config_factory) { + public function __construct( + ModuleHandlerInterface $handler, + AccessCodeManager $manager, + ConfigFactoryInterface $config_factory, + FloodInterface $flood, + ) { $this->moduleHandler = $handler; $this->accessCodeManager = $manager; $this->config = $config_factory->get('access_code.settings'); + $this->flood = $flood; } /** @@ -46,7 +60,8 @@ class LoginForm extends FormBase { return new static( $container->get('module_handler'), $container->get('access_code.manager'), - $container->get('config.factory') + $container->get('config.factory'), + $container->get('flood'), ); } @@ -86,13 +101,21 @@ class LoginForm extends FormBase { * {@inheritdoc} */ public function validateForm(array &$form, FormStateInterface $form_state) { - $uid = $this->accessCodeManager->validateAccessCode($form_state->getValue('access_code')); - - if (!$uid) { - $form_state->setErrorByName('access_code', $this->t('Invalid access code.')); - } - else { - $form_state->set('uid', $uid); + $ip_address = $this->getRequest()->getClientIp(); + $limit = $this->config('access_code.settings')->get('login_attempts_limit') ?: 5; + $window = $this->config('access_code.settings')->get('login_attempts_window') ?: 3600; + + if ($this->flood->isAllowed('access_code_login', $limit, $window, $ip_address)) { + $uid = $this->accessCodeManager->validateAccessCode($form_state->getValue('access_code')); + + if (!$uid) { + $this->flood->register('access_code_login', $window, $ip_address); + $form_state->setErrorByName('access_code', $this->t('Invalid access code.')); + } else { + $form_state->set('uid', $uid); + } + } else { + $form_state->setErrorByName('access_code', $this->t('Too many failed login attempts. Please try again later.')); } } @@ -106,6 +129,9 @@ class LoginForm extends FormBase { $user = User::load($uid); + $ip_address = $this->getRequest()->getClientIp(); + $this->flood->clear('access_code_login', $ip_address); + $url = $this->accessCodeManager->processLogin($user); $form_state->setRedirectUrl($url); } diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php index 08c325f..7d7f205 100644 --- a/src/Form/SettingsForm.php +++ b/src/Form/SettingsForm.php @@ -143,6 +143,27 @@ class SettingsForm extends ConfigFormBase { '#description' => $this->t('Display the entered characters in the access code field when logging in.'), ]; + $form['flood_protection'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Flood protection settings'), + ]; + + $form['flood_protection']['login_attempts_limit'] = [ + '#type' => 'number', + '#title' => $this->t('Login attempts limit'), + '#default_value' => $config->get('login_attempts_limit') ?: 5, + '#description' => $this->t('The number of allowed login attempts before blocking.'), + '#min' => 1, + ]; + + $form['flood_protection']['login_attempts_window'] = [ + '#type' => 'number', + '#title' => $this->t('Login attempts window (in seconds)'), + '#default_value' => $config->get('login_attempts_window') ?: 3600, + '#description' => $this->t('The time window in seconds for counting login attempts.'), + '#min' => 1, + ]; + return $form; } @@ -160,6 +181,8 @@ class SettingsForm extends ConfigFormBase { ->set('expiration_default', $form_state->getValue('expiration_default')) ->set('blocked_roles', $form_state->getValue('blocked_roles')) ->set('display_input', $form_state->getValue('display_input')) + ->set('login_attempts_limit', intval($form_state->getValue('login_attempts_limit'))) + ->set('login_attempts_window', intval($form_state->getValue('login_attempts_window'))) ->save(); } -- GitLab