From 1bc57510d59057e1ec7460c55c3a48ae55ccd3c9 Mon Sep 17 00:00:00 2001 From: Benni Mack <benni@typo3.org> Date: Tue, 10 Dec 2019 15:58:14 +0100 Subject: [PATCH] [BUGFIX] Send HTTP headers with PSR-7 response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Current Backend User Authentication sends header() which is not appended to the headers of the PSR-7 response and cannot be tested or validated properly. This is mainly due to legacy reasons as AbstractUserAuthentication sends these headers. When using TYPO3 in a scenario to do sub-requests within one PHP process, it is not possible to properly evaluate these Response headers. To enable this, the BackendUserAuthenticator middlewares now apply the headers to the Response object. Resolves: #89911 Releases: master Change-Id: Id22ca1a65e52f101d3775fbe79ea0ef1622e9fa9 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/62593 Tested-by: TYPO3com <noreply@typo3.com> Tested-by: Tobi Kretschmann <tobi@tobishome.de> Tested-by: Susanne Moog <look@susi.dev> Reviewed-by: Jörg Bösche <typo3@joergboesche.de> Reviewed-by: Sascha Rademacher <sascha.rademacher+typo3@gmail.com> Reviewed-by: Tobi Kretschmann <tobi@tobishome.de> Reviewed-by: Susanne Moog <look@susi.dev> --- .../Middleware/BackendUserAuthenticator.php | 40 +++----- .../Middleware/BackendUserAuthenticator.php | 97 +++++++++++++++++++ .../Middleware/BackendUserAuthenticator.php | 35 ++----- .../BackendUserAuthenticatorTest.php | 73 ++++++++++++++ 4 files changed, 189 insertions(+), 56 deletions(-) create mode 100644 typo3/sysext/core/Classes/Middleware/BackendUserAuthenticator.php create mode 100644 typo3/sysext/frontend/Tests/Functional/Middleware/BackendUserAuthenticatorTest.php diff --git a/typo3/sysext/backend/Classes/Middleware/BackendUserAuthenticator.php b/typo3/sysext/backend/Classes/Middleware/BackendUserAuthenticator.php index 352802aec7d6..0213398f0c6b 100644 --- a/typo3/sysext/backend/Classes/Middleware/BackendUserAuthenticator.php +++ b/typo3/sysext/backend/Classes/Middleware/BackendUserAuthenticator.php @@ -17,12 +17,8 @@ namespace TYPO3\CMS\Backend\Middleware; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; -use TYPO3\CMS\Core\Context\Context; -use TYPO3\CMS\Core\Context\UserAspect; -use TYPO3\CMS\Core\Context\WorkspaceAspect; use TYPO3\CMS\Core\Core\Bootstrap; use TYPO3\CMS\Core\Localization\LanguageService; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -32,7 +28,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; * * @internal */ -class BackendUserAuthenticator implements MiddlewareInterface +class BackendUserAuthenticator extends \TYPO3\CMS\Core\Middleware\BackendUserAuthenticator { /** * List of requests that don't need a valid BE user @@ -50,16 +46,6 @@ class BackendUserAuthenticator implements MiddlewareInterface '/ajax/core/requirejs', ]; - /** - * @var Context - */ - protected $context; - - public function __construct(Context $context) - { - $this->context = $context; - } - /** * Calls the bootstrap process to set up $GLOBALS['BE_USER'] AND $GLOBALS['LANG'] * @@ -71,14 +57,21 @@ class BackendUserAuthenticator implements MiddlewareInterface { $pathToRoute = $request->getAttribute('routePath', '/login'); - Bootstrap::initializeBackendUser(); + // The global must be available very early, because methods below + // might trigger code which relies on it. See: #45625 + $GLOBALS['BE_USER'] = GeneralUtility::makeInstance(BackendUserAuthentication::class); + $GLOBALS['BE_USER']->start(); // @todo: once this logic is in this method, the redirect URL should be handled as response here - Bootstrap::initializeBackendAuthentication($this->isLoggedInBackendUserRequired($pathToRoute)); + $GLOBALS['BE_USER']->backendCheckLogin($this->isLoggedInBackendUserRequired($pathToRoute)); $GLOBALS['LANG'] = LanguageService::createFromUserPreferences($GLOBALS['BE_USER']); // Register the backend user as aspect $this->setBackendUserAspect($GLOBALS['BE_USER']); - return $handler->handle($request); + $response = $handler->handle($request); + + // Additional headers to never cache any PHP request should be sent at any time when + // accessing the TYPO3 Backend + return $this->applyHeadersToResponse($response); } /** @@ -92,15 +85,4 @@ class BackendUserAuthenticator implements MiddlewareInterface { return in_array($routePath, $this->publicRoutes, true); } - - /** - * Register the backend user as aspect - * - * @param BackendUserAuthentication $user - */ - protected function setBackendUserAspect(BackendUserAuthentication $user) - { - $this->context->setAspect('backend.user', GeneralUtility::makeInstance(UserAspect::class, $user)); - $this->context->setAspect('workspace', GeneralUtility::makeInstance(WorkspaceAspect::class, $user->workspace)); - } } diff --git a/typo3/sysext/core/Classes/Middleware/BackendUserAuthenticator.php b/typo3/sysext/core/Classes/Middleware/BackendUserAuthenticator.php new file mode 100644 index 000000000000..fbd1a0321aa2 --- /dev/null +++ b/typo3/sysext/core/Classes/Middleware/BackendUserAuthenticator.php @@ -0,0 +1,97 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Core\Middleware; + +/* + * 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! + */ + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; +use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Core\Context\UserAspect; +use TYPO3\CMS\Core\Context\WorkspaceAspect; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Boilerplate to authenticate a backend user in the current workflow, can be used + * for TYPO3 Backend and Frontend requests. + * + * The actual authentication and the selection if no-cache headers to responses should + * be applied should still reside in the "process()" method which should be + * extended by derivative classes. + * + * In derivative classes, the Context API can be used to detect, if a backend user is logged in + * like this: + * + * $response = $handler->handle($request); + * if ($this->context->getAspect('backend.user')->isLoggedIn()) { + * return $this->applyHeadersToResponse($response); + * } + * + * @internal this class might get merged again with the subclasses + */ +abstract class BackendUserAuthenticator implements MiddlewareInterface +{ + /** + * @var Context + */ + protected $context; + + public function __construct(Context $context) + { + $this->context = $context; + } + + /** + * @inheritDoc + */ + abstract public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface; + + /** + * Adding headers to the response to avoid caching on the client side. + * These headers will override any previous headers of these names sent. + * Get the http headers to be sent if an authenticated user is available, + * in order to disallow browsers to store the response on the client side. + * + * @param ResponseInterface $response + * @return ResponseInterface the modified response object. + */ + protected function applyHeadersToResponse(ResponseInterface $response): ResponseInterface + { + $headers = [ + 'Expires' => 0, + 'Last-Modified' => gmdate('D, d M Y H:i:s') . ' GMT', + 'Cache-Control' => 'no-cache, must-revalidate', + // HTTP 1.0 compatibility, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Pragma + 'Pragma' => 'no-cache' + ]; + foreach ($headers as $headerName => $headerValue) { + $response = $response->withHeader($headerName, (string)$headerValue); + } + return $response; + } + + /** + * Register the backend user as aspect + * + * @param BackendUserAuthentication|null $user + */ + protected function setBackendUserAspect(?BackendUserAuthentication $user): void + { + $this->context->setAspect('backend.user', GeneralUtility::makeInstance(UserAspect::class, $user)); + $this->context->setAspect('workspace', GeneralUtility::makeInstance(WorkspaceAspect::class, $user ? $user->workspace : 0)); + } +} diff --git a/typo3/sysext/frontend/Classes/Middleware/BackendUserAuthenticator.php b/typo3/sysext/frontend/Classes/Middleware/BackendUserAuthenticator.php index ea7eb5349bce..8a5eb2f6682e 100644 --- a/typo3/sysext/frontend/Classes/Middleware/BackendUserAuthenticator.php +++ b/typo3/sysext/frontend/Classes/Middleware/BackendUserAuthenticator.php @@ -18,13 +18,9 @@ namespace TYPO3\CMS\Frontend\Middleware; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use TYPO3\CMS\Backend\FrontendBackendUserAuthentication; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; -use TYPO3\CMS\Core\Context\Context; -use TYPO3\CMS\Core\Context\UserAspect; -use TYPO3\CMS\Core\Context\WorkspaceAspect; use TYPO3\CMS\Core\Core\Bootstrap; use TYPO3\CMS\Core\Http\NormalizedParams; use TYPO3\CMS\Core\Localization\LanguageService; @@ -38,18 +34,8 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; * page due to rights management. As this can only happen once the page ID is resolved, this will happen * after the routing middleware. */ -class BackendUserAuthenticator implements MiddlewareInterface +class BackendUserAuthenticator extends \TYPO3\CMS\Core\Middleware\BackendUserAuthenticator { - /** - * @var Context - */ - protected $context; - - public function __construct(Context $context) - { - $this->context = $context; - } - /** * Creates a backend user authentication object, tries to authenticate a user * @@ -77,7 +63,13 @@ class BackendUserAuthenticator implements MiddlewareInterface $this->setBackendUserAspect($GLOBALS['BE_USER']); } - return $handler->handle($request); + $response = $handler->handle($request); + + // If, when building the response, the user is still available, then ensure that the headers are sent properly + if ($this->context->getAspect('backend.user')->isLoggedIn()) { + return $this->applyHeadersToResponse($response); + } + return $response; } /** @@ -123,15 +115,4 @@ class BackendUserAuthenticator implements MiddlewareInterface } return $user->backendCheckLogin(); } - - /** - * Register the backend user as aspect - * - * @param BackendUserAuthentication|null $user - */ - protected function setBackendUserAspect(BackendUserAuthentication $user) - { - $this->context->setAspect('backend.user', GeneralUtility::makeInstance(UserAspect::class, $user)); - $this->context->setAspect('workspace', GeneralUtility::makeInstance(WorkspaceAspect::class, $user->workspace)); - } } diff --git a/typo3/sysext/frontend/Tests/Functional/Middleware/BackendUserAuthenticatorTest.php b/typo3/sysext/frontend/Tests/Functional/Middleware/BackendUserAuthenticatorTest.php new file mode 100644 index 000000000000..200ec497bd88 --- /dev/null +++ b/typo3/sysext/frontend/Tests/Functional/Middleware/BackendUserAuthenticatorTest.php @@ -0,0 +1,73 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Frontend\Tests\Functional\Middleware; + +/* + * 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! + */ + +use TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait; +use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest; +use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequestContext; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +class BackendUserAuthenticatorTest extends FunctionalTestCase +{ + use SiteBasedTestTrait; + + protected const LANGUAGE_PRESETS = [ + 'EN' => ['id' => 0, 'title' => 'English', 'locale' => 'en_US.UTF8', 'iso' => 'en', 'hrefLang' => 'en-US', 'direction' => ''], + ]; + + protected function setUp(): void + { + parent::setUp(); + $this->importDataSet('EXT:core/Tests/Functional/Fixtures/pages.xml'); + $this->setUpBackendUserFromFixture(1); + $this->writeSiteConfiguration( + 'acme-com', + $this->buildSiteConfiguration(1, 'https://acme.com/'), + [ + $this->buildDefaultLanguageConfiguration('EN', '/'), + ] + ); + } + + /** + * @test + */ + public function nonAuthenticatedRequestDoesNotSendHeaders(): void + { + $response = $this->executeFrontendRequest( + (new InternalRequest('/'))->withPageId(1), + (new InternalRequestContext()) + ); + self::assertArrayNotHasKey('Cache-Control', $response->getHeaders()); + self::assertArrayNotHasKey('Pragma', $response->getHeaders()); + self::assertArrayNotHasKey('Expires', $response->getHeaders()); + } + + /** + * @test + */ + public function authenticatedRequestIncludesInvalidCacheHeaders(): void + { + $response = $this->executeFrontendRequest( + (new InternalRequest('/'))->withPageId(1), + (new InternalRequestContext()) + ->withBackendUserId(1) + ); + self::assertEquals('no-cache, must-revalidate', $response->getHeaders()['Cache-Control'][0]); + self::assertEquals('no-cache', $response->getHeaders()['Pragma'][0]); + self::assertEquals(0, $response->getHeaders()['Expires'][0]); + } +} -- GitLab