From a4fb013f66a7cd31a1239158aaa4fb6b38fe12c6 Mon Sep 17 00:00:00 2001 From: Benni Mack <benni@typo3.org> Date: Mon, 27 Mar 2023 10:04:03 +0200 Subject: [PATCH] [FEATURE] Replace AbstractUserAuth hooks with PSR-14 Events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new Events are added * TYPO3\CMS\Core\Authentication\Event\BeforeUserLogoutEvent * TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedOutEvent * TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedInEvent They should be used instead of the hooks: * $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_pre_processing'] * $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_post_processing'] * $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['backendUserLogin'] as they are now deprecated. This is an ongoing effort to remove hooks and use a better and more flexible PSR-14-based EventListener system. Resolves: #100307 Releases: main Change-Id: Iec5f96cc052ad5da572c2ba19c77da80a30025f3 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/78263 Tested-by: core-ci <typo3@b13.com> Tested-by: Andreas Fernandez <a.fernandez@scripting-base.de> Reviewed-by: Andreas Fernandez <a.fernandez@scripting-base.de> Tested-by: Stefan Bürk <stefan@buerk.tech> Reviewed-by: Stefan Bürk <stefan@buerk.tech> --- .../FrontendBackendUserAuthentication.php | 3 +- .../Middleware/BackendUserAuthenticator.php | 2 +- .../Security/EmailLoginNotification.php | 35 ++++----- .../backend/Configuration/Services.yaml | 6 ++ .../Security/EmailLoginNotificationTest.php | 57 ++++++-------- typo3/sysext/backend/ext_localconf.php | 2 - .../AbstractUserAuthentication.php | 34 ++++++-- .../BackendUserAuthentication.php | 22 ++++-- .../CommandLineUserAuthentication.php | 2 +- .../Event/AfterUserLoggedInEvent.php | 45 +++++++++++ .../Event/AfterUserLoggedOutEvent.php | 36 +++++++++ .../Event/BeforeUserLogoutEvent.php | 55 +++++++++++++ ...riousHooksRelatedToAuthenticationUsers.rst | 49 ++++++++++++ ...-100307-PSR-14EventsForUserLoginLogout.rst | 78 +++++++++++++++++++ .../BackendUserAuthenticationTest.php | 6 ++ .../Middleware/BackendUserAuthenticator.php | 6 +- .../Php/ArrayDimensionMatcher.php | 18 +++++ .../ReactionUserAuthentication.php | 4 +- 18 files changed, 388 insertions(+), 72 deletions(-) create mode 100644 typo3/sysext/core/Classes/Authentication/Event/AfterUserLoggedInEvent.php create mode 100644 typo3/sysext/core/Classes/Authentication/Event/AfterUserLoggedOutEvent.php create mode 100644 typo3/sysext/core/Classes/Authentication/Event/BeforeUserLogoutEvent.php create mode 100644 typo3/sysext/core/Documentation/Changelog/12.3/Deprecation-100307-VariousHooksRelatedToAuthenticationUsers.rst create mode 100644 typo3/sysext/core/Documentation/Changelog/12.3/Feature-100307-PSR-14EventsForUserLoginLogout.rst diff --git a/typo3/sysext/backend/Classes/FrontendBackendUserAuthentication.php b/typo3/sysext/backend/Classes/FrontendBackendUserAuthentication.php index 0a92a42c144c..89d76f8e59c1 100644 --- a/typo3/sysext/backend/Classes/FrontendBackendUserAuthentication.php +++ b/typo3/sysext/backend/Classes/FrontendBackendUserAuthentication.php @@ -15,6 +15,7 @@ namespace TYPO3\CMS\Backend; +use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; use TYPO3\CMS\Core\Context\Context; @@ -71,7 +72,7 @@ class FrontendBackendUserAuthentication extends BackendUserAuthentication * * @return bool Returns TRUE if access is OK */ - public function backendCheckLogin() + public function backendCheckLogin(ServerRequestInterface $request = null) { if (empty($this->user['uid'])) { return false; diff --git a/typo3/sysext/backend/Classes/Middleware/BackendUserAuthenticator.php b/typo3/sysext/backend/Classes/Middleware/BackendUserAuthenticator.php index 4d88ad4a7c49..062f73e8a30c 100644 --- a/typo3/sysext/backend/Classes/Middleware/BackendUserAuthenticator.php +++ b/typo3/sysext/backend/Classes/Middleware/BackendUserAuthenticator.php @@ -134,7 +134,7 @@ class BackendUserAuthenticator extends \TYPO3\CMS\Core\Middleware\BackendUserAut } } if ($this->context->getAspect('backend.user')->isLoggedIn()) { - $GLOBALS['BE_USER']->initializeBackendLogin(); + $GLOBALS['BE_USER']->initializeBackendLogin($request); // Reset the limiter after successful login if ($rateLimiter) { $rateLimiter->reset(); diff --git a/typo3/sysext/backend/Classes/Security/EmailLoginNotification.php b/typo3/sysext/backend/Classes/Security/EmailLoginNotification.php index e31bd56c79d4..0802faf6a093 100644 --- a/typo3/sysext/backend/Classes/Security/EmailLoginNotification.php +++ b/typo3/sysext/backend/Classes/Security/EmailLoginNotification.php @@ -24,6 +24,7 @@ use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mime\Exception\RfcComplianceException; use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedInEvent; use TYPO3\CMS\Core\Http\ServerRequestFactory; use TYPO3\CMS\Core\Mail\FluidEmail; use TYPO3\CMS\Core\Mail\MailerInterface; @@ -39,46 +40,41 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; * * @internal this is not part of TYPO3 API as this is an internal hook */ -class EmailLoginNotification implements LoggerAwareInterface +final class EmailLoginNotification implements LoggerAwareInterface { use LoggerAwareTrait; - /** - * @var int - */ - private $warningMode; - - /** - * @var string - */ - private $warningEmailRecipient; + private int $warningMode = 0; + private string $warningEmailRecipient = ''; /** * @var ServerRequestInterface */ private $request; - public function __construct() - { + public function __construct( + private readonly MailerInterface $mailer + ) { $this->warningMode = (int)($GLOBALS['TYPO3_CONF_VARS']['BE']['warning_mode'] ?? 0); $this->warningEmailRecipient = $GLOBALS['TYPO3_CONF_VARS']['BE']['warning_email_addr'] ?? ''; } /** * Sends an email notification to warning_email_address and/or the logged-in user's email address. - * - * @param array $parameters array data - * @param BackendUserAuthentication $currentUser the currently just-logged in user */ - public function emailAtLogin(array $parameters, BackendUserAuthentication $currentUser): void + public function emailAtLogin(AfterUserLoggedInEvent $event): void { - $user = $parameters['user']; + if (!$event->getUser() instanceof BackendUserAuthentication) { + return; + } + $currentUser = $event->getUser(); + $user = $currentUser->user; $genericLoginWarning = $this->warningMode > 0 && !empty($this->warningEmailRecipient); $userLoginNotification = ($currentUser->uc['emailMeAtLogin'] ?? null) && GeneralUtility::validEmail($user['email']); if (!$genericLoginWarning && !$userLoginNotification) { return; } - $this->request = $parameters['request'] ?? $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals(); + $this->request = $event->getRequest() ?? $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals(); if ($genericLoginWarning) { $prefix = $currentUser->isAdmin() ? '[AdminLoginWarning]' : '[LoginWarning]'; @@ -114,8 +110,7 @@ class EmailLoginNotification implements LoggerAwareInterface 'headline' => $headline, ]); try { - // TODO: DI should be used to inject the MailerInterface - GeneralUtility::makeInstance(MailerInterface::class)->send($email); + $this->mailer->send($email); } catch (TransportException $e) { $this->logger->warning('Could not send notification email to "{recipient}" due to mailer settings error', [ 'recipient' => $recipient, diff --git a/typo3/sysext/backend/Configuration/Services.yaml b/typo3/sysext/backend/Configuration/Services.yaml index 48dc69a12684..deb99a50d334 100644 --- a/typo3/sysext/backend/Configuration/Services.yaml +++ b/typo3/sysext/backend/Configuration/Services.yaml @@ -173,6 +173,12 @@ services: - name: event.listener identifier: 'typo3/cms-backend/failed-login-attempt-notification' + TYPO3\CMS\Backend\Security\EmailLoginNotification: + tags: + - name: event.listener + identifier: 'typo3/cms-backend/login-notification' + method: 'emailAtLogin' + TYPO3\CMS\Backend\EventListener\SilentSiteLanguageFlagMigration: tags: - name: event.listener diff --git a/typo3/sysext/backend/Tests/Unit/Security/EmailLoginNotificationTest.php b/typo3/sysext/backend/Tests/Unit/Security/EmailLoginNotificationTest.php index 0a34601c0d39..ce1f2a6da133 100644 --- a/typo3/sysext/backend/Tests/Unit/Security/EmailLoginNotificationTest.php +++ b/typo3/sysext/backend/Tests/Unit/Security/EmailLoginNotificationTest.php @@ -20,6 +20,7 @@ namespace TYPO3\CMS\Backend\Tests\Unit\Security; use PHPUnit\Framework\MockObject\MockObject; use TYPO3\CMS\Backend\Security\EmailLoginNotification; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedInEvent; use TYPO3\CMS\Core\Mail\FluidEmail; use TYPO3\CMS\Core\Mail\MailerInterface; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -39,18 +40,16 @@ class EmailLoginNotificationTest extends UnitTestCase ->disableOriginalConstructor() ->getMock(); $backendUser->uc['emailMeAtLogin'] = 1; - - $userData = [ + $backendUser->user = [ 'email' => 'test@acme.com', ]; $mailMessage = $this->setUpMailMessageMock(); $mailerMock = $this->createMock(MailerInterface::class); $mailerMock->expects(self::once())->method('send')->with($mailMessage); - GeneralUtility::addInstance(MailerInterface::class, $mailerMock); - $subject = new EmailLoginNotification(); - $subject->emailAtLogin(['user' => $userData], $backendUser); + $subject = new EmailLoginNotification($mailerMock); + $subject->emailAtLogin(new AfterUserLoggedInEvent($backendUser)); } /** @@ -65,14 +64,14 @@ class EmailLoginNotificationTest extends UnitTestCase ->disableOriginalConstructor() ->getMock(); $backendUser->uc['emailMeAtLogin'] = 0; - - $userData = [ + $backendUser->user = [ 'username' => 'karl', 'email' => 'test@acme.com', ]; + $mailerMock = $this->createMock(MailerInterface::class); - $subject = new EmailLoginNotification(); - $subject->emailAtLogin(['user' => $userData], $backendUser); + $subject = new EmailLoginNotification($mailerMock); + $subject->emailAtLogin(new AfterUserLoggedInEvent($backendUser)); // no additional assertion here, as the test would fail due to missing mail mocking if it actually tried to send an email } @@ -89,14 +88,14 @@ class EmailLoginNotificationTest extends UnitTestCase ->disableOriginalConstructor() ->getMock(); $backendUser->uc['emailMeAtLogin'] = 1; - - $userData = [ + $backendUser->user = [ 'username' => 'karl', 'email' => 'dot.com', ]; + $mailerMock = $this->createMock(MailerInterface::class); - $subject = new EmailLoginNotification(); - $subject->emailAtLogin(['user' => $userData], $backendUser); + $subject = new EmailLoginNotification($mailerMock); + $subject->emailAtLogin(new AfterUserLoggedInEvent($backendUser)); // no additional assertion here, as the test would fail due to missing mail mocking if it actually tried to send an email } @@ -115,18 +114,16 @@ class EmailLoginNotificationTest extends UnitTestCase ->disableOriginalConstructor() ->getMock(); $backendUser->method('isAdmin')->willReturn(true); - - $userData = [ + $backendUser->user = [ 'username' => 'karl', ]; $mailMessage = $this->setUpMailMessageMock('typo3-admin@acme.com'); $mailerMock = $this->createMock(MailerInterface::class); $mailerMock->expects(self::once())->method('send')->with($mailMessage); - GeneralUtility::addInstance(MailerInterface::class, $mailerMock); - $subject = new EmailLoginNotification(); - $subject->emailAtLogin(['user' => $userData], $backendUser); + $subject = new EmailLoginNotification($mailerMock); + $subject->emailAtLogin(new AfterUserLoggedInEvent($backendUser)); } /** @@ -143,18 +140,16 @@ class EmailLoginNotificationTest extends UnitTestCase ->disableOriginalConstructor() ->getMock(); $backendUser->method('isAdmin')->willReturn(true); - - $userData = [ + $backendUser->user = [ 'username' => 'karl', ]; $mailMessage = $this->setUpMailMessageMock('typo3-admin@acme.com'); $mailerMock = $this->createMock(MailerInterface::class); $mailerMock->expects(self::once())->method('send')->with($mailMessage); - GeneralUtility::addInstance(MailerInterface::class, $mailerMock); - $subject = new EmailLoginNotification(); - $subject->emailAtLogin(['user' => $userData], $backendUser); + $subject = new EmailLoginNotification($mailerMock); + $subject->emailAtLogin(new AfterUserLoggedInEvent($backendUser)); } /** @@ -171,18 +166,16 @@ class EmailLoginNotificationTest extends UnitTestCase ->disableOriginalConstructor() ->getMock(); $backendUser->method('isAdmin')->willReturn(false); - - $userData = [ + $backendUser->user = [ 'username' => 'karl', ]; $mailMessage = $this->setUpMailMessageMock('typo3-admin@acme.com'); $mailerMock = $this->createMock(MailerInterface::class); $mailerMock->expects(self::once())->method('send')->with($mailMessage); - GeneralUtility::addInstance(MailerInterface::class, $mailerMock); - $subject = new EmailLoginNotification(); - $subject->emailAtLogin(['user' => $userData], $backendUser); + $subject = new EmailLoginNotification($mailerMock); + $subject->emailAtLogin(new AfterUserLoggedInEvent($backendUser)); } /** @@ -199,13 +192,13 @@ class EmailLoginNotificationTest extends UnitTestCase ->disableOriginalConstructor() ->getMock(); $backendUser->method('isAdmin')->willReturn(false); - - $userData = [ + $backendUser->user = [ 'username' => 'karl', ]; + $mailerMock = $this->createMock(MailerInterface::class); - $subject = new EmailLoginNotification(); - $subject->emailAtLogin(['user' => $userData], $backendUser); + $subject = new EmailLoginNotification($mailerMock); + $subject->emailAtLogin(new AfterUserLoggedInEvent($backendUser)); // no additional assertion here as the test would fail due to not mocking the email API } diff --git a/typo3/sysext/backend/ext_localconf.php b/typo3/sysext/backend/ext_localconf.php index aac771a2ff9b..f89075407952 100644 --- a/typo3/sysext/backend/ext_localconf.php +++ b/typo3/sysext/backend/ext_localconf.php @@ -4,7 +4,6 @@ declare(strict_types=1); use TYPO3\CMS\Backend\Backend\Avatar\DefaultAvatarProvider; use TYPO3\CMS\Backend\LoginProvider\UsernamePasswordLoginProvider; -use TYPO3\CMS\Backend\Security\EmailLoginNotification; use TYPO3\CMS\Backend\View\BackendLayout\PageTsBackendLayoutDataProvider; defined('TYPO3') or die(); @@ -25,4 +24,3 @@ $GLOBALS['TYPO3_CONF_VARS']['SYS']['livesearch']['page'] = 'pages'; // Register BackendLayoutDataProvider for PageTs $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider']['pagets'] = PageTsBackendLayoutDataProvider::class; -$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['backendUserLogin']['sendEmailOnLogin'] = EmailLoginNotification::class . '->emailAtLogin'; diff --git a/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php b/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php index 42a6fdeb3fc5..8bed9c67fbe5 100644 --- a/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php +++ b/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php @@ -21,7 +21,9 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use Symfony\Component\HttpFoundation\Cookie; +use TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedOutEvent; use TYPO3\CMS\Core\Authentication\Event\BeforeRequestTokenProcessedEvent; +use TYPO3\CMS\Core\Authentication\Event\BeforeUserLogoutEvent; use TYPO3\CMS\Core\Authentication\Event\LoginAttemptFailedEvent; use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderRegistry; use TYPO3\CMS\Core\Authentication\Mfa\MfaRequiredException; @@ -879,14 +881,36 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface { $this->logger->debug('logoff: ses_id = {session}', ['session' => sha1($this->userSession->getIdentifier())]); - $_params = []; - foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_pre_processing'] ?? [] as $_funcRef) { - if ($_funcRef) { - GeneralUtility::callUserFunction($_funcRef, $_params, $this); + $dispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); + + $event = new BeforeUserLogoutEvent($this); + $event = $dispatcher->dispatch($event); + + if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_pre_processing'] ?? null)) { + trigger_error( + '$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'t3lib/class.t3lib_userauth.php\'][\'logoff_pre_processing\'] will be removed in TYPO3 v13.0. Use the PSR-14 "BeforeUserLogoutEvent" instead.', + E_USER_DEPRECATED + ); + } + + if ($event->shouldLogout()) { + $_params = []; + foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_pre_processing'] ?? [] as $_funcRef) { + if ($_funcRef) { + GeneralUtility::callUserFunction($_funcRef, $_params, $this); + } } + $this->performLogoff(); } - $this->performLogoff(); + $dispatcher->dispatch(new AfterUserLoggedOutEvent($this)); + + if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_post_processing'] ?? null)) { + trigger_error( + '$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'t3lib/class.t3lib_userauth.php\'][\'logoff_post_processing\'] will be removed in TYPO3 v13.0. Use the PSR-14 "BeforeUserLogoutEvent" instead.', + E_USER_DEPRECATED + ); + } foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_post_processing'] ?? [] as $_funcRef) { if ($_funcRef) { GeneralUtility::callUserFunction($_funcRef, $_params, $this); diff --git a/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php b/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php index d40f03e474f2..2c44304cccb9 100644 --- a/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php +++ b/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php @@ -15,8 +15,11 @@ namespace TYPO3\CMS\Core\Authentication; +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Backend\Module\ModuleProvider; use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedInEvent; use TYPO3\CMS\Core\Cache\CacheManager; use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Database\Connection; @@ -2066,7 +2069,7 @@ class BackendUserAuthentication extends AbstractUserAuthentication * @throws \RuntimeException * @todo deprecate */ - public function backendCheckLogin() + public function backendCheckLogin(ServerRequestInterface $request = null) { if (empty($this->user['uid'])) { // @todo: throw a proper AccessDeniedException in TYPO3 v12.0. and handle this functionality in the calling code @@ -2075,7 +2078,7 @@ class BackendUserAuthentication extends AbstractUserAuthentication throw new ImmediateResponseException(new RedirectResponse($url, 303), 1607271747); } if ($this->isUserAllowedToLogin()) { - $this->initializeBackendLogin(); + $this->initializeBackendLogin($request); } else { // @todo: throw a proper AccessDeniedException in TYPO3 v12.0. throw new \RuntimeException('Login Error: TYPO3 is in maintenance mode at the moment. Only administrators are allowed access.', 1294585860); @@ -2085,7 +2088,7 @@ class BackendUserAuthentication extends AbstractUserAuthentication /** * @internal */ - public function initializeBackendLogin(): void + public function initializeBackendLogin(ServerRequestInterface $request = null): void { // The groups are fetched and ready for permission checking in this initialization. // Tables.php must be read before this because stuff like the modules has impact in this @@ -2101,9 +2104,18 @@ class BackendUserAuthentication extends AbstractUserAuthentication ->getConnectionForTable($this->user_table) ->update($this->user_table, ['password_reset_token' => ''], ['uid' => $this->user['uid']]); } + + $event = new AfterUserLoggedInEvent($this, $request); + GeneralUtility::makeInstance(EventDispatcherInterface::class)->dispatch($event); // Process hooks - $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['backendUserLogin']; - foreach ($hooks ?? [] as $_funcRef) { + $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['backendUserLogin'] ?? []; + if (!empty($hooks)) { + trigger_error( + '$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'t3lib/class.t3lib_userauthgroup.php\'][\'backendUserLogin\'] will be removed in TYPO3 v13.0. Use the PSR-14 "AfterUserLoggedInEvent" instead.', + E_USER_DEPRECATED + ); + } + foreach ($hooks as $_funcRef) { $_params = ['user' => $this->user]; GeneralUtility::callUserFunction($_funcRef, $_params, $this); } diff --git a/typo3/sysext/core/Classes/Authentication/CommandLineUserAuthentication.php b/typo3/sysext/core/Classes/Authentication/CommandLineUserAuthentication.php index c9e8d4352690..2d7d43c89457 100644 --- a/typo3/sysext/core/Classes/Authentication/CommandLineUserAuthentication.php +++ b/typo3/sysext/core/Classes/Authentication/CommandLineUserAuthentication.php @@ -113,7 +113,7 @@ class CommandLineUserAuthentication extends BackendUserAuthentication /** * Logs in the TYPO3 Backend user "_cli_" */ - public function backendCheckLogin() + public function backendCheckLogin(ServerRequestInterface $request = null) { $this->authenticate(); } diff --git a/typo3/sysext/core/Classes/Authentication/Event/AfterUserLoggedInEvent.php b/typo3/sysext/core/Classes/Authentication/Event/AfterUserLoggedInEvent.php new file mode 100644 index 000000000000..15c35c64762a --- /dev/null +++ b/typo3/sysext/core/Classes/Authentication/Event/AfterUserLoggedInEvent.php @@ -0,0 +1,45 @@ +<?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; + +/** + * Event fired after a user has been actively logged in (incl. possible MFA). + * Currently only working in BE context, but might get opened up in FE context + * as well in TYPO3 v13+. + */ +final class AfterUserLoggedInEvent +{ + public function __construct( + private readonly AbstractUserAuthentication $user, + private readonly ?ServerRequestInterface $request = null + ) { + } + + public function getUser(): AbstractUserAuthentication + { + return $this->user; + } + + public function getRequest(): ?ServerRequestInterface + { + return $this->request; + } +} diff --git a/typo3/sysext/core/Classes/Authentication/Event/AfterUserLoggedOutEvent.php b/typo3/sysext/core/Classes/Authentication/Event/AfterUserLoggedOutEvent.php new file mode 100644 index 000000000000..599369b32ac8 --- /dev/null +++ b/typo3/sysext/core/Classes/Authentication/Event/AfterUserLoggedOutEvent.php @@ -0,0 +1,36 @@ +<?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 TYPO3\CMS\Core\Authentication\AbstractUserAuthentication; + +/** + * Event fired after a user has been actively logged out. + */ +final class AfterUserLoggedOutEvent +{ + public function __construct( + private readonly AbstractUserAuthentication $user + ) { + } + + public function getUser(): AbstractUserAuthentication + { + return $this->user; + } +} diff --git a/typo3/sysext/core/Classes/Authentication/Event/BeforeUserLogoutEvent.php b/typo3/sysext/core/Classes/Authentication/Event/BeforeUserLogoutEvent.php new file mode 100644 index 000000000000..98246b515769 --- /dev/null +++ b/typo3/sysext/core/Classes/Authentication/Event/BeforeUserLogoutEvent.php @@ -0,0 +1,55 @@ +<?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 TYPO3\CMS\Core\Authentication\AbstractUserAuthentication; + +/** + * Event fired before a user is going to be actively logged out. + * An option to interrupt the regular logout flow from TYPO3 Core (so you can do this yourself) + * is also available. + */ +final class BeforeUserLogoutEvent +{ + private bool $shouldLogout = true; + + public function __construct( + private readonly AbstractUserAuthentication $user + ) { + } + + public function getUser(): AbstractUserAuthentication + { + return $this->user; + } + + public function disableRegularLogoutProcess(): void + { + $this->shouldLogout = false; + } + + public function enableRegularLogoutProcess(): void + { + $this->shouldLogout = true; + } + + public function shouldLogout(): bool + { + return $this->shouldLogout; + } +} diff --git a/typo3/sysext/core/Documentation/Changelog/12.3/Deprecation-100307-VariousHooksRelatedToAuthenticationUsers.rst b/typo3/sysext/core/Documentation/Changelog/12.3/Deprecation-100307-VariousHooksRelatedToAuthenticationUsers.rst new file mode 100644 index 000000000000..f6dc7656be93 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/12.3/Deprecation-100307-VariousHooksRelatedToAuthenticationUsers.rst @@ -0,0 +1,49 @@ +.. include:: /Includes.rst.txt + +.. _deprecation-100307-1679924603: + +==================================================================== +Deprecation: #100307 - Various hooks related to authentication users +==================================================================== + +See :issue:`100307` + +Description +=========== + +The following hooks have been marked as deprecated: + +* :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_pre_processing']` +* :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_post_processing']` +* :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['backendUserLogin']` + +They can be used to add notifications or actions to a TYPO3 installation +after a frontend user or a backend user has been actively logged +in or logged out. + +Impact +====== + +If one of the hooks is registered in a TYPO3 installation, +a PHP :php:`E_USER_DEPRECATED` error is triggered when a user logs +in or logs out. + + +Affected installations +====================== + +TYPO3 installations with custom extensions using one of these hooks. + +The extension scanner detects any usage of the hooks. + + +Migration +========= + +Migrate to the newly introduced PSR-14 events: + +* :php:`\TYPO3\CMS\Core\Authentication\Event\BeforeUserLogoutEvent` +* :php:`\TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedOutEvent` +* :php:`\TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedInEvent` + +.. index:: Backend, Frontend, PHP-API, FullyScanned, ext:core diff --git a/typo3/sysext/core/Documentation/Changelog/12.3/Feature-100307-PSR-14EventsForUserLoginLogout.rst b/typo3/sysext/core/Documentation/Changelog/12.3/Feature-100307-PSR-14EventsForUserLoginLogout.rst new file mode 100644 index 000000000000..048fd5ea18a3 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/12.3/Feature-100307-PSR-14EventsForUserLoginLogout.rst @@ -0,0 +1,78 @@ +.. include:: /Includes.rst.txt + +.. _feature-100307-1679924551: + +======================================================== +Feature: #100307 - PSR-14 Events for User Login & Logout +======================================================== + +See :issue:`100307` + +Description +=========== + +Three new PSR-14 events have been added: + +- :php:`\TYPO3\CMS\Core\Authentication\Event\BeforeUserLogoutEvent` +- :php:`\TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedOutEvent` +- :php:`\TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedInEvent` + +The purpose of these events is to trigger any kind of action when a user +has been successfully logged in or logged out. + +TYPO3 Core itself uses :php:`AfterUserLoggedInEvent` in the TYPO3 Backend +to send an email to a user if the has successfully logged in. + +The event features the following methods: + +- :php:`getUser()`: Returns the :php:`\TYPO3\CMS\Core\Authentication\AbstractUserAuthentication` derivative in question + +The PSR-14 event :php:`BeforeUserLogoutEvent` on top has the possibility +to bypass the regular logout process by TYPO3 (removing the cookie and +the user session) by calling :php:`$event->disableRegularLogoutProcess()` +in an Event Listener. + +The PSR-14 event :php:`AfterUserLoggedInEvent` contains the method +:php:`getRequest()` to return PSR-7 Request object of the current request. + +Registration of the event in your extension's :file:`Services.yaml`: + +.. code-block:: yaml + :caption: EXT:my_extension/Configuration/Services.yaml + + MyVendor\MyExtension\EventListener\ExampleEventListener: + tags: + - name: event.listener + identifier: 'exampleEventListener' + +The corresponding event listener class for :php:`AfterUserLoggedInEvent`: + +.. code-block:: php + :caption: EXT:my_extension/Classes/EventListener/ExampleEventListener.php + + namespace MyVendor\MyExtension\EventListener; + + use TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedInEvent; + + final class ExampleEventListener + { + public function __invoke(AfterUserLoggedInEvent $event): void + { + if ( + $event->getUser() instanceof BackendUserAuthentication + && $event->getUser()->isAdmin() + ) + { + // Do something like: Clear all caches after login + } + } + } + + +Impact +====== + +It is now possible to modify and adapt user functionality based on successful +login or active logout. + +.. index:: Backend, Frontend, PHP-API, ext:core diff --git a/typo3/sysext/core/Tests/Unit/Authentication/BackendUserAuthenticationTest.php b/typo3/sysext/core/Tests/Unit/Authentication/BackendUserAuthenticationTest.php index 7ad30df6f43b..0e3019ff6e0d 100644 --- a/typo3/sysext/core/Tests/Unit/Authentication/BackendUserAuthenticationTest.php +++ b/typo3/sysext/core/Tests/Unit/Authentication/BackendUserAuthenticationTest.php @@ -17,6 +17,8 @@ declare(strict_types=1); namespace TYPO3\CMS\Core\Tests\Unit\Authentication; +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\EventDispatcher\ListenerProviderInterface; use Psr\Log\NullLogger; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; use TYPO3\CMS\Core\Authentication\IpLocker; @@ -26,6 +28,7 @@ use TYPO3\CMS\Core\Database\Connection; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder; use TYPO3\CMS\Core\Database\Query\QueryBuilder; +use TYPO3\CMS\Core\EventDispatcher\EventDispatcher; use TYPO3\CMS\Core\FormProtection\BackendFormProtection; use TYPO3\CMS\Core\FormProtection\FormProtectionFactory; use TYPO3\CMS\Core\Localization\LanguageService; @@ -42,6 +45,8 @@ use TYPO3\TestingFramework\Core\Unit\UnitTestCase; class BackendUserAuthenticationTest extends UnitTestCase { + protected bool $resetSingletonInstances = true; + /** * @test */ @@ -68,6 +73,7 @@ class BackendUserAuthenticationTest extends UnitTestCase ); GeneralUtility::addInstance(FormProtectionFactory::class, $formProtectionFactory); GeneralUtility::addInstance(BackendFormProtection::class, $formProtectionMock); + GeneralUtility::setSingletonInstance(EventDispatcherInterface::class, new EventDispatcher($this->createMock(ListenerProviderInterface::class))); $sessionBackendMock = $this->createMock(SessionBackendInterface::class); $sessionBackendMock->method('remove')->with(self::anything())->willReturn(true); diff --git a/typo3/sysext/frontend/Classes/Middleware/BackendUserAuthenticator.php b/typo3/sysext/frontend/Classes/Middleware/BackendUserAuthenticator.php index 95fea6fbba9a..74f6573eea97 100644 --- a/typo3/sysext/frontend/Classes/Middleware/BackendUserAuthenticator.php +++ b/typo3/sysext/frontend/Classes/Middleware/BackendUserAuthenticator.php @@ -104,7 +104,7 @@ class BackendUserAuthenticator extends \TYPO3\CMS\Core\Middleware\BackendUserAut $backendUserObject->fetchGroupData(); } // Unset the user initialization if any setting / restriction applies - if (!$this->isAuthenticated($backendUserObject, $request->getAttribute('normalizedParams'))) { + if (!$this->isAuthenticated($backendUserObject, $request, $request->getAttribute('normalizedParams'))) { $backendUserObject = null; $this->setBackendUserAspect(null); } @@ -115,7 +115,7 @@ class BackendUserAuthenticator extends \TYPO3\CMS\Core\Middleware\BackendUserAut * Implementing the access checks that the TYPO3 CMS bootstrap script does before a user is ever logged in. * Returns TRUE if access is OK */ - protected function isAuthenticated(FrontendBackendUserAuthentication $user, NormalizedParams $normalizedParams): bool + protected function isAuthenticated(FrontendBackendUserAuthentication $user, ServerRequestInterface $request, NormalizedParams $normalizedParams): bool { // Check IP $ipMask = trim($GLOBALS['TYPO3_CONF_VARS']['BE']['IPmaskList'] ?? ''); @@ -126,6 +126,6 @@ class BackendUserAuthenticator extends \TYPO3\CMS\Core\Middleware\BackendUserAut if ((bool)$GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSL'] && !$normalizedParams->isHttps()) { return false; } - return $user->backendCheckLogin(); + return $user->backendCheckLogin($request); } } diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/ArrayDimensionMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/ArrayDimensionMatcher.php index 3ddc8f2bfcec..57fc5c720d4c 100644 --- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/ArrayDimensionMatcher.php +++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/ArrayDimensionMatcher.php @@ -998,4 +998,22 @@ return [ 'Feature-100278-PSR-14EventAfterFailedLoginsInBackendOrFrontendUsers.rst', ], ], + '$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'t3lib/class.t3lib_userauth.php\'][\'logoff_pre_processing\']' => [ + 'restFiles' => [ + 'Deprecation-100307-VariousHooksRelatedToAuthenticationUsers.rst', + 'Feature-100307-PSR-14EventsForUserLoginLogout.rst', + ], + ], + '$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'t3lib/class.t3lib_userauth.php\'][\'logoff_post_processing\']' => [ + 'restFiles' => [ + 'Deprecation-100307-VariousHooksRelatedToAuthenticationUsers.rst', + 'Feature-100307-PSR-14EventsForUserLoginLogout.rst', + ], + ], + '$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'t3lib/class.t3lib_userauthgroup.php\'][\'backendUserLogin\']' => [ + 'restFiles' => [ + 'Deprecation-100307-VariousHooksRelatedToAuthenticationUsers.rst', + 'Feature-100307-PSR-14EventsForUserLoginLogout.rst', + ], + ], ]; diff --git a/typo3/sysext/reactions/Classes/Authentication/ReactionUserAuthentication.php b/typo3/sysext/reactions/Classes/Authentication/ReactionUserAuthentication.php index 02122c1f8fb5..2797be4b91cd 100644 --- a/typo3/sysext/reactions/Classes/Authentication/ReactionUserAuthentication.php +++ b/typo3/sysext/reactions/Classes/Authentication/ReactionUserAuthentication.php @@ -66,7 +66,7 @@ class ReactionUserAuthentication extends BackendUserAuthentication return null; } - public function backendCheckLogin(): void + public function backendCheckLogin(ServerRequestInterface $request = null): void { // do nothing } @@ -82,7 +82,7 @@ class ReactionUserAuthentication extends BackendUserAuthentication return (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly'] === 0; } - public function initializeBackendLogin(): void + public function initializeBackendLogin(ServerRequestInterface $request = null): void { throw new \RuntimeException('Login Error: No login possible for reaction.', 1669800914); } -- GitLab