TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
<?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\Backend\Middleware;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\RateLimiter\LimiterInterface;
|
||||
use TYPO3\CMS\Backend\Routing\Route;
|
||||
use TYPO3\CMS\Backend\Routing\RouteRedirect;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Authentication\Mfa\MfaRequiredException;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Controller\ErrorPageController;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Http\Response;
|
||||
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
|
||||
use TYPO3\CMS\Core\RateLimiter\RateLimiterFactoryInterface;
|
||||
use TYPO3\CMS\Core\RateLimiter\RequestRateLimitedException;
|
||||
use TYPO3\CMS\Core\Session\UserSessionManager;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\HttpUtility;
|
||||
|
||||
/**
|
||||
* Initializes the backend user authentication object (BE_USER) and the global LANG object.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class BackendUserAuthenticator extends \TYPO3\CMS\Core\Middleware\BackendUserAuthenticator
|
||||
{
|
||||
/**
|
||||
* List of requests that don't need a valid BE user
|
||||
*/
|
||||
protected array $publicRoutes = [
|
||||
'/login',
|
||||
'/login/frame',
|
||||
'/login/password-reset/forget',
|
||||
'/login/password-reset/initiate-reset',
|
||||
'/login/password-reset/validate',
|
||||
'/login/password-reset/finish',
|
||||
'/login/request-token',
|
||||
'/install/server-response-check/host',
|
||||
'/install',
|
||||
'/install.php',
|
||||
'/ajax/login',
|
||||
'/ajax/logout',
|
||||
'/ajax/login/preflight',
|
||||
'/ajax/login/refresh',
|
||||
'/ajax/login/timedout',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
Context $context,
|
||||
private readonly LanguageServiceFactory $languageServiceFactory,
|
||||
private readonly RateLimiterFactoryInterface $rateLimiterFactory,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly UriBuilder $uriBuilder,
|
||||
) {
|
||||
parent::__construct($context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the bootstrap process to set up $GLOBALS['BE_USER'] AND $GLOBALS['LANG']
|
||||
*/
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
/** @var Route $route */
|
||||
$route = $request->getAttribute('route');
|
||||
$isAjaxCall = (bool)($route->getOption('ajax') ?? false);
|
||||
|
||||
// The global must be available very early, because methods below
|
||||
// might trigger code which relies on it. See: #45625
|
||||
$GLOBALS['BE_USER'] = GeneralUtility::makeInstance(BackendUserAuthentication::class);
|
||||
// Rate Limiting
|
||||
$rateLimiter = $this->ensureLoginRateLimit($GLOBALS['BE_USER'], $request);
|
||||
// Whether multi-factor authentication is requested
|
||||
$mfaRequested = $route->getOption('_identifier') === 'auth_mfa';
|
||||
try {
|
||||
$GLOBALS['BE_USER']->start($request);
|
||||
} catch (MfaRequiredException $mfaRequiredException) {
|
||||
// If MFA is required and we are not already on the "auth_mfa"
|
||||
// route, force the user to it for further authentication.
|
||||
if (!$mfaRequested && $this->isLoggedInBackendUserRequired($route)) {
|
||||
if ($isAjaxCall) {
|
||||
return new Response(statusCode: 401);
|
||||
}
|
||||
return $this->redirectToMfaEndpoint(
|
||||
'auth_mfa',
|
||||
$GLOBALS['BE_USER'],
|
||||
$request,
|
||||
['identifier' => $mfaRequiredException->getProvider()->getIdentifier()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Register the backend user as aspect and initializing workspace once for TSconfig conditions
|
||||
$this->setBackendUserAspect($GLOBALS['BE_USER'], (int)($GLOBALS['BE_USER']->user['workspace_id'] ?? 0));
|
||||
if ($this->isLoggedInBackendUserRequired($route)) {
|
||||
if (!$this->context->getAspect('backend.user')->isLoggedIn()) {
|
||||
if ($isAjaxCall) {
|
||||
return new Response(statusCode: 401);
|
||||
}
|
||||
$uri = $this->uriBuilder->buildUriWithRedirect(
|
||||
'login',
|
||||
[],
|
||||
RouteRedirect::createFromRoute($route, $request->getQueryParams())
|
||||
);
|
||||
$response = new RedirectResponse($uri);
|
||||
return $this->enrichResponseWithHeadersAndCookieInformation($request, $response, $GLOBALS['BE_USER']);
|
||||
}
|
||||
if (!$GLOBALS['BE_USER']->isUserAllowedToLogin()) {
|
||||
$content = GeneralUtility::makeInstance(ErrorPageController::class)->errorAction(
|
||||
'Login Error',
|
||||
'TYPO3 is in maintenance mode at the moment. Only administrators are allowed access.',
|
||||
1294585860,
|
||||
503
|
||||
);
|
||||
$response = new HtmlResponse($content, 503);
|
||||
return $this->enrichResponseWithHeadersAndCookieInformation($request, $response, $GLOBALS['BE_USER']);
|
||||
}
|
||||
}
|
||||
if ($this->context->getAspect('backend.user')->isLoggedIn()) {
|
||||
$GLOBALS['BE_USER']->initializeBackendLogin($request);
|
||||
// Reset the limiter after successful login
|
||||
if ($rateLimiter) {
|
||||
$rateLimiter->reset();
|
||||
}
|
||||
// In case the current request is not targeted to authenticate against MFA, the "mfa"
|
||||
// key is not yet set in session (indicating that MFA has already been passed) and it's
|
||||
// no "switch-user" mode, check whether the user is required to set up MFA and redirect
|
||||
// to the corresponding setup endpoint if not already on it.
|
||||
if (!$mfaRequested
|
||||
&& !(bool)($GLOBALS['BE_USER']->getSessionData('mfa') ?? false)
|
||||
&& !$GLOBALS['BE_USER']->getOriginalUserIdWhenInSwitchUserMode()
|
||||
&& $GLOBALS['BE_USER']->isMfaSetupRequired()
|
||||
&& $route->getOption('_identifier') !== 'setup_mfa'
|
||||
) {
|
||||
if ($isAjaxCall) {
|
||||
return new Response(statusCode: 401);
|
||||
}
|
||||
return $this->redirectToMfaEndpoint('setup_mfa', $GLOBALS['BE_USER'], $request);
|
||||
}
|
||||
}
|
||||
$GLOBALS['LANG'] = $this->languageServiceFactory->createFromUserPreferences($GLOBALS['BE_USER']);
|
||||
// Re-setting the user and take the workspace from the user object now
|
||||
$this->setBackendUserAspect($GLOBALS['BE_USER']);
|
||||
$response = $handler->handle($request);
|
||||
$this->sessionGarbageCollection();
|
||||
return $this->enrichResponseWithHeadersAndCookieInformation($request, $response, $GLOBALS['BE_USER']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Backend requests should always apply Set-Cookie information and never be cacheable.
|
||||
* This is also needed if there is a redirect from somewhere in the code.
|
||||
*
|
||||
* @throws \TYPO3\CMS\Core\Context\Exception\AspectNotFoundException
|
||||
*/
|
||||
protected function enrichResponseWithHeadersAndCookieInformation(
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
?BackendUserAuthentication $userAuthentication
|
||||
): ResponseInterface {
|
||||
if ($userAuthentication) {
|
||||
// If no backend user is logged-in, the cookie should be removed
|
||||
if (!$this->context->getAspect('backend.user')->isLoggedIn()) {
|
||||
$userAuthentication->removeCookie();
|
||||
}
|
||||
// Ensure to always apply a cookie
|
||||
$response = $userAuthentication->appendCookieToResponse($response, $request->getAttribute('normalizedParams'));
|
||||
}
|
||||
// Additional headers to never cache any PHP request should be sent at any time when
|
||||
// accessing the TYPO3 Backend
|
||||
$response = $this->applyHeadersToResponse($response);
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Garbage collection for be_sessions (with a probability)
|
||||
*/
|
||||
protected function sessionGarbageCollection(): void
|
||||
{
|
||||
UserSessionManager::create('BE')->collectGarbage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate a redirect to the given MFA endpoint with necessary cookies and headers appended
|
||||
*/
|
||||
protected function redirectToMfaEndpoint(
|
||||
string $endpoint,
|
||||
BackendUserAuthentication $user,
|
||||
ServerRequestInterface $request,
|
||||
array $parameters = []
|
||||
): ResponseInterface {
|
||||
$response = new RedirectResponse(
|
||||
$this->uriBuilder->buildUriWithRedirect($endpoint, $parameters, RouteRedirect::createFromRequest($request))
|
||||
);
|
||||
// Add necessary cookies and headers to the response so
|
||||
// the already passed authentication step is not lost.
|
||||
$response = $user->appendCookieToResponse($response, $request->getAttribute('normalizedParams'));
|
||||
$response = $this->applyHeadersToResponse($response);
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is required for the request.
|
||||
* If we're trying to do a login or an ajax login, don't require a user.
|
||||
*
|
||||
* @param Route $route the Route path to check against, something like '
|
||||
* @return bool true when the Route requires an authenticated backend user
|
||||
*/
|
||||
protected function isLoggedInBackendUserRequired(Route $route): bool
|
||||
{
|
||||
return in_array($route->getPath(), $this->publicRoutes, true) === false;
|
||||
}
|
||||
|
||||
protected function ensureLoginRateLimit(BackendUserAuthentication $user, ServerRequestInterface $request): ?LimiterInterface
|
||||
{
|
||||
if (!$user->isActiveLogin($request)) {
|
||||
return null;
|
||||
}
|
||||
$loginRateLimiter = $this->rateLimiterFactory->createLoginRateLimiter($request, $user->loginType);
|
||||
$limit = $loginRateLimiter->consume();
|
||||
if (!$limit->isAccepted()) {
|
||||
$this->logger->debug('Login request has been rate limited for IP address {ipAddress}', ['ipAddress' => $request->getAttribute('normalizedParams')->getRemoteAddress()]);
|
||||
$dateformat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
|
||||
$lockedUntil = $limit->getRetryAfter()->getTimestamp() > 0
|
||||
? ' until ' . date($dateformat, $limit->getRetryAfter()->getTimestamp()) : '';
|
||||
throw new RequestRateLimitedException(
|
||||
HttpUtility::HTTP_STATUS_403,
|
||||
'The login is locked' . $lockedUntil . ' due to too many failed login attempts from your IP address.',
|
||||
'Login Request Rate Limited',
|
||||
1616175867
|
||||
);
|
||||
}
|
||||
return $loginRateLimiter;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user