From 0308595bc544e9ceab05106b248a04ab3cef0d48 Mon Sep 17 00:00:00 2001
From: Torben Hansen <derhansen@gmail.com>
Date: Wed, 8 Mar 2023 19:38:33 +0100
Subject: [PATCH] [FEATURE] Make PSR-7 request accessible for authentication
 services

Custom TYPO3 authentication services can now directly
access the PSR-7 Request object via the $authInfo array,
handed over to the initAuth() method of those services.
This therefore allows to further reduce usages of PHP
super globals and `GeneralUtility::getIndpEnv()`.

Resolves: #100116
Releases: main
Signed-off-by: Torben Hansen <derhansen@gmail.com>
Change-Id: I12a3484b49862886e7013dc2106a0705ef39c91f
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/78077
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: core-ci <typo3@b13.com>
Tested-by: Oliver Bartsch <bo@cedev.de>
Reviewed-by: Oliver Bartsch <bo@cedev.de>
---
 .../AbstractUserAuthentication.php            | 24 ++++++-----
 ...estAccessibleForAuthenticationServices.rst | 43 +++++++++++++++++++
 .../AbstractUserAuthenticationTest.php        |  3 +-
 .../Controller/BackendModuleController.php    |  4 +-
 4 files changed, 60 insertions(+), 14 deletions(-)
 create mode 100644 typo3/sysext/core/Documentation/Changelog/12.3/Feature-100116-MakePSR-7RequestAccessibleForAuthenticationServices.rst

diff --git a/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php b/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php
index 2c4859ddddae..ba7602c4a3af 100644
--- a/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php
+++ b/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php
@@ -254,6 +254,7 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface
     public function start(ServerRequestInterface $request)
     {
         $this->logger->debug('## Beginning of auth logging.');
+
         // Make certain that NO user is set initially
         $this->user = null;
 
@@ -502,7 +503,7 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface
         }
 
         // Fetch users from the database (or somewhere else)
