[BUGFIX] Send HTTP headers with PSR-7 response 93/62593/5
authorBenni Mack <benni@typo3.org>
Tue, 10 Dec 2019 14:58:14 +0000 (15:58 +0100)
committerSusanne Moog <look@susi.dev>
Thu, 16 Jan 2020 09:24:25 +0000 (10:24 +0100)
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>
typo3/sysext/backend/Classes/Middleware/BackendUserAuthenticator.php
typo3/sysext/core/Classes/Middleware/BackendUserAuthenticator.php [new file with mode: 0644]
typo3/sysext/frontend/Classes/Middleware/BackendUserAuthenticator.php
typo3/sysext/frontend/Tests/Functional/Middleware/BackendUserAuthenticatorTest.php [new file with mode: 0644]

index 352802a..0213398 100644 (file)
@@ -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
@@ -51,16 +47,6 @@ class BackendUserAuthenticator implements MiddlewareInterface
     ];
 
     /**
-     * @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']
      *
      * @param ServerRequestInterface $request
@@ -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 (file)
index 0000000..fbd1a03
--- /dev/null
@@ -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));
+    }
+}
index ea7eb53..8a5eb2f 100644 (file)
@@ -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,19 +34,9 @@ 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
      *
      * @param ServerRequestInterface $request
@@ -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 (file)
index 0000000..200ec49
--- /dev/null
@@ -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]);
+    }
+}