385 lines
19 KiB
PHP
385 lines
19 KiB
PHP
<?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\Controller;
|
|
|
|
use Psr\Http\Message\ResponseInterface;
|
|
use Psr\Http\Message\ServerRequestInterface;
|
|
use Psr\Http\Message\UriInterface;
|
|
use TYPO3\CMS\Backend\Attribute\AsController;
|
|
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
|
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
|
|
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
|
|
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
|
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
|
|
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderManifestInterface;
|
|
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager;
|
|
use TYPO3\CMS\Core\Authentication\Mfa\MfaViewType;
|
|
use TYPO3\CMS\Core\Http\HtmlResponse;
|
|
use TYPO3\CMS\Core\Http\RedirectResponse;
|
|
use TYPO3\CMS\Core\Imaging\IconFactory;
|
|
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
|
use TYPO3\CMS\Core\Messaging\FlashMessageService;
|
|
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
|
|
/**
|
|
* Controller to configure MFA providers in the backend
|
|
*
|
|
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
|
*/
|
|
#[AsController]
|
|
class MfaConfigurationController extends AbstractMfaController
|
|
{
|
|
protected array $allowedActions = ['overview', 'setup', 'activate', 'deactivate', 'unlock', 'edit', 'save'];
|
|
private array $providerActionsWhenInactive = ['setup', 'activate'];
|
|
private array $providerActionsWhenActive = ['deactivate', 'unlock', 'edit', 'save'];
|
|
protected ModuleTemplate $view;
|
|
|
|
public function __construct(
|
|
protected readonly ComponentFactory $componentFactory,
|
|
protected readonly IconFactory $iconFactory,
|
|
protected readonly UriBuilder $uriBuilder,
|
|
protected readonly ModuleTemplateFactory $moduleTemplateFactory,
|
|
protected readonly FlashMessageService $flashMessageService,
|
|
) {}
|
|
|
|
/**
|
|
* Main entry point, checking prerequisite, initializing and setting
|
|
* up the view and finally dispatching to the requested action.
|
|
*/
|
|
public function handleRequest(ServerRequestInterface $request): ResponseInterface
|
|
{
|
|
$this->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;
|
|
}
|
|
}
|