-        $possibleUsers = $this->fetchPossibleUsers($loginData, $activeLogin, $isExistingSession, $authenticatedUserFromSession);
+        $possibleUsers = $this->fetchPossibleUsers($loginData, $activeLogin, $isExistingSession, $authenticatedUserFromSession, $request);
 
         // If no new user was set we use the already found user session
         if (empty($possibleUsers) && $isExistingSession && !$anonymousSession) {
@@ -534,7 +535,7 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface
                 $subType = 'authUser' . $this->loginType;
 
                 /** @var AuthenticationService $serviceObj */
-                foreach ($this->getAuthServices($subType, $loginData, $authenticatedUserFromSession) as $serviceObj) {
+                foreach ($this->getAuthServices($subType, $loginData, $authenticatedUserFromSession, $request) as $serviceObj) {
                     if (($ret = (int)$serviceObj->authUser($userRecordCandidate)) > 0) {
                         // If the service returns >=200 then no more checking is needed - useful for IP checking without password
                         if ($ret >= 200) {
@@ -561,7 +562,7 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface
         // @link https://cwe.mitre.org/data/definitions/208.html
         } elseif ($activeLogin) {
             $subType = 'authUser' . $this->loginType;
-            foreach ($this->getAuthServices($subType, $loginData, $authenticatedUserFromSession) as $serviceObj) {
+            foreach ($this->getAuthServices($subType, $loginData, $authenticatedUserFromSession, $request) as $serviceObj) {
                 if ($serviceObj instanceof MimicServiceInterface && $serviceObj->mimicAuthUser() === false) {
                     break;
                 }
@@ -647,7 +648,7 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface
      *
      * @param array|null $authenticatedUserFromSession if we have a user from an existing session, this is set here, otherwise null
      */
-    protected function fetchPossibleUsers(array $loginData, bool $activeLogin, bool $isExistingSession, ?array $authenticatedUserFromSession): array
+    protected function fetchPossibleUsers(array $loginData, bool $activeLogin, bool $isExistingSession, ?array $authenticatedUserFromSession, ServerRequestInterface $request): array
     {
         $possibleUsers = [];
         $authConfiguration = $this->getAuthServiceConfiguration();
@@ -662,7 +663,7 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface
             // First found user will be used
             $subType = 'getUser' . $this->loginType;
             /** @var AuthenticationService $serviceObj */
-            foreach ($this->getAuthServices($subType, $loginData, $authenticatedUserFromSession) as $serviceObj) {
+            foreach ($this->getAuthServices($subType, $loginData, $authenticatedUserFromSession, $request) as $serviceObj) {
                 $row = $serviceObj->getUser();
                 if (is_array($row)) {
                     $possibleUsers[] = $row;
@@ -742,11 +743,11 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface
      * @param array|null $authenticatedUserFromSession the user which was loaded from the session, or null if none was found
      * @return \Traversable A generator of service objects
      */
-    protected function getAuthServices(string $subType, array $loginData, ?array $authenticatedUserFromSession): \Traversable
+    protected function getAuthServices(string $subType, array $loginData, ?array $authenticatedUserFromSession, ServerRequestInterface $request): \Traversable
     {
         $serviceChain = [];
         // The info array provide additional information for auth services
-        $authInfo = $this->getAuthInfoArray();
+        $authInfo = $this->getAuthInfoArray($request);
         if ($authenticatedUserFromSession !== null) {
             $authInfo['user'] = $authenticatedUserFromSession;
         }
@@ -1118,7 +1119,7 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface
         ];
         // Only process the login data if a login is requested
         if ($loginData['status'] === LoginType::LOGIN) {
-            $loginData = $this->processLoginData($loginData);
+            $loginData = $this->processLoginData($loginData, $request);
         }
         return $loginData;
     }
@@ -1136,14 +1137,14 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface
      * @return array
      * @internal
      */
-    public function processLoginData($loginData)
+    public function processLoginData(array $loginData, ServerRequestInterface $request): array
     {
         $this->logger->debug('Login data before processing', $this->removeSensitiveLoginDataForLoggingInfo($loginData));
         $subType = 'processLoginData' . $this->loginType;
         $isLoginDataProcessed = false;
         $processedLoginData = $loginData;
         /** @var AuthenticationService $serviceObject */
-        foreach ($this->getAuthServices($subType, $loginData, null) as $serviceObject) {
+        foreach ($this->getAuthServices($subType, $loginData, null, $request) as $serviceObject) {
             $serviceResult = $serviceObject->processLoginData($processedLoginData, 'normal');
             if (!empty($serviceResult)) {
                 $isLoginDataProcessed = true;
@@ -1194,12 +1195,13 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface
      * @return array
      * @internal
      */
-    public function getAuthInfoArray()
+    public function getAuthInfoArray(ServerRequestInterface $request)
     {
         $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->user_table);
         $expressionBuilder = $queryBuilder->expr();
         $authInfo = [];
         $authInfo['loginType'] = $this->loginType;
+        $authInfo['request'] = $request;
         $authInfo['refInfo'] = parse_url(GeneralUtility::getIndpEnv('HTTP_REFERER'));
         $authInfo['HTTP_HOST'] = GeneralUtility::getIndpEnv('HTTP_HOST');
         $authInfo['REMOTE_ADDR'] = GeneralUtility::getIndpEnv('REMOTE_ADDR');
diff --git a/typo3/sysext/core/Documentation/Changelog/12.3/Feature-100116-MakePSR-7RequestAccessibleForAuthenticationServices.rst b/typo3/sysext/core/Documentation/Changelog/12.3/Feature-100116-MakePSR-7RequestAccessibleForAuthenticationServices.rst
new file mode 100644
index 000000000000..b5257fa80a80
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/12.3/Feature-100116-MakePSR-7RequestAccessibleForAuthenticationServices.rst
@@ -0,0 +1,43 @@
+.. include:: /Includes.rst.txt
+
+.. _feature-100116-1678299307:
+
+============================================================================
+Feature: #100116 - Make PSR-7 Request accessible for authentication services
+============================================================================
+
+See :issue:`100116`
+
+Description
+===========
+
+Authentication services can now access the PSR-7 Request object via the
+:php:`$authInfo` array. Previously, custom TYPO3 authentication services
+did not have direct access to the object and therefore had to either
+use PHP super globals or TYPO3's `GeneralUtility::getIndpEnv()` method.
+
+The following example shows how to retrieve the PSR-7 Request in the
+`initAuth()` method of a custom authentication service:
+
+..  code-block:: php
+
+    public function initAuth($mode, $loginData, $authInfo, $pObj)
+    {
+        /** @var ServerRequestInterface $request */
+        $request = $authInfo['request'];
+
+        /** @var NormalizedParams $normalizedParams */
+        $normalizedParams = $request->getAttribute('normalizedParams');
+        $isHttps = $normalizedParams->isHttps();
+    }
+
+
+Impact
+======
+
+Custom TYPO3 authentication services can now directly access the PSR-7
+Request object from the authentication process. It is available via the
+:php:`request` key of the :php:`$authInfo` array, which is handed over
+to the :php:`initAuth()` method.
+
+.. index:: ext:core
diff --git a/typo3/sysext/core/Tests/Functional/Authentication/AbstractUserAuthenticationTest.php b/typo3/sysext/core/Tests/Functional/Authentication/AbstractUserAuthenticationTest.php
index 233c30e1b509..7e534c573b16 100644
--- a/typo3/sysext/core/Tests/Functional/Authentication/AbstractUserAuthenticationTest.php
+++ b/typo3/sysext/core/Tests/Functional/Authentication/AbstractUserAuthenticationTest.php
@@ -18,6 +18,7 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Core\Tests\Functional\Authentication;
 
 use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
+use TYPO3\CMS\Core\Http\ServerRequest;
 use TYPO3\CMS\Core\Session\UserSession;
 use TYPO3\CMS\Core\Tests\Functional\Authentication\Fixtures\AnyUserAuthentication;
 use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
@@ -80,7 +81,7 @@ class AbstractUserAuthenticationTest extends FunctionalTestCase
         $this->subject->user_table = 'be_users';
         $this->subject->checkPid_value = null;
 
-        $authInfoArray = $this->subject->getAuthInfoArray();
+        $authInfoArray = $this->subject->getAuthInfoArray(new ServerRequest('https://example.com'));
 
         $enableClause = $authInfoArray['db_user']['enable_clause'];
         self::assertInstanceOf(CompositeExpression::class, $enableClause);
diff --git a/typo3/sysext/install/Classes/Controller/BackendModuleController.php b/typo3/sysext/install/Classes/Controller/BackendModuleController.php
index 2556d097470d..974227edf4ae 100644
--- a/typo3/sysext/install/Classes/Controller/BackendModuleController.php
+++ b/typo3/sysext/install/Classes/Controller/BackendModuleController.php
@@ -226,8 +226,8 @@ class BackendModuleController
         ];
         // currently there is no dedicated API to perform authentication
         // that's why this process partially has to be simulated here
-        $loginData = $backendUser->processLoginData($loginData);
-        $authInfo = $backendUser->getAuthInfoArray();
+        $loginData = $backendUser->processLoginData($loginData, $request);
+        $authInfo = $backendUser->getAuthInfoArray($request);
 
         $authenticated = false;
         /** @var AbstractAuthenticationService $service or any other service (sic!) */
-- 
GitLab