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; } }