'GET', 'activate' => 'POST', 'cancel' => 'GET', ]; public function __construct( protected readonly UriBuilder $uriBuilder, protected readonly AuthenticationStyleInformation $authenticationStyleInformation, protected readonly PageRenderer $pageRenderer, protected readonly ExtensionConfiguration $extensionConfiguration, protected readonly LoggerInterface $logger, protected readonly BackendViewFactory $backendViewFactory, protected readonly FlashMessageService $flashMessageService, ) {} public function handleRequest(ServerRequestInterface $request): ResponseInterface { $this->initializeMfaConfiguration(); $action = (string)($request->getQueryParams()['action'] ?? 'setup'); $backendUser = $this->getBackendUser(); if (($backendUser->getSessionData('mfa') ?? false) || $backendUser->getOriginalUserIdWhenInSwitchUserMode() !== null || !$backendUser->isMfaSetupRequired() || $this->mfaProviderRegistry->hasActiveProviders($backendUser) ) { // Since the current user either did already pass MFA, is in "switch-user" mode, // is not required to set up MFA or has already activated a provider, throw an // exception to prevent the endpoint from being called unintentionally by custom code. throw new \InvalidArgumentException('MFA setup is not necessary. Do not call this endpoint on your own.', 1632154036); } $actionMethod = self::ACTION_METHOD_MAP[$action] ?? null; if ($actionMethod !== null && $request->getMethod() === $actionMethod) { return $this->{$action . 'Action'}($request); } return new HtmlResponse('', 404); } /** * Render form to setup a provider by using provider specific content. Fall * back to provider selection view, in case no valid provider was yet selected. */ protected function setupAction(ServerRequestInterface $request): ResponseInterface { $identifier = (string)($request->getQueryParams()['identifier'] ?? ''); if ($identifier === '' || !$this->isValidIdentifier($identifier)) { return $this->renderSelectionView($request); } $mfaProvider = $this->mfaProviderRegistry->getProvider($identifier); $this->log('Required MFA setup initiated', $mfaProvider); return $this->renderSetupView($request, $mfaProvider); } /** * Handle activate request, receiving from the setup view * by forwarding the request to the appropriate provider. */ protected function activateAction(ServerRequestInterface $request): ResponseInterface { $identifier = (string)($request->getParsedBody()['identifier'] ?? ''); if ($identifier === '' || !$this->isValidIdentifier($identifier)) { // Return to selection view in case no valid identifier is given return new RedirectResponse($this->uriBuilder->buildUriWithRedirect('setup_mfa', [], RouteRedirect::createFromRequest($request))); } $mfaProvider = $this->mfaProviderRegistry->getProvider($identifier); $backendUser = $this->getBackendUser(); $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $backendUser); // Check whether activation operation was successful and the provider is now active. if (!$mfaProvider->activate($request, $propertyManager) || !$mfaProvider->isActive($propertyManager)) { $this->log('Required MFA setup failed', $mfaProvider); return new RedirectResponse( $this->uriBuilder->buildUriWithRedirect( 'setup_mfa', [ 'identifier' => $mfaProvider->getIdentifier(), 'hasErrors' => true, ], RouteRedirect::createFromRequest($request) ) ); } $this->log('Required MFA setup successful', $mfaProvider); // Set the activated provider as the default provider, store the "mfa" key in the session data, // add a flash message to the session and finally initiate a redirect to the login, on which // possible redirect parameters are evaluated again. $backendUser->uc['mfa']['defaultProvider'] = $mfaProvider->getIdentifier(); $backendUser->writeUC(); $backendUser->setAndSaveSessionData('mfa', true); $this->addSuccessMessage($mfaProvider->getTitle()); return new RedirectResponse($this->uriBuilder->buildUriWithRedirect('login', [], RouteRedirect::createFromRequest($request))); } /** * Allow the user to cancel the multi-factor authentication setup process * by calling logoff on the user object, to destroy the session and other * already gathered information and finally initiate a redirect back to the login. */ protected function cancelAction(ServerRequestInterface $request): ResponseInterface { $this->log('Required MFA setup canceled'); $this->getBackendUser()->logoff(); return new RedirectResponse($this->uriBuilder->buildUriWithRedirect('login', [], RouteRedirect::createFromRequest($request))); } /** * Allow the user - required to set up MFA - to select between all available providers */ protected function renderSelectionView(ServerRequestInterface $request): ResponseInterface { $this->setUpBasicPageRendererForBackend($this->pageRenderer, $this->extensionConfiguration, $request, $this->getLanguageService()); $this->pageRenderer->setTitle('TYPO3 CMS Login: ' . ($GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? '')); $this->pageRenderer->loadJavaScriptModule('bootstrap'); $recommendedProvider = $this->getRecommendedProvider(); $providers = array_filter($this->allowedProviders, static function (MfaProviderManifestInterface $provider) use ($recommendedProvider): bool { // Remove the recommended provider and providers, which can not be used as default, e.g. recovery codes return $provider->isDefaultProviderAllowed() && ($recommendedProvider === null || $provider->getIdentifier() !== $recommendedProvider->getIdentifier()); }); $view = $this->initializeView($request); $view->assignMultiple([ 'recommendedProvider' => $recommendedProvider, 'providers' => $providers, ]); $this->pageRenderer->setBodyContent('' . $view->render('Mfa/Standalone/Selection')); return $this->pageRenderer->renderResponse($request); } /** * Render form to setup a provider by using provider specific content */ protected function renderSetupView( ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider ): ResponseInterface { $this->setUpBasicPageRendererForBackend($this->pageRenderer, $this->extensionConfiguration, $request, $this->getLanguageService()); $this->pageRenderer->setTitle('TYPO3 CMS Login: ' . ($GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? '')); $this->pageRenderer->loadJavaScriptModule('bootstrap'); $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser()); $providerResponse = $mfaProvider->handleRequest($request, $propertyManager, MfaViewType::SETUP); $view = $this->initializeView($request); $view->assignMultiple([ 'provider' => $mfaProvider, 'providerContent' => $providerResponse->getBody(), 'hasErrors' => (bool)($request->getQueryParams()['hasErrors'] ?? false), ]); $this->pageRenderer->setBodyContent('' . $view->render('Mfa/Standalone/Setup')); return $this->pageRenderer->renderResponse($request); } /** * Initialize the standalone view by setting the paths and assigning view variables */ protected function initializeView(ServerRequestInterface $request): ViewInterface { $view = $this->backendViewFactory->create($request); $view->assignMultiple([ 'redirect' => $request->getQueryParams()['redirect'] ?? '', 'redirectParams' => $request->getQueryParams()['redirectParams'] ?? '', 'siteName' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'], 'footerNote' => $this->authenticationStyleInformation->getFooterNote(), ]); $this->addCustomAuthenticationFormStyles($request); return $view; } protected function addCustomAuthenticationFormStyles(ServerRequestInterface $request): void { 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); } } /** * Extend base identifier check to further evaluate whether * the provider is allowed to be a default provider. */ protected function isValidIdentifier(string $identifier): bool { return parent::isValidIdentifier($identifier) && $this->mfaProviderRegistry->getProvider($identifier)->isDefaultProviderAllowed(); } /** * Add a flash message to inform the user about the successful activation of MFA and * store this in the session, so it will be shown in the backend after the redirect. */ protected function addSuccessMessage(string $mfaProviderTitle): void { $lang = $this->getLanguageService(); $this->flashMessageService->getMessageQueueByIdentifier()->enqueue( new FlashMessage( sprintf($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:standalone.setup.success.message'), $lang->sL($mfaProviderTitle)), $lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:standalone.setup.success.title'), ContextualFeedbackSeverity::OK, true ) ); } /** * Log debug information for MFA setup events */ protected function log(string $message, ?MfaProviderManifestInterface $mfaProvider = null): void { $user = $this->getBackendUser(); $context = [ 'user' => [ 'uid' => $user->getUserId(), 'username' => $user->getUserName(), ], ]; if ($mfaProvider !== null) { $context['provider'] = $mfaProvider->getIdentifier(); } $this->logger->debug($message, $context); } }