initializeMfaConfiguration(); $this->view = $this->moduleTemplateFactory->create($request); $action = (string)($request->getQueryParams()['action'] ?? $request->getParsedBody()['action'] ?? 'overview'); if (!$this->isActionAllowed($action)) { return new HtmlResponse('Action not allowed', 400); } $mfaProvider = null; $identifier = (string)($request->getQueryParams()['identifier'] ?? $request->getParsedBody()['identifier'] ?? ''); // Check if given identifier is valid if ($this->isValidIdentifier($identifier)) { $mfaProvider = $this->mfaProviderRegistry->getProvider($identifier); } // All actions expect "overview" require a provider to deal with. // If non is found at this point, initiate a redirect to the overview. if ($mfaProvider === null && $action !== 'overview') { $this->addFlashMessage($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:providerNotFound'), '', ContextualFeedbackSeverity::ERROR); return new RedirectResponse($this->getActionUri('overview')); } // If a valid provider is given, check if the requested action can be performed on this provider if ($mfaProvider !== null) { $isProviderActive = $mfaProvider->isActive( MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser()) ); // Some actions require the provider to be inactive if ($isProviderActive && in_array($action, $this->providerActionsWhenInactive, true)) { $this->addFlashMessage($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:providerActive'), '', ContextualFeedbackSeverity::ERROR); return new RedirectResponse($this->getActionUri('overview')); } // Some actions require the provider to be active if (!$isProviderActive && in_array($action, $this->providerActionsWhenActive, true)) { $this->addFlashMessage($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:providerNotActive'), '', ContextualFeedbackSeverity::ERROR); return new RedirectResponse($this->getActionUri('overview')); } } switch ($action) { case 'overview': return $this->overviewAction($request); case 'setup': case 'edit': case 'activate': case 'deactivate': case 'unlock': case 'save': return $this->{$action . 'Action'}($request, $mfaProvider); default: return new HtmlResponse('Action not allowed', 400); } } /** * Setup the overview with all available MFA providers */ protected function overviewAction(ServerRequestInterface $request): ResponseInterface { $this->addOverviewButtons($request); $this->view->assignMultiple([ 'providers' => $this->allowedProviders, 'defaultProvider' => $this->getDefaultProviderIdentifier(), 'recommendedProvider' => $this->getRecommendedProviderIdentifier(), 'setupRequired' => $this->mfaRequired && !$this->mfaProviderRegistry->hasActiveProviders($this->getBackendUser()), ]); return $this->view->renderResponse('Mfa/Overview'); } /** * Render form to setup a provider by using provider specific content */ protected function setupAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface { $this->addFormButtons(); $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser()); $providerResponse = $mfaProvider->handleRequest($request, $propertyManager, MfaViewType::SETUP); $this->view->assignMultiple([ 'provider' => $mfaProvider, 'providerContent' => $providerResponse->getBody(), ]); return $this->view->renderResponse('Mfa/Setup'); } /** * Handle activate request, receiving from the setup view * by forwarding the request to the appropriate provider. * Furthermore, add the provider as default provider in case * it is the recommended provider for this user, or no default * provider is yet defined the newly activated provider is allowed * to be a default provider and there are no other providers which * would suite as default provider. */ protected function activateAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface { $backendUser = $this->getBackendUser(); $isRecommendedProvider = $this->getRecommendedProviderIdentifier() === $mfaProvider->getIdentifier(); $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $backendUser); $languageService = $this->getLanguageService(); // Check whether activation operation was successful and the provider is now active. if (!$mfaProvider->activate($request, $propertyManager) || !$mfaProvider->isActive($propertyManager)) { $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:activate.failure'), $languageService->sL($mfaProvider->getTitle())), '', ContextualFeedbackSeverity::ERROR); return new RedirectResponse($this->getActionUri('setup', ['identifier' => $mfaProvider->getIdentifier()])); } if ($isRecommendedProvider || ( $this->getDefaultProviderIdentifier() === '' && $mfaProvider->isDefaultProviderAllowed() && !$this->hasSuitableDefaultProviders([$mfaProvider->getIdentifier()]) ) ) { $this->setDefaultProvider($mfaProvider); } // If this is the first activated provider, the user has logged in without being required // to pass the MFA challenge. Therefore, no session entry exists. To prevent the challenge // from showing up after the activation we need to set the session data here. if (!(bool)($backendUser->getSessionData('mfa') ?? false)) { $backendUser->setSessionData('mfa', true); } $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:activate.success'), $languageService->sL($mfaProvider->getTitle())), '', ContextualFeedbackSeverity::OK); return new RedirectResponse($this->getActionUri('overview')); } /** * Handle deactivate request by forwarding the request to the * appropriate provider. Also remove the provider as default * provider from user UC, if set. */ protected function deactivateAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface { $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser()); $languageService = $this->getLanguageService(); if (!$mfaProvider->deactivate($request, $propertyManager)) { $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:deactivate.failure'), $languageService->sL($mfaProvider->getTitle())), '', ContextualFeedbackSeverity::ERROR); } else { if ($this->isDefaultProvider($mfaProvider)) { $this->removeDefaultProvider(); } $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:deactivate.success'), $languageService->sL($mfaProvider->getTitle())), '', ContextualFeedbackSeverity::OK); } return new RedirectResponse($this->getActionUri('overview')); } /** * Handle unlock request by forwarding the request to the appropriate provider */ protected function unlockAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface { $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser()); $languageService = $this->getLanguageService(); if (!$mfaProvider->unlock($request, $propertyManager)) { $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:unlock.failure'), $languageService->sL($mfaProvider->getTitle())), '', ContextualFeedbackSeverity::ERROR); } else { $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:unlock.success'), $languageService->sL($mfaProvider->getTitle())), '', ContextualFeedbackSeverity::OK); } return new RedirectResponse($this->getActionUri('overview')); } /** * Render form to edit a provider by using provider specific content */ protected function editAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface { $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser()); if ($mfaProvider->isLocked($propertyManager)) { // Do not show edit view for locked providers $this->addFlashMessage($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:providerIsLocked'), '', ContextualFeedbackSeverity::ERROR); return new RedirectResponse($this->getActionUri('overview')); } $this->addFormButtons(); $providerResponse = $mfaProvider->handleRequest($request, $propertyManager, MfaViewType::EDIT); $this->view->assignMultiple([ 'provider' => $mfaProvider, 'providerContent' => $providerResponse->getBody(), 'isDefaultProvider' => $this->isDefaultProvider($mfaProvider), ]); return $this->view->renderResponse('Mfa/Edit'); } /** * Handle save request, receiving from the edit view by * forwarding the request to the appropriate provider. */ protected function saveAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface { $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser()); $languageService = $this->getLanguageService(); if (!$mfaProvider->update($request, $propertyManager)) { $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:save.failure'), $languageService->sL($mfaProvider->getTitle())), '', ContextualFeedbackSeverity::ERROR); } else { if ($request->getParsedBody()['defaultProvider'] ?? false) { $this->setDefaultProvider($mfaProvider); } elseif ($this->isDefaultProvider($mfaProvider)) { $this->removeDefaultProvider(); } $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:save.success'), $languageService->sL($mfaProvider->getTitle())), '', ContextualFeedbackSeverity::OK); } if (!$mfaProvider->isActive($propertyManager)) { return new RedirectResponse($this->getActionUri('overview')); } return new RedirectResponse($this->getActionUri('edit', ['identifier' => $mfaProvider->getIdentifier()])); } /** * Build a uri for the current controller based on the * given action, respecting additional parameters. */ protected function getActionUri(string $action, array $additionalParameters = []): UriInterface { if (!$this->isActionAllowed($action)) { $action = 'overview'; } return $this->uriBuilder->buildUriFromRoute('mfa', array_merge(['action' => $action], $additionalParameters)); } /** * Check if there are more suitable default providers for the current user */ protected function hasSuitableDefaultProviders(array $excludedProviders = []): bool { foreach ($this->allowedProviders as $identifier => $provider) { if (!in_array($identifier, $excludedProviders, true) && $provider->isDefaultProviderAllowed() && $provider->isActive(MfaProviderPropertyManager::create($provider, $this->getBackendUser())) ) { return true; } } return false; } /** * Get the default provider */ protected function getDefaultProviderIdentifier(): string { $defaultProviderIdentifier = (string)($this->getBackendUser()->uc['mfa']['defaultProvider'] ?? ''); // The default provider value is only valid, if the corresponding provider exist and is allowed if ($this->isValidIdentifier($defaultProviderIdentifier)) { $defaultProvider = $this->mfaProviderRegistry->getProvider($defaultProviderIdentifier); $propertyManager = MfaProviderPropertyManager::create($defaultProvider, $this->getBackendUser()); // Also check if the provider is activated for the user if ($defaultProvider->isActive($propertyManager)) { return $defaultProviderIdentifier; } } // If the stored provider is not valid, clean up the UC $this->removeDefaultProvider(); return ''; } /** * Get the recommended provider */ protected function getRecommendedProviderIdentifier(): string { $recommendedProvider = $this->getRecommendedProvider(); if ($recommendedProvider === null) { return ''; } $propertyManager = MfaProviderPropertyManager::create($recommendedProvider, $this->getBackendUser()); // If the defined recommended provider is valid, check if it is not yet activated return !$recommendedProvider->isActive($propertyManager) ? $recommendedProvider->getIdentifier() : ''; } protected function isDefaultProvider(MfaProviderManifestInterface $mfaProvider): bool { return $this->getDefaultProviderIdentifier() === $mfaProvider->getIdentifier(); } protected function setDefaultProvider(MfaProviderManifestInterface $mfaProvider): void { $this->getBackendUser()->uc['mfa']['defaultProvider'] = $mfaProvider->getIdentifier(); $this->getBackendUser()->writeUC(); } protected function removeDefaultProvider(): void { $this->getBackendUser()->uc['mfa']['defaultProvider'] = ''; $this->getBackendUser()->writeUC(); } protected function addFlashMessage(string $message, string $title = '', ContextualFeedbackSeverity $severity = ContextualFeedbackSeverity::INFO): void { $flashMessage = new FlashMessage($message, $title, $severity, true); $defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier(); $defaultFlashMessageQueue->enqueue($flashMessage); } protected function addOverviewButtons(ServerRequestInterface $request): void { if (($returnUrl = $this->getReturnUrl($request)) !== '') { $this->view->addButtonToButtonBar($this->componentFactory->createBackButton($returnUrl)); } } protected function addFormButtons(): void { $closeButton = $this->componentFactory->createCloseButton((string)$this->uriBuilder->buildUriFromRoute('mfa', ['action' => 'overview'])) ->setClasses('t3js-editform-close'); $this->view->addButtonToButtonBar($closeButton); $this->view->addButtonToButtonBar($this->componentFactory->createSaveButton('mfaConfigurationController')->setName('save'), ButtonBar::BUTTON_POSITION_LEFT, 2); } protected function getReturnUrl(ServerRequestInterface $request): string { $returnUrl = GeneralUtility::sanitizeLocalUrl( $request->getQueryParams()['returnUrl'] ?? $request->getParsedBody()['returnUrl'] ?? '', $request ); if ($returnUrl === '') { $returnUrl = (string)$this->uriBuilder->buildUriFromRoute('user_setup'); } return $returnUrl; } }