Passed
Push — master ( 428a99...6f7ef8 )
by
unknown
19:55
created

MfaController::getMfaProviderFromRequest()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
c 0
b 0
f 0
dl 0
loc 12
rs 10
cc 3
nc 3
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Backend\Controller;
19
20
use Psr\Http\Message\ResponseInterface;
21
use Psr\Http\Message\ServerRequestInterface;
22
use Psr\Log\LoggerAwareInterface;
23
use Psr\Log\LoggerAwareTrait;
24
use TYPO3\CMS\Backend\ContextMenu\ItemProviders\ProviderInterface;
25
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderManifestInterface;
26
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager;
27
use TYPO3\CMS\Core\Authentication\Mfa\MfaViewType;
28
use TYPO3\CMS\Core\Http\HtmlResponse;
29
use TYPO3\CMS\Core\Http\RedirectResponse;
30
31
/**
32
 * Controller to provide a multi-factor authentication endpoint
33
 *
34
 * @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
35
 */
36
class MfaController extends AbstractMfaController implements LoggerAwareInterface
37
{
38
    use LoggerAwareTrait;
39
40
    /**
41
     * Main entry point, checking prerequisite, initializing and setting
42
     * up the view and finally dispatching to the requested action.
43
     */
44
    public function handleRequest(ServerRequestInterface $request): ResponseInterface
45
    {
46
        $action = (string)($request->getQueryParams()['action'] ?? $request->getParsedBody()['action'] ?? 'auth');
47
48
        switch ($action) {
49
            case 'auth':
50
            case 'verify':
51
                $mfaProvider = $this->getMfaProviderFromRequest($request);
52
                // All actions except "cancel" require a provider to deal with.
53
                // If non is found at this point, throw an exception since this should never happen.
54
                if ($mfaProvider === null) {
55
                    throw new \InvalidArgumentException('No active MFA provider was found!', 1611879242);
56
                }
57
                return $this->{$action . 'Action'}($request, $mfaProvider);
58
            case 'cancel':
59
                return $this->cancelAction();
60
            default:
61
                throw new \InvalidArgumentException('Action not allowed', 1611879244);
62
        }
63
    }
64
65
    /**
66
     * Setup the authentication view for the provider by using provider specific content
67
     */
68
    public function authAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface
69
    {
70
        $view = $this->moduleTemplate->getView();
71
        $view->setTemplateRootPaths(['EXT:backend/Resources/Private/Templates/Mfa']);
72
        $view->setTemplate('Auth');
73
        $view->assign('hasAuthError', (bool)($request->getQueryParams()['failure'] ?? false));
74
        $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser());
75
        $providerResponse = $mfaProvider->handleRequest($request, $propertyManager, MfaViewType::AUTH);
76
        $view->assignMultiple([
77
            'provider' => $mfaProvider,
78
            'alternativeProviders' => $this->getAlternativeProviders($mfaProvider),
79
            'isLocked' => $mfaProvider->isLocked($propertyManager),
80
            'providerContent' => $providerResponse->getBody()
81
        ]);
82
        $this->moduleTemplate->setTitle('TYPO3 CMS Login: ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename']);
83
        return new HtmlResponse($this->moduleTemplate->renderContent());
84
    }
85
86
    /**
87
     * Handle verification request, receiving from the auth view
88
     * by forwarding the request to the appropriate provider.
89
     */
90
    public function verifyAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface
91
    {
92
        $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser());
93
94
        // Check if the provider can process the request and is not temporarily blocked
95
        if (!$mfaProvider->canProcess($request) || $mfaProvider->isLocked($propertyManager)) {
96
            // If this fails, cancel the authentication
97
            return $this->cancelAction();
98
        }
99
        // Call the provider to verify the request
100
        if (!$mfaProvider->verify($request, $propertyManager)) {
101
            $this->log('Multi-factor authentication failed');
102
            // If failed, initiate a redirect back to the auth view
103
            return new RedirectResponse($this->uriBuilder->buildUriFromRoute(
104
                'auth_mfa',
105
                [
106
                    'identifier' => $mfaProvider->getIdentifier(),
107
                    'failure' => true
108
                ]
109
            ));
110
        }
111
        $this->log('Multi-factor authentication successfull');
112
        // If verified, store this information in the session
113
        // and initiate a redirect back to the login view.
114
        $this->getBackendUser()->setAndSaveSessionData('mfa', true);
115
        return new RedirectResponse($this->uriBuilder->buildUriFromRoute('login'));
116
    }
117
118
    /**
119
     * Allow the user to cancel the multi-factor authentication by
120
     * calling logoff on the user object, to destroy the session and
121
     * other already gathered information and finally initiate a
122
     * redirect back to the login.
123
     */
124
    public function cancelAction(): ResponseInterface
125
    {
126
        $this->log('Multi-factor authentication canceled');
127
        $this->getBackendUser()->logoff();
128
        return new RedirectResponse($this->uriBuilder->buildUriFromRoute('login'));
129
    }
130
131
    /**
132
     * Fetch alternative (activated and allowed) providers for the user to chose from
133
     *
134
     * @return ProviderInterface[]
135
     */
136
    protected function getAlternativeProviders(MfaProviderManifestInterface $mfaProvider): array
137
    {
138
        return array_filter($this->allowedProviders, function ($provider) use ($mfaProvider) {
139
            return $provider !== $mfaProvider
140
                && $provider->isActive(MfaProviderPropertyManager::create($provider, $this->getBackendUser()));
141
        });
142
    }
143
144
    /**
145
     * Log debug information for MFA events
146
     */
147
    protected function log(string $message, array $additionalData = [], ?MfaProviderManifestInterface $mfaProvider = null): void
148
    {
149
        $user = $this->getBackendUser();
150
        $context = [
151
            'user' => [
152
                'uid' => $user->user[$user->userid_column],
153
                'username' => $user->user[$user->username_column]
154
            ]
155
        ];
156
        if ($mfaProvider !== null) {
157
            $context['provider'] = $mfaProvider->getIdentifier();
158
            $context['isProviderLocked'] = $mfaProvider->isLocked(
159
                MfaProviderPropertyManager::create($mfaProvider, $user)
160
            );
161
        }
162
        $this->logger->debug($message, array_replace_recursive($context, $additionalData));
163
    }
164
165
    protected function getMfaProviderFromRequest(ServerRequestInterface $request): ?MfaProviderManifestInterface
166
    {
167
        $identifier = (string)($request->getQueryParams()['identifier'] ?? $request->getParsedBody()['identifier'] ?? '');
168
        // Check if given identifier is valid
169
        if ($this->isValidIdentifier($identifier)) {
170
            $provider = $this->mfaProviderRegistry->getProvider($identifier);
171
            // Only add provider if it was activated by the current user
172
            if ($provider->isActive(MfaProviderPropertyManager::create($provider, $this->getBackendUser()))) {
173
                return $provider;
174
            }
175
        }
176
        return null;
177
    }
178
}
179