‪TYPO3CMS  ‪main
FailedLoginAttemptNotification.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Psr\Http\Message\ServerRequestInterface;
21 use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
28 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
32 use ‪TYPO3\CMS\Core\SysLog\Action\Login as SystemLogLoginAction;
33 use ‪TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
34 use ‪TYPO3\CMS\Core\SysLog\Type as SystemLogType;
36 
47 {
48  use ‪LogDataTrait;
49 
51 
57  public function ‪__construct(
59  protected readonly int $warningPeriod = 3600,
60  protected readonly int $failedLoginAttemptsThreshold = 3
61  ) {
62  $this->notificationRecipientEmailAddress = ‪$notificationRecipientEmailAddress ?? (string)‪$GLOBALS['TYPO3_CONF_VARS']['BE']['warning_email_addr'];
63  }
64 
72  #[AsEventListener(identifier: 'typo3/cms-backend/failed-login-attempt-notification', event: LoginAttemptFailedEvent::class)]
73  #[AsEventListener(identifier: 'typo3/cms-backend/failed-mfa-verification-notification', event: MfaVerificationFailedEvent::class)]
75  {
76  if (!$event->‪isBackendAttempt()) {
77  // This notification only works for backend users
78  return;
79  }
80  if (!GeneralUtility::validEmail($this->notificationRecipientEmailAddress)) {
81  return;
82  }
83 
85  $user = $event->‪getUser();
86  $earliestTimeToCheckForFailures = ‪$GLOBALS['EXEC_TIME'] - $this->warningPeriod;
87  $loginFailures = $this->‪getLoginFailures($earliestTimeToCheckForFailures);
88  // Check for more than a maximum number of login failures with the last period
89  if (count($loginFailures) > $this->failedLoginAttemptsThreshold) {
90  // OK, so there were more than the max allowed number of login failures - so we will send an email then.
91  $this->‪sendLoginAttemptEmail($loginFailures, $event->‪getRequest());
92  // Login failure attempt written to log, which will be picked up later-on again
93  $user->writelog(
94  SystemLogType::LOGIN,
95  SystemLogLoginAction::SEND_FAILURE_WARNING_EMAIL,
96  SystemLogErrorClassification::MESSAGE,
97  3,
98  'Failure warning (%s failures within %s seconds) sent by email to %s',
99  [count($loginFailures), $this->warningPeriod, $this->notificationRecipientEmailAddress]
100  );
101  }
102  }
103 
110  protected function ‪getLoginFailures(int $earliestTimeToCheckForFailures): array
111  {
112  // Get last flag set in the log for sending an email
113  // If a notification was e.g. sent 20mins ago, only check the entries of the last 20 minutes
114  $queryBuilder = $this->‪createPreparedQueryBuilder($earliestTimeToCheckForFailures, SystemLogLoginAction::SEND_FAILURE_WARNING_EMAIL);
115  $statement = $queryBuilder
116  ->select('tstamp')
117  ->orderBy('tstamp', 'DESC')
118  ->setMaxResults(1)
119  ->executeQuery();
120  if ($lastTimeANotificationWasSent = $statement->fetchOne()) {
121  $earliestTimeToCheckForFailures = (int)$lastTimeANotificationWasSent;
122  }
123  $queryBuilder = $this->‪createPreparedQueryBuilder($earliestTimeToCheckForFailures, SystemLogLoginAction::ATTEMPT);
124  return $queryBuilder
125  ->select('*')
126  ->orderBy('tstamp')
127  ->executeQuery()
128  ->fetchAllAssociative();
129  }
130 
136  protected function ‪sendLoginAttemptEmail(array $previousFailures, ServerRequestInterface $request): void
137  {
138  $emailData = [];
139  foreach ($previousFailures as $row) {
140  $text = $this->‪formatLogDetails($row['details'] ?? '', $row['log_data'] ?? '');
141  if ((int)$row['type'] === SystemLogType::LOGIN) {
142  $text = str_replace('###IP###', $row['IP'], $text);
143  }
144  $emailData[] = [
145  'row' => $row,
146  'text' => $text,
147  ];
148  }
149  $email = GeneralUtility::makeInstance(FluidEmail::class)
150  ->to($this->notificationRecipientEmailAddress)
151  ->setTemplate('Security/LoginAttemptFailedWarning')
152  ->assign('lines', $emailData)
153  ->setRequest($request);
154 
155  try {
156  // @todo DI should be used to inject the MailerInterface
157  GeneralUtility::makeInstance(MailerInterface::class)->send($email);
158  } catch (TransportExceptionInterface $e) {
159  // Sending mail failed. Probably broken smtp setup.
160  // @todo Maybe log that sending mail failed.
161  }
162  }
163 
164  protected function ‪createPreparedQueryBuilder(int $earliestLogDate, int $loginAction): QueryBuilder
165  {
166  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
167  ->getQueryBuilderForTable('sys_log');
168  $queryBuilder
169  ->from('sys_log')
170  ->where(
171  $queryBuilder->expr()->eq(
172  'type',
173  $queryBuilder->createNamedParameter(SystemLogType::LOGIN, ‪Connection::PARAM_INT)
174  ),
175  $queryBuilder->expr()->eq(
176  'action',
177  $queryBuilder->createNamedParameter($loginAction, ‪Connection::PARAM_INT)
178  ),
179  $queryBuilder->expr()->gt(
180  'tstamp',
181  $queryBuilder->createNamedParameter($earliestLogDate, ‪Connection::PARAM_INT)
182  )
183  );
184  return $queryBuilder;
185  }
186 }
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:52
‪TYPO3\CMS\Backend\EventListener\FailedLoginAttemptNotification\getLoginFailures
‪array getLoginFailures(int $earliestTimeToCheckForFailures)
Definition: FailedLoginAttemptNotification.php:109
‪TYPO3\CMS\Core\Authentication\Event\LoginAttemptFailedEvent
Definition: LoginAttemptFailedEvent.php:27
‪TYPO3\CMS\Core\Attribute\AsEventListener
Definition: AsEventListener.php:25
‪TYPO3\CMS\Backend\EventListener\FailedLoginAttemptNotification\__invoke
‪__invoke(LoginAttemptFailedEvent|MfaVerificationFailedEvent $event)
Definition: FailedLoginAttemptNotification.php:73
‪TYPO3\CMS\Backend\EventListener\FailedLoginAttemptNotification\__construct
‪__construct(string $notificationRecipientEmailAddress=null, protected readonly int $warningPeriod=3600, protected readonly int $failedLoginAttemptsThreshold=3)
Definition: FailedLoginAttemptNotification.php:56
‪TYPO3\CMS\Core\Log\LogDataTrait\formatLogDetails
‪formatLogDetails(string $detailString, mixed $substitutes)
Definition: LogDataTrait.php:43
‪TYPO3\CMS\Core\Authentication\Event\AbstractAuthenticationFailedEvent\isBackendAttempt
‪isBackendAttempt()
Definition: AbstractAuthenticationFailedEvent.php:43
‪TYPO3\CMS\Core\Mail\MailerInterface
Definition: MailerInterface.php:28
‪TYPO3\CMS\Core\Authentication\Event\MfaVerificationFailedEvent
Definition: MfaVerificationFailedEvent.php:29
‪TYPO3\CMS\Backend\EventListener
Definition: AfterBackendPageRenderEventListener.php:18
‪TYPO3\CMS\Backend\EventListener\FailedLoginAttemptNotification\sendLoginAttemptEmail
‪sendLoginAttemptEmail(array $previousFailures, ServerRequestInterface $request)
Definition: FailedLoginAttemptNotification.php:135
‪TYPO3\CMS\Backend\EventListener\FailedLoginAttemptNotification
Definition: FailedLoginAttemptNotification.php:47
‪TYPO3\CMS\Core\SysLog\Action\Login
Definition: Login.php:24
‪TYPO3\CMS\Backend\EventListener\FailedLoginAttemptNotification\createPreparedQueryBuilder
‪createPreparedQueryBuilder(int $earliestLogDate, int $loginAction)
Definition: FailedLoginAttemptNotification.php:163
‪TYPO3\CMS\Core\Mail\FluidEmail
Definition: FluidEmail.php:35
‪TYPO3\CMS\Core\SysLog\Error
Definition: Error.php:24
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:62
‪TYPO3\CMS\Backend\EventListener\FailedLoginAttemptNotification\$notificationRecipientEmailAddress
‪string $notificationRecipientEmailAddress
Definition: FailedLoginAttemptNotification.php:49
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:41
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Authentication\Event\MfaVerificationFailedEvent\getUser
‪getUser()
Definition: MfaVerificationFailedEvent.php:38
‪TYPO3\CMS\Core\Authentication\Event\AbstractAuthenticationFailedEvent\getRequest
‪getRequest()
Definition: AbstractAuthenticationFailedEvent.php:48
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\SysLog\Type
Definition: Type.php:28
‪TYPO3\CMS\Core\Log\LogDataTrait
Definition: LogDataTrait.php:25