Passed
Push — master ( 25a926...39145a )
by
unknown
20:30
created

enrichResponseWithHeadersAndCookieInformation()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 16
rs 10
c 0
b 0
f 0
cc 3
nc 3
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\Controller\ErrorPageController;
29
use TYPO3\CMS\Core\Http\HtmlResponse;
30
use TYPO3\CMS\Core\Http\RedirectResponse;
31
use TYPO3\CMS\Core\Localization\LanguageService;
32
use TYPO3\CMS\Core\Messaging\AbstractMessage;
33
use TYPO3\CMS\Core\Session\UserSessionManager;
34
use TYPO3\CMS\Core\Utility\GeneralUtility;
35
36
/**
37
 * Initializes the backend user authentication object (BE_USER) and the global LANG object.
38
 *
39
 * @internal
40
 */
41
class BackendUserAuthenticator extends \TYPO3\CMS\Core\Middleware\BackendUserAuthenticator
42
{
43
    /**
44
     * List of requests that don't need a valid BE user
45
     *
46
     * @var array
47
     */
48
    protected $publicRoutes = [
49
        '/login',
50
        '/login/frame',
51
        '/login/password-reset/forget',
52
        '/login/password-reset/initiate-reset',
53
        '/login/password-reset/validate',
54
        '/login/password-reset/finish',
55
        '/ajax/login',
56
        '/ajax/logout',
57
        '/ajax/login/refresh',
58
        '/ajax/login/timedout',
59
        '/ajax/rsa/publickey',
60
        '/ajax/core/requirejs',
61
    ];
62
63
    /**
64
     * Calls the bootstrap process to set up $GLOBALS['BE_USER'] AND $GLOBALS['LANG']
65
     *
66
     * @param ServerRequestInterface $request
67
     * @param RequestHandlerInterface $handler
68
     * @return ResponseInterface
69
     */
70
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
71
    {
72
        $route = $request->getAttribute('route');
73
74
        // The global must be available very early, because methods below
75
        // might trigger code which relies on it. See: #45625
76
        $GLOBALS['BE_USER'] = GeneralUtility::makeInstance(BackendUserAuthentication::class);
77
        try {
78
            $GLOBALS['BE_USER']->start();
79
        } catch (MfaRequiredException $mfaRequiredException) {
80
            // If MFA is required and we are not already on the "auth_mfa"
81
            // route, force the user to it for further authentication
82
            if ($route->getOption('_identifier') !== 'auth_mfa') {
83
                return $this->redirectToMfaAuthProcess($GLOBALS['BE_USER'], $mfaRequiredException->getProvider());
84
            }
85
        }
86
87
        // Register the backend user as aspect and initializing workspace once for TSconfig conditions
88
        $this->setBackendUserAspect($GLOBALS['BE_USER'], (int)$GLOBALS['BE_USER']->user['workspace_id']);
89
        if ($this->isLoggedInBackendUserRequired($route)) {
90
            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

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