diff --git a/typo3/sysext/backend/Classes/Controller/MfaController.php b/typo3/sysext/backend/Classes/Controller/MfaController.php index 7014363c61c29aaf77af6d982b2e7d1b5849c038..1faf759161fed1dd7e4149a345c0718b84c2e633 100644 --- a/typo3/sysext/backend/Classes/Controller/MfaController.php +++ b/typo3/sysext/backend/Classes/Controller/MfaController.php @@ -17,6 +17,7 @@ declare(strict_types=1); namespace TYPO3\CMS\Backend\Controller; +use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; @@ -27,6 +28,7 @@ use TYPO3\CMS\Backend\Routing\UriBuilder; use TYPO3\CMS\Backend\Template\PageRendererBackendSetupTrait; use TYPO3\CMS\Backend\View\AuthenticationStyleInformation; use TYPO3\CMS\Backend\View\BackendViewFactory; +use TYPO3\CMS\Core\Authentication\Event\MfaVerificationFailedEvent; use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderManifestInterface; use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager; use TYPO3\CMS\Core\Authentication\Mfa\MfaViewType; @@ -55,6 +57,7 @@ class MfaController extends AbstractMfaController protected readonly ExtensionConfiguration $extensionConfiguration, protected readonly LoggerInterface $logger, protected readonly BackendViewFactory $backendViewFactory, + protected readonly EventDispatcherInterface $eventDispatcher, ) { } @@ -126,7 +129,14 @@ class MfaController extends AbstractMfaController } // Call the provider to verify the request if (!$mfaProvider->verify($request, $propertyManager)) { - $this->log('Multi-factor authentication failed for user ###USERNAME###'); + $this->log( + message: 'Multi-factor authentication failed for user \'###USERNAME###\' with provider \'' . $mfaProvider->getIdentifier() . '\'!', + action: Login::ATTEMPT, + error: SystemLogErrorClassification::SECURITY_NOTICE + ); + $this->eventDispatcher->dispatch( + new MfaVerificationFailedEvent($request, $propertyManager) + ); // If failed, initiate a redirect back to the auth view return new RedirectResponse($this->uriBuilder->buildUriWithRedirect( 'auth_mfa', @@ -175,8 +185,13 @@ class MfaController extends AbstractMfaController /** * Log debug information for MFA events */ - protected function log(string $message, array $additionalData = [], ?MfaProviderManifestInterface $mfaProvider = null): void - { + protected function log( + string $message, + array $additionalData = [], + ?MfaProviderManifestInterface $mfaProvider = null, + int $action = Login::LOGIN, + int $error = SystemLogErrorClassification::MESSAGE + ): void { $user = $this->getBackendUser(); $username = $user->user[$user->username_column]; $context = [ @@ -196,7 +211,7 @@ class MfaController extends AbstractMfaController $this->logger->debug($message, $data); if ($user->writeStdLog) { // Write to sys_log if enabled - $user->writelog(SystemLogType::LOGIN, Login::LOGIN, SystemLogErrorClassification::MESSAGE, 1, $message, $data); + $user->writelog(SystemLogType::LOGIN, $action, $error, 1, $message, $data); } } diff --git a/typo3/sysext/backend/Classes/EventListener/FailedLoginAttemptNotification.php b/typo3/sysext/backend/Classes/EventListener/FailedLoginAttemptNotification.php index 047f39ed74a9acb12df216c128bbe94733cb4bf5..62d9f2af18c30bd3d4acb234f76d02008093188a 100644 --- a/typo3/sysext/backend/Classes/EventListener/FailedLoginAttemptNotification.php +++ b/typo3/sysext/backend/Classes/EventListener/FailedLoginAttemptNotification.php @@ -21,6 +21,7 @@ use Psr\Http\Message\ServerRequestInterface; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; use TYPO3\CMS\Core\Authentication\Event\LoginAttemptFailedEvent; +use TYPO3\CMS\Core\Authentication\Event\MfaVerificationFailedEvent; use TYPO3\CMS\Core\Database\Connection; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\QueryBuilder; @@ -64,9 +65,10 @@ final class FailedLoginAttemptNotification * Sends a warning email if there has been a certain amount of failed logins during a period. * If a login fails, this function is called. It will look up the sys_log to see if there * have been more than $failedLoginAttemptsThreshold failed logins the last X seconds - * (default 3600, see $warningPeriod). If so, an email with a warning is sent. + * (default 3600, see $warningPeriod). If so, an email with a warning is sent. This also + * includes failed multi-factor authentication failures. */ - public function __invoke(LoginAttemptFailedEvent $event): void + public function __invoke(LoginAttemptFailedEvent|MfaVerificationFailedEvent $event): void { if (!$event->isBackendAttempt()) { // This notification only works for backend users diff --git a/typo3/sysext/backend/Configuration/Services.yaml b/typo3/sysext/backend/Configuration/Services.yaml index 7fa184a827cca1c8089885307e30305453068d00..486008a713fc19094081db0c27be159d38ee0053 100644 --- a/typo3/sysext/backend/Configuration/Services.yaml +++ b/typo3/sysext/backend/Configuration/Services.yaml @@ -188,6 +188,10 @@ services: tags: - name: event.listener identifier: 'typo3/cms-backend/failed-login-attempt-notification' + event: TYPO3\CMS\Core\Authentication\Event\LoginAttemptFailedEvent + - name: event.listener + identifier: 'typo3/cms-backend/failed-mfa-verification-notification' + event: TYPO3\CMS\Core\Authentication\Event\MfaVerificationFailedEvent TYPO3\CMS\Backend\Security\EmailLoginNotification: tags: diff --git a/typo3/sysext/backend/Tests/Functional/Controller/MfaControllerTest.php b/typo3/sysext/backend/Tests/Functional/Controller/MfaControllerTest.php index 3751a67059189d8fdab0041127d9c89d286e19e8..6550e2e729f9a3f35c61096e2d04099adf24e4b7 100644 --- a/typo3/sysext/backend/Tests/Functional/Controller/MfaControllerTest.php +++ b/typo3/sysext/backend/Tests/Functional/Controller/MfaControllerTest.php @@ -17,6 +17,7 @@ declare(strict_types=1); namespace TYPO3\CMS\Backend\Tests\Functional\Controller; +use Psr\EventDispatcher\EventDispatcherInterface; use TYPO3\CMS\Backend\Controller\MfaController; use TYPO3\CMS\Backend\Routing\Route; use TYPO3\CMS\Backend\Routing\UriBuilder; @@ -66,7 +67,8 @@ final class MfaControllerTest extends FunctionalTestCase $this->get(PageRenderer::class), $this->get(ExtensionConfiguration::class), new Logger('testing'), - $this->get(BackendViewFactory::class) + $this->get(BackendViewFactory::class), + $this->get(EventDispatcherInterface::class) ); $this->subject->injectMfaProviderRegistry($this->get(MfaProviderRegistry::class)); diff --git a/typo3/sysext/core/Classes/Authentication/Event/AbstractAuthenticationFailedEvent.php b/typo3/sysext/core/Classes/Authentication/Event/AbstractAuthenticationFailedEvent.php new file mode 100644 index 0000000000000000000000000000000000000000..49365843920d43b13b2146643ff8cdeb8d4c445f --- /dev/null +++ b/typo3/sysext/core/Classes/Authentication/Event/AbstractAuthenticationFailedEvent.php @@ -0,0 +1,53 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Core\Authentication\Event; + +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication; +use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; + +/** + * Class to be extended by events, fired after authentication has failed + */ +abstract class AbstractAuthenticationFailedEvent +{ + public function __construct( + private readonly ServerRequestInterface $request + ) { + } + + /** + * Returns the user, who failed to authenticate successfully + */ + abstract public function getUser(): AbstractUserAuthentication; + + public function isFrontendAttempt(): bool + { + return !$this->isBackendAttempt(); + } + + public function isBackendAttempt(): bool + { + return $this->getUser() instanceof BackendUserAuthentication; + } + + public function getRequest(): ServerRequestInterface + { + return $this->request; + } +} diff --git a/typo3/sysext/core/Classes/Authentication/Event/LoginAttemptFailedEvent.php b/typo3/sysext/core/Classes/Authentication/Event/LoginAttemptFailedEvent.php index dfadc5bb247e22e0aff504ecbe782e99a0af5d9d..96da574750c59147509d43dbf79694d3511d6415 100644 --- a/typo3/sysext/core/Classes/Authentication/Event/LoginAttemptFailedEvent.php +++ b/typo3/sysext/core/Classes/Authentication/Event/LoginAttemptFailedEvent.php @@ -19,28 +19,18 @@ namespace TYPO3\CMS\Core\Authentication\Event; use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication; -use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; /** * Event fired after a login attempt failed. */ -final class LoginAttemptFailedEvent +final class LoginAttemptFailedEvent extends AbstractAuthenticationFailedEvent { public function __construct( private readonly AbstractUserAuthentication $user, private readonly ServerRequestInterface $request, private readonly array $loginData, ) { - } - - public function isFrontendAttempt(): bool - { - return !$this->isBackendAttempt(); - } - - public function isBackendAttempt(): bool - { - return $this->user instanceof BackendUserAuthentication; + parent::__construct($this->request); } public function getUser(): AbstractUserAuthentication @@ -48,11 +38,6 @@ final class LoginAttemptFailedEvent return $this->user; } - public function getRequest(): ServerRequestInterface - { - return $this->request; - } - public function getLoginData(): array { return $this->loginData; diff --git a/typo3/sysext/core/Classes/Authentication/Event/MfaVerificationFailedEvent.php b/typo3/sysext/core/Classes/Authentication/Event/MfaVerificationFailedEvent.php new file mode 100644 index 0000000000000000000000000000000000000000..ebc3cf1a11e498ff176a954d257e6e92254872ac --- /dev/null +++ b/typo3/sysext/core/Classes/Authentication/Event/MfaVerificationFailedEvent.php @@ -0,0 +1,50 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Core\Authentication\Event; + +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication; +use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager; + +/** + * Event fired after MFA verification failed. + */ +final class MfaVerificationFailedEvent extends AbstractAuthenticationFailedEvent +{ + public function __construct( + private readonly ServerRequestInterface $request, + private readonly MfaProviderPropertyManager $propertyManager, + ) { + parent::__construct($this->request); + } + + public function getUser(): AbstractUserAuthentication + { + return $this->propertyManager->getUser(); + } + + public function getProviderIdentifier(): string + { + return $this->propertyManager->getIdentifier(); + } + + public function getProviderProperties(): array + { + return $this->propertyManager->getProperties(); + } +}