diff --git a/access_code.install b/access_code.install index 37cf927cb7c0f01ad9ff393db72af2ca90e5e4fb..2e28872f2d1f0330755a0193f5d2668d2aaca1a7 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 a3af1589c6ee33d1155d6cf39e047f3099f35fb9..51fb4c7ff0dcb2fe3d236c269d15da1eb7013e48 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 0396b5a9330673a36e5ec631831c2960aa50c886..ebea5d1493e73fd1448172ea93fb640d38e03685 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 08c325fe6855a04ba6585ed3e81bb415b0521f5d..7d7f2053543cf727e462866138118f39f903f68c 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(); }