Passed
Push — master ( 0a1053...4cce54 )
by
unknown
25:41 queued 13:11
created

BackendUserAuthenticator::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
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\Middleware;
19
20
use Psr\Http\Message\ResponseInterface;
21
use Psr\Http\Message\ServerRequestInterface;
22
use Psr\Http\Server\RequestHandlerInterface;
23
use TYPO3\CMS\Backend\Routing\Route;
24
use TYPO3\CMS\Backend\Routing\UriBuilder;
25
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
26
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderManifestInterface;
27
use TYPO3\CMS\Core\Authentication\Mfa\MfaRequiredException;
28
use TYPO3\CMS\Core\Context\Context;
29
use TYPO3\CMS\Core\Controller\ErrorPageController;
30
use TYPO3\CMS\Core\Http\HtmlResponse;
31
use TYPO3\CMS\Core\Http\RedirectResponse;
32
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
33
use TYPO3\CMS\Core\Messaging\AbstractMessage;
34
use TYPO3\CMS\Core\Session\UserSessionManager;
35
use TYPO3\CMS\Core\Utility\GeneralUtility;
36
37
/**
38
 * Initializes the backend user authentication object (BE_USER) and the global LANG object.
39
 *
40
 * @internal
41
 */
42
class BackendUserAuthenticator extends \TYPO3\CMS\Core\Middleware\BackendUserAuthenticator
43
{
44
    /**
45
     * List of requests that don't need a valid BE user
46
     *
47
     * @var array
48
     */
49
    protected $publicRoutes = [
50
        '/login',
51
        '/login/frame',
52
        '/login/password-reset/forget',
53
        '/login/password-reset/initiate-reset',
54
        '/login/password-reset/validate',
55
        '/login/password-reset/finish',
56
        '/ajax/login',
57
        '/ajax/logout',
58
        '/ajax/login/preflight',
59
        '/ajax/login/refresh',
60
        '/ajax/login/timedout',
61
        '/ajax/core/requirejs',
62
    ];
63
64
    private LanguageServiceFactory $languageServiceFactory;
65
66
    public function __construct(
67
        Context $context,
68
        LanguageServiceFactory $languageServiceFactory
69
    ) {
70
        parent::__construct($context);
71
        $this->languageServiceFactory = $languageServiceFactory;
72
    }
73
74
    /**
75
     * Calls the bootstrap process to set up $GLOBALS['BE_USER'] AND $GLOBALS['LANG']
76
     *
77
     * @param ServerRequestInterface $request
78
     * @param RequestHandlerInterface $handler
79
     * @return ResponseInterface
80
     */
81
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
82
    {
83
        /** @var Route $route */
84
        $route = $request->getAttribute('route');
85
86
        // The global must be available very early, because methods below
87
        // might trigger code which relies on it. See: #45625
88
        $GLOBALS['BE_USER'] = GeneralUtility::makeInstance(BackendUserAuthentication::class);
89
        try {
90
            $GLOBALS['BE_USER']->start();
91
        } catch (MfaRequiredException $mfaRequiredException) {
92
            // If MFA is required and we are not already on the "auth_mfa"
93
            // route, force the user to it for further authentication
94
            if ($route->getOption('_identifier') !== 'auth_mfa') {
95
                return $this->redirectToMfaAuthProcess($GLOBALS['BE_USER'], $mfaRequiredException->getProvider(), $request);
96
            }
97
        }
98
99
        // Register the backend user as aspect and initializing workspace once for TSconfig conditions
100
        $this->setBackendUserAspect($GLOBALS['BE_USER'], (int)($GLOBALS['BE_USER']->user['workspace_id'] ?? 0));
101
        if ($this->isLoggedInBackendUserRequired($route)) {
102
            if (!$this->context->getAspect('backend.user')->isLoggedIn()) {
0 ignored issues
show
Bug introduced by
The method isLoggedIn() does not exist on TYPO3\CMS\Core\Context\AspectInterface. It seems like you code against a sub-type of TYPO3\CMS\Core\Context\AspectInterface such as TYPO3\CMS\Core\Context\UserAspect. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

102
            if (!$this->context->getAspect('backend.user')->/** @scrutinizer ignore-call */ isLoggedIn()) {
Loading history...
103
                $uri = GeneralUtility::makeInstance(UriBuilder::class)->buildUriWithRedirect(
104
                    'login',
105
                    [],
106
                    $route->getOption('_identifier'),
107
                    $request->getQueryParams()
108
                );
109
                $response = new RedirectResponse($uri);
110
                return $this->enrichResponseWithHeadersAndCookieInformation($response, $GLOBALS['BE_USER']);
111
            }
112
            if (!$GLOBALS['BE_USER']->isUserAllowedToLogin()) {
113
                $content = GeneralUtility::makeInstance(ErrorPageController::class)->errorAction(
114
                    'Login Error',
115
                    'TYPO3 is in maintenance mode at the moment. Only administrators are allowed access.',
116
                    AbstractMessage::ERROR,
117
                    1294585860
118
                );
119
                $response = new HtmlResponse($content, 503);
120
                return $this->enrichResponseWithHeadersAndCookieInformation($response, $GLOBALS['BE_USER']);
121
            }
122
        }
123
        if ($this->context->getAspect('backend.user')->isLoggedIn()) {
124
            $GLOBALS['BE_USER']->initializeBackendLogin();
125
        }
126
        $GLOBALS['LANG'] = $this->languageServiceFactory->createFromUserPreferences($GLOBALS['BE_USER']);
127
        // Re-setting the user and take the workspace from the user object now
128
        $this->setBackendUserAspect($GLOBALS['BE_USER']);
129
        $response = $handler->handle($request);
130
        $this->sessionGarbageCollection();
131
        return $this->enrichResponseWithHeadersAndCookieInformation($response, $GLOBALS['BE_USER']);
132
    }
133
134
    /**
135
     * Backend requests should always apply Set-Cookie information and never be cacheable.
136
     * This is also needed if there is a redirect from somewhere in the code.
137
     *
138
     * @param ResponseInterface $response
139
     * @param BackendUserAuthentication|null $userAuthentication
140
     * @return ResponseInterface
141
     * @throws \TYPO3\CMS\Core\Context\Exception\AspectNotFoundException
142
     */
143
    protected function enrichResponseWithHeadersAndCookieInformation(
144
        ResponseInterface $response,
145
        ?BackendUserAuthentication $userAuthentication
146
    ): ResponseInterface {
147
        if ($userAuthentication) {
148
            // If no backend user is logged-in, the cookie should be removed
149
            if (!$this->context->getAspect('backend.user')->isLoggedIn()) {
150
                $userAuthentication->removeCookie();
151
            }
152
            // Ensure to always apply a cookie
153
            $response = $userAuthentication->appendCookieToResponse($response);
154
        }
155
        // Additional headers to never cache any PHP request should be sent at any time when
156
        // accessing the TYPO3 Backend
157
        $response = $this->applyHeadersToResponse($response);
158
        return $response;
159
    }
160
161
    /**
162
     * Garbage collection for be_sessions (with a probability)
163
     */
164
    protected function sessionGarbageCollection(): void
165
    {
166
        UserSessionManager::create('BE')->collectGarbage();
167
    }
168
169
    /**
170
     * Initiate a redirect to the auth_mfa route with the given
171
     * provider and necessary cookies and headers appended.
172
     *
173
     * @param BackendUserAuthentication $user
174
     * @param MfaProviderManifestInterface $provider
175
     * @param ServerRequestInterface $request
176
     * @return ResponseInterface
177
     */
178
    protected function redirectToMfaAuthProcess(
179
        BackendUserAuthentication $user,
180
        MfaProviderManifestInterface $provider,
181
        ServerRequestInterface $request
182
    ): ResponseInterface {
183
        // GLOBALS[LANG] needs to be set up, because the UriBuilder is generating a token, which in turn
184
        // needs the FormProtectionFactory, which then builds a Message Closure with GLOBALS[LANG] (hacky, yes!)
185
        $GLOBALS['LANG'] = $this->languageServiceFactory->createFromUserPreferences($user);
186
        $uri = GeneralUtility::makeInstance(UriBuilder::class)
187
            ->buildUriWithRedirectFromRequest(
188
                'auth_mfa',
189
                [
190
                    'identifier' => $provider->getIdentifier()
191
                ],
192
                $request
193
            );
194
        $response = new RedirectResponse($uri);
195
        // Add necessary cookies and headers to the response so
196
        // the already passed authentication step is not lost.
197
        $response = $user->appendCookieToResponse($response);
198
        $response = $this->applyHeadersToResponse($response);
199
        return $response;
200
    }
201
    /**
202
     * Check if the user is required for the request.
203
     * If we're trying to do a login or an ajax login, don't require a user.
204
     *
205
     * @param Route $route the Route path to check against, something like '
206
     * @return bool true when the Route requires an authenticated backend user
207
     */
208
    protected function isLoggedInBackendUserRequired(Route $route): bool
209
    {
210
        return in_array($route->getPath(), $this->publicRoutes, true) === false;
211
    }
212
}
213