createLoginLogout($request, (bool)($request->getParsedBody()['loginRefresh'] ?? $request->getQueryParams()['loginRefresh'] ?? false)); } /** * Calls the main function but with loginRefresh enabled at any time */ public function refreshAction(ServerRequestInterface $request): ResponseInterface { return $this->createLoginLogout($request, true); } /** * @param bool $loginRefresh The backend triggers this with this value set when the login is * close to being expired and the form needs to be redrawn. * @throws PropagateResponseException * @throws RouteNotFoundException */ protected function createLoginLogout(ServerRequestInterface $request, bool $loginRefresh): ResponseInterface { $backendUser = $this->getBackendUserAuthentication(); if (!empty($backendUser->user['uid'])) { // If BE user is logged in, redirect to backend. Also handles "refresh" foo. $this->checkRedirect($request, $loginRefresh); } $languageService = $this->getLanguageService(); if (empty($backendUser->user['uid'])) { // If no user is logged in, initialize LanguageService with preferred browser language and set the // language to the backend user object, so labels in fluid views are translated. $httpAcceptLanguage = $request->getServerParams()['HTTP_ACCEPT_LANGUAGE'] ?? ''; $preferredBrowserLanguage = $this->locales->getPreferredClientLanguage($httpAcceptLanguage); $languageService->init($this->locales->createLocale($preferredBrowserLanguage)); $backendUser->user['lang'] = $preferredBrowserLanguage; } if (($backgroundImageStyles = $this->authenticationStyleInformation->getBackgroundImageStyles($request)) !== '') { $this->pageRenderer->addCssInlineBlock('loginBackgroundImage', $backgroundImageStyles, null, false, true); } if (($highlightColorStyles = $this->authenticationStyleInformation->getHighlightColorStyles()) !== '') { $this->pageRenderer->addCssInlineBlock('loginHighlightColor', $highlightColorStyles, null, false, true); } $loginProviderIdentifier = $this->loginProviderResolver->resolveLoginProviderIdentifierFromRequest($request, 'be_lastLoginProvider'); if (empty($backendUser->user['uid'])) { // Show login form $action = 'login'; $formActionUrl = $this->uriBuilder->buildUriWithRedirect('login', ['loginProvider' => $loginProviderIdentifier], RouteRedirect::createFromRequest($request)); } else { // Show logout form $action = 'logout'; $formActionUrl = $this->uriBuilder->buildUriFromRoute('logout'); } $forgotPasswordUrl = $this->uriBuilder->buildUriWithRedirect('password_forget', ['loginProvider' => $loginProviderIdentifier], RouteRedirect::createFromRequest($request)); $viewVariables = [ 'copyright' => $this->typo3Information->getCopyrightNotice(), 'loginFootnote' => $this->authenticationStyleInformation->getFooterNote(), 'referrerCheckEnabled' => $this->features->isFeatureEnabled('security.backend.enforceReferrer'), 'loginUrl' => (string)$request->getUri(), 'loginProviderIdentifier' => $loginProviderIdentifier, 'backendUser' => $backendUser->user, 'hasLoginError' => $this->isLoginInProgress($request), 'action' => $action, 'formActionUrl' => $formActionUrl, 'requestTokenName' => RequestToken::PARAM_NAME, 'requestTokenValue' => $this->provideRequestTokenJwt(), 'forgetPasswordUrl' => $forgotPasswordUrl, 'loginRefresh' => $loginRefresh, 'loginProviders' => $this->loginProviderResolver->getLoginProviders(), 'loginNewsItems' => $this->getSystemNews(), ]; $this->setUpBasicPageRendererForBackend($this->pageRenderer, $this->extensionConfiguration, $request, $languageService); $this->pageRenderer->setTitle('TYPO3 CMS Login: ' . ($GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? '')); $loginProviderConfiguration = $this->loginProviderResolver->getLoginProviderConfigurationByIdentifier($loginProviderIdentifier); $loginProvider = GeneralUtility::makeInstance($loginProviderConfiguration['provider']); if (!$loginProvider instanceof LoginProviderInterface) { throw new \RuntimeException($loginProviderConfiguration['provider'] . ' must implement LoginProviderInterface', 1724772171); } $viewFactoryData = new ViewFactoryData( templateRootPaths: ['EXT:backend/Resources/Private/Templates'], partialRootPaths: ['EXT:backend/Resources/Private/Partials'], layoutRootPaths: ['EXT:backend/Resources/Private/Layouts'], request: $request, ); $view = $this->viewFactory->create($viewFactoryData); $view->assignMultiple($viewVariables); $this->eventDispatcher->dispatch(new ModifyPageLayoutOnLoginProviderSelectionEvent($view, $request)); $templateFile = $loginProvider->modifyView($request, $view); $content = $view->render($templateFile); $this->pageRenderer->setBodyContent('' . $content); $response = $this->pageRenderer->renderResponse($request); return $this->appendLoginProviderCookie($request, $response); } /** * Returns a new request-token value, which is signed by a new nonce value (the nonce is sent * as cookie automatically in `RequestTokenMiddleware` since it is created via the `NoncePool`). */ public function requestTokenAction(ServerRequestInterface $request): ResponseInterface { return new JsonResponse([ 'headerName' => RequestToken::HEADER_NAME, 'requestToken' => $this->provideRequestTokenJwt(), ]); } /** * @throws PropagateResponseException */ protected function checkRedirect(ServerRequestInterface $request, bool $loginRefresh): void { $formProtection = $this->formProtectionFactory->createFromRequest($request); if (!$formProtection instanceof BackendFormProtection) { throw new \RuntimeException('The Form Protection retrieved does not match the expected one.', 1432080411); } if ($loginRefresh) { // Triggering `TYPO3/CMS/Backend/LoginRefresh` module happens in JS `TYPO3/CMS/Backend/Login` $formProtection->setSessionTokenFromRegistry(); $formProtection->persistSessionToken(); } else { $formProtection->storeSessionTokenInRegistry(); // @todo: Consolidate RouteDispatcher::evaluateReferrer() when changing 'main' to something different $redirectToURL = (string)$this->uriBuilder->buildUriWithRedirect('main', [], RouteRedirect::createFromRequest($request)); throw new PropagateResponseException(new RedirectResponse($redirectToURL, 303), 1724705833); } } /** * If a login provider was chosen in the previous request, which is not the default provider, * it is stored in a Cookie and appended to the HTTP Response. */ protected function appendLoginProviderCookie(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { $normalizedParams = $request->getAttribute('normalizedParams'); $loginProviderIdentifier = $this->loginProviderResolver->resolveLoginProviderIdentifierFromRequest($request, 'be_lastLoginProvider'); if ($loginProviderIdentifier === $this->loginProviderResolver->getPrimaryLoginProviderIdentifier()) { return $response; } $cookie = new Cookie( 'be_lastLoginProvider', $loginProviderIdentifier, $GLOBALS['EXEC_TIME'] + 7776000, // 90 days $this->backendEntryPointResolver->getPathFromRequest($request), '', // Use the secure option when the current request is served by a secure connection $normalizedParams->isHttps(), true, false, Cookie::SAMESITE_STRICT ); return $response->withAddedHeader('Set-Cookie', $cookie->__toString()); } /** * Gets news as array from sys_news and converts them into a * format suitable for showing them at the login screen. */ protected function getSystemNews(): array { $systemNews = []; $queryResult = $this->connectionPool ->getQueryBuilderForTable('sys_news') ->select('uid', 'title', 'content', 'crdate') ->from('sys_news') ->orderBy('crdate', 'DESC') ->executeQuery(); while ($row = $queryResult->fetchAssociative()) { $systemNews[] = [ 'uid' => $row['uid'], 'date' => $row['crdate'] ? date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], (int)$row['crdate']) : '', 'header' => $row['title'], 'content' => $row['content'], ]; } return $systemNews; } /** * Checks if login credentials have been submitted */ protected function isLoginInProgress(ServerRequestInterface $request): bool { // @todo: Restrict to POST?! // Value of forms submit button for login. If set, the login button was pressed. $submitValue = $request->getParsedBody()['commandLI'] ?? $request->getQueryParams()['commandLI'] ?? ''; $username = $request->getParsedBody()['username'] ?? $request->getQueryParams()['username'] ?? null; return !empty($username) || !empty($submitValue); } protected function provideRequestTokenJwt(): string { $nonce = SecurityAspect::provideIn($this->context)->provideNonce(); return RequestToken::create('core/user-auth/be')->toHashSignedJwt($nonce); } protected function getLanguageService(): LanguageService { return $GLOBALS['LANG']; } protected function getBackendUserAuthentication(): BackendUserAuthentication { return $GLOBALS['BE_USER']; } }