Commit 50d7c6f5 authored by Benni Mack's avatar Benni Mack Committed by Benjamin Franzke
Browse files

[FEATURE] Only set cookies for HTTP Responses in PSR-15 middlewares

In previous TYPO3 versions, AbstractUserAuthentication emitted
cookies directly via header() or setcookie() methods.

In order to have a better testing scenario, this change
builds Cookie objects and keeps them until a PSR-15 middleware
asks to apply the cookie information to a PSR-7 Response.

This also makes it possible to manipulate the authentication
cookies in Middlewares.

AbstractUserAuthentication does not actually "remove"
or "set" a cookie but rather keeps the information for
setting a cookie.

Resolves: #93011
Releases: master
Change-Id: Iaec0007a1347676bc3ba570b4b5a1da63d58d7e6
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/67032


Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Tested-by: Benjamin Franzke's avatarBenjamin Franzke <bfr@qbus.de>
Reviewed-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Reviewed-by: Benjamin Franzke's avatarBenjamin Franzke <bfr@qbus.de>
parent 67a56c5a
......@@ -21,6 +21,7 @@ ignoreFiles+="sysext/form/Classes/Mvc/Property/Exception/TypeConverterException.
ignoreFiles+="sysext/core/Classes/Database/Driver/PDOStatement.php"
ignoreFiles+="sysext/core/Classes/Database/Driver/PDOConnection.php"
ignoreFiles+="sysext/frontend/Classes/Typolink/PageLinkBuilder.php"
ignoreFiles+="sysext/backend/Classes/Middleware/BackendUserAuthenticator.php"
foundNewFile=0
oldFilename=""
......
......@@ -23,7 +23,7 @@ use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Backend\Routing\Route;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Http\ImmediateResponseException;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
......@@ -74,24 +74,52 @@ class BackendUserAuthenticator extends \TYPO3\CMS\Core\Middleware\BackendUserAut
$this->setBackendUserAspect($GLOBALS['BE_USER'], (int)$GLOBALS['BE_USER']->user['workspace_id']);
if (!$this->isLoggedInBackendUserRequired($route) && !$this->context->getAspect('backend.user')->isLoggedIn()) {
$uri = GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute('login');
return new RedirectResponse($uri);
$response = new RedirectResponse($uri);
return $this->enrichResponseWithHeadersAndCookieInformation($response, $GLOBALS['BE_USER']);
}
// @todo: Ensure that the runtime exceptions are caught
$GLOBALS['BE_USER']->backendCheckLogin($this->isLoggedInBackendUserRequired($route));
$GLOBALS['LANG'] = LanguageService::createFromUserPreferences($GLOBALS['BE_USER']);
// Re-setting the user and take the workspace from the user object now
$this->setBackendUserAspect($GLOBALS['BE_USER']);
$response = $handler->handle($request);
// If no backend user is logged-in, the cookie should be removed
if (!GeneralUtility::makeInstance(Context::class)->getAspect('backend.user')->isLoggedIn()) {
$GLOBALS['BE_USER']->removeCookie($GLOBALS['BE_USER']->name);
try {
// @todo: Ensure that the runtime exceptions are caught
$GLOBALS['BE_USER']->backendCheckLogin($this->isLoggedInBackendUserRequired($route));
$GLOBALS['LANG'] = LanguageService::createFromUserPreferences($GLOBALS['BE_USER']);
// Re-setting the user and take the workspace from the user object now
$this->setBackendUserAspect($GLOBALS['BE_USER']);
$response = $handler->handle($request);
} catch (ImmediateResponseException $e) {
$response = $this->enrichResponseWithHeadersAndCookieInformation(
$e->getResponse(),
$GLOBALS['BE_USER']
);
// Re-throw this exception
throw new ImmediateResponseException($response, $e->getCode());
}
return $this->enrichResponseWithHeadersAndCookieInformation($response, $GLOBALS['BE_USER']);
}
/**
* Backend requests should always apply Set-Cookie information and never be cacheable.
* This is also needed if there is a redirect from somewhere in the code.
*
* @param ResponseInterface $response
* @param BackendUserAuthentication|null $userAuthentication
* @return ResponseInterface
* @throws \TYPO3\CMS\Core\Context\Exception\AspectNotFoundException
*/
protected function enrichResponseWithHeadersAndCookieInformation(
ResponseInterface $response,
?BackendUserAuthentication $userAuthentication
): ResponseInterface {
if ($userAuthentication) {
// If no backend user is logged-in, the cookie should be removed
if (!$this->context->getAspect('backend.user')->isLoggedIn()) {
$userAuthentication->removeCookie();
}
// Ensure to always apply a cookie
$response = $userAuthentication->appendCookieToResponse($response);
}
// Additional headers to never cache any PHP request should be sent at any time when
// accessing the TYPO3 Backend
return $this->applyHeadersToResponse($response);
$response = $this->applyHeadersToResponse($response);
return $response;
}
/**
......
......@@ -15,6 +15,7 @@
namespace TYPO3\CMS\Core\Authentication;
use Psr\Http\Message\ResponseInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\HttpFoundation\Cookie;
......@@ -245,11 +246,6 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface
*/
public $dontSetCookie = false;
/**
* @var bool
*/
protected $cookieWasSetOnCurrentRequest = false;
/**
* Login type, used for services.
* @var string
......@@ -279,6 +275,13 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface
*/
protected $sessionData = [];
/**
* If set, this cookie will be set to the response.
*
* @var Cookie|null
*/
protected ?Cookie $setCookie;
/**
* Initialize some important variables
*
......@@ -347,15 +350,26 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface
}
/**
* Sets the session cookie for the current disposal.
* Used to apply a cookie to a PSR-7 Response.
*
* @throws Exception
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function appendCookieToResponse(ResponseInterface $response): ResponseInterface
{
if (isset($this->setCookie)) {
$response = $response->withAddedHeader('Set-Cookie', $this->setCookie->__toString());
}
return $response;
}
/**
* Sets the session cookie for the current disposal.
*/
protected function setSessionCookie()
{
$isSetSessionCookie = $this->isSetSessionCookie();
$isRefreshTimeBasedCookie = $this->isRefreshTimeBasedCookie();
if ($isSetSessionCookie || $isRefreshTimeBasedCookie) {
if ($this->isSetSessionCookie() || $isRefreshTimeBasedCookie) {
// Get the domain to be used for the cookie (if any):
$cookieDomain = $this->getCookieDomain();
// If no cookie domain is set, use the base path:
......@@ -369,7 +383,7 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface
// Use the secure option when the current request is served by a secure connection:
// SameSite "none" needs the secure option (only allowed on HTTPS)
$isSecure = $cookieSameSite === Cookie::SAMESITE_NONE || GeneralUtility::getIndpEnv('TYPO3_SSL');
$cookie = new Cookie(
$this->setCookie = new Cookie(
$this->name,
$this->id,
$cookieExpire,
......@@ -380,8 +394,6 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface
false,
$cookieSameSite
);
header('Set-Cookie: ' . $cookie->__toString(), false);
$this->cookieWasSetOnCurrentRequest = true;
$this->logger->debug(
($isRefreshTimeBasedCookie ? 'Updated Cookie: ' : 'Set Cookie: ')
. $this->id . ($cookieDomain ? ', ' . $cookieDomain : '')
......@@ -946,14 +958,21 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface
/**
* Empty / unset the cookie
*
* @param string $cookieName usually, this is $this->name
* @param string|null $cookieName usually, this is $this->name
*/
public function removeCookie($cookieName)
public function removeCookie($cookieName = null)
{
$cookieName = $cookieName ?? $this->name;
$cookieDomain = $this->getCookieDomain();
// If no cookie domain is set, use the base path
$cookiePath = $cookieDomain ? '/' : GeneralUtility::getIndpEnv('TYPO3_SITE_PATH');
setcookie($cookieName, '', -1, $cookiePath, $cookieDomain);
$this->setCookie = new Cookie(
$cookieName,
'',
-1,
$cookiePath,
$cookieDomain
);
}
/**
......@@ -989,7 +1008,7 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface
*/
public function isCookieSet()
{
return $this->cookieWasSetOnCurrentRequest || $this->getCookie($this->name);
return isset($this->setCookie) || $this->getCookie($this->name);
}
/*************************
......
.. include:: ../../Includes.txt
================================================================================
Feature: #93011 - Authentication-related cookies are attached to PSR-7 Responses
================================================================================
See :issue:`93011`
Description
===========
Cookies, used to keep the session identifiers for Frontend sessions
and Backend user sessions, were previously added natively via PHP
:php:`header()` and :php:`setcookie()` at the very beginning
of the User Authentication workflow, although this did not allow
to later-on manipulate these HTTP response headers, as they were
emitted directly via PHP when calling the native PHP functions.
TYPO3 now attaches the cookie information for the user session information in
the PSR-7 Responses, by default in a PSR-15 middleware.
Impact
======
It is now possible to attach the cookies to a PSR-7 Response via
:php:`$GLOBALS[BE_USER]->appendCookieToResponse()`, which is especially handy
in custom middlewares that have custom endpoints when using other PHP
frameworks via the PSR-15 middleware stack.
.. index:: Backend, Frontend, ext:core
......@@ -75,6 +75,7 @@ class FrontendUserAuthenticator implements MiddlewareInterface
// Store session data for fe_users if it still exists
if ($frontendUser instanceof FrontendUserAuthentication) {
$frontendUser->storeSessionData();
$response = $frontendUser->appendCookieToResponse($response);
if ($frontendUser->sendNoCacheHeaders) {
$response = $this->applyHeadersToResponse($response);
}
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment