Skip to content

Commit

Permalink
[BUGFIX] Dispatch event on failed mfa attempt
Browse files Browse the repository at this point in the history
By dispatching a new PSR-14 Event on a failed
multi-factor authentication attempt, it's now
possible to consider such failed attempts in
the "send email on failed login" functionality.

The improved log message does now also use the
correct "action" and "error" types, which allows
further functionality, such as the corresponding
dashboard widget, to make use of those information.

Resolves: #100129
Releases: main, 12.4
Change-Id: I52047bc45dba124f050aa31640444ce4af12c4df
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/79816
Reviewed-by: Oliver Bartsch <bo@cedev.de>
Tested-by: core-ci <typo3@b13.com>
Tested-by: Oliver Bartsch <bo@cedev.de>
  • Loading branch information
o-ba committed Jul 6, 2023
1 parent e0d0047 commit 9e9da14
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 24 deletions.
23 changes: 19 additions & 4 deletions typo3/sysext/backend/Classes/Controller/MfaController.php
Expand Up @@ -17,6 +17,7 @@

namespace TYPO3\CMS\Backend\Controller;

use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
Expand All @@ -27,6 +28,7 @@
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;
Expand Down Expand Up @@ -55,6 +57,7 @@ public function __construct(
protected readonly ExtensionConfiguration $extensionConfiguration,
protected readonly LoggerInterface $logger,
protected readonly BackendViewFactory $backendViewFactory,
protected readonly EventDispatcherInterface $eventDispatcher,
) {
}

Expand Down Expand Up @@ -126,7 +129,14 @@ protected function verifyAction(ServerRequestInterface $request, MfaProviderMani
}
// 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',
Expand Down Expand Up @@ -175,8 +185,13 @@ protected function getAlternativeProviders(MfaProviderManifestInterface $mfaProv
/**
* 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 = [
Expand All @@ -196,7 +211,7 @@ protected function log(string $message, array $additionalData = [], ?MfaProvider
$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);
}
}

Expand Down
Expand Up @@ -21,6 +21,7 @@
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;
Expand Down Expand Up @@ -64,9 +65,10 @@ public function __construct(
* 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
Expand Down
4 changes: 4 additions & 0 deletions typo3/sysext/backend/Configuration/Services.yaml
Expand Up @@ -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:
Expand Down
Expand Up @@ -17,6 +17,7 @@

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;
Expand Down Expand Up @@ -66,7 +67,8 @@ protected function setUp(): void
$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));

Expand Down
@@ -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;
}
}
Expand Up @@ -19,40 +19,25 @@

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
{
return $this->user;
}

public function getRequest(): ServerRequestInterface
{
return $this->request;
}

public function getLoginData(): array
{
return $this->loginData;
Expand Down
@@ -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();
}
}

0 comments on commit 9e9da14

Please sign in to comment.