From 9b7884ce4dbee765d5e06e68de086920cde0d37e Mon Sep 17 00:00:00 2001 From: Markus Poerschke Date: Thu, 13 Aug 2020 14:59:59 +0200 Subject: [PATCH 001/142] Remove empty lines from .gitignore and order alphabetically (#41) --- .gitignore | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index dbc11c3..b059ff7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,4 @@ /.Build/* /composer.lock -/typo3cms - /Libraries/* - +/typo3cms From d0caaee3235e16f299aeae30fdeb24e8a84d4ab2 Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Fri, 21 Aug 2020 16:59:11 +0200 Subject: [PATCH 002/142] [TASK] Bump extension as stable and version 1.0.0 --- composer.json | 4 ++-- ext_emconf.php | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index f1cf629..b2a223c 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ ], "license": "GPL-2.0-or-later", "require": { - "php": ">= 7.0.0, <= 7.3.99", + "php": ">= 7.0.0, <= 7.4.99", "typo3/cms-core": "^8.7 || ^9.5", "league/oauth2-client": "^2.0" }, @@ -66,7 +66,7 @@ }, "extra": { "branch-alias": { - "dev-master": "0.5.x-dev" + "dev-master": "1.0.x-dev" }, "typo3/cms": { "extension-key": "oidc", diff --git a/ext_emconf.php b/ext_emconf.php index 5970a2d..f7391b8 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -11,17 +11,17 @@ 'conflicts' => '', 'priority' => '', 'module' => '', - 'state' => 'beta', + 'state' => 'stable', 'internal' => '', 'uploadfolder' => 0, 'createDirs' => '', 'modify_tables' => '', 'clearCacheOnLoad' => 0, 'lockType' => '', - 'version' => '0.5.0', + 'version' => '1.0.0', 'constraints' => [ 'depends' => [ - 'php' => '7.0.0-7.3.99', + 'php' => '7.0.0-7.4.99', 'typo3' => '8.7.0-9.5.99', ], 'conflicts' => [], From e9df1d6e2c125351a207b53520af4c83a3f9fefe Mon Sep 17 00:00:00 2001 From: Thomas Maroschik Date: Mon, 5 Oct 2020 15:39:19 +0200 Subject: [PATCH 003/142] [TASK] Make extension compatible with TYPO3 v10 --- composer.json | 2 +- ext_emconf.php | 2 +- ext_localconf.php | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index b2a223c..45a1075 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "license": "GPL-2.0-or-later", "require": { "php": ">= 7.0.0, <= 7.4.99", - "typo3/cms-core": "^8.7 || ^9.5", + "typo3/cms-core": "^8.7 || ^9.5 || ^10.4", "league/oauth2-client": "^2.0" }, "autoload": { diff --git a/ext_emconf.php b/ext_emconf.php index f7391b8..fbde817 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -22,7 +22,7 @@ 'constraints' => [ 'depends' => [ 'php' => '7.0.0-7.4.99', - 'typo3' => '8.7.0-9.5.99', + 'typo3' => '8.7.0-10.4.99', ], 'conflicts' => [], 'suggests' => [], diff --git a/ext_localconf.php b/ext_localconf.php index 62da84b..f190200 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -2,6 +2,11 @@ defined('TYPO3_MODE') || die(); $boot = function ($_EXTKEY) { + if (class_exists(\TYPO3\CMS\Core\Authentication\AuthenticationService::class) + && !class_exists(\TYPO3\CMS\Sv\AuthenticationService::class)) { + class_alias(\TYPO3\CMS\Core\Authentication\AuthenticationService::class, \TYPO3\CMS\Sv\AuthenticationService::class); + } + // Configuration of authentication service $typo3Branch = class_exists(\TYPO3\CMS\Core\Information\Typo3Version::class) ? (new \TYPO3\CMS\Core\Information\Typo3Version())->getBranch() From f6dce833ec2a224c54e5b62df196f62dcb34b93a Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Wed, 2 Dec 2020 09:04:09 +0100 Subject: [PATCH 004/142] [TASK] Switch to static anonymous function --- ext_localconf.php | 7 ++----- ext_tables.php | 7 ++----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/ext_localconf.php b/ext_localconf.php index 62da84b..cc878b1 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -1,7 +1,7 @@ getBranch() @@ -67,7 +67,4 @@ if (is_file($pharFileName)) { @include 'phar://' . $pharFileName . '/vendor/autoload.php'; } -}; - -$boot('oidc'); -unset($boot); +})('oidc'); diff --git a/ext_tables.php b/ext_tables.php index 9022205..32da868 100644 --- a/ext_tables.php +++ b/ext_tables.php @@ -1,7 +1,7 @@ Date: Wed, 2 Dec 2020 09:04:09 +0100 Subject: [PATCH 005/142] [TASK] Switch to static anonymous function --- ext_localconf.php | 7 ++----- ext_tables.php | 7 ++----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/ext_localconf.php b/ext_localconf.php index f190200..f560d36 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -1,7 +1,7 @@ Date: Mon, 4 Jan 2021 14:31:19 +0100 Subject: [PATCH 006/142] [FEATURE] Add signal to re-use access token retrieved on login (#49) --- Classes/Service/AuthenticationService.php | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 8f77f45..51540d4 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -14,6 +14,7 @@ namespace Causal\Oidc\Service; +use Causal\Oidc\Service\OAuthService; use League\OAuth2\Client\Token\AccessToken; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction; @@ -21,8 +22,9 @@ use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\HttpUtility; -use Causal\Oidc\Service\OAuthService; use TYPO3\CMS\Core\Utility\RootlineUtility; +use TYPO3\CMS\Extbase\Object\ObjectManager; +use TYPO3\CMS\Extbase\SignalSlot\Dispatcher; use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication; /** @@ -100,6 +102,15 @@ public function getUser() $user = $this->authenticateWithResourceOwnerPasswordCredentials($username, $password); } + // dispatch a signal (containing the user with his access token if auth was successful) + // so other extensions can use them to make further requests to an API + // provided by the authentication server + $dispatcher = GeneralUtility::makeInstance(ObjectManager::class)->get(Dispatcher::class); + $dispatcher->dispatch(__CLASS__, 'getUser', ['user' => $user]); + if (is_array($user)) { + unset($user['accessToken']); + } + return $user; } @@ -132,6 +143,10 @@ protected function authenticateWithAuhorizationCode($code) } $user = $this->getUserFromAccessToken($service, $accessToken); + if (is_array($user)) { + $user['accessToken'] = $accessToken; + } + return $user; } @@ -170,6 +185,10 @@ protected function authenticateWithResourceOwnerPasswordCredentials($username, $ ]); } + if (is_array($user)) { + $user['accessToken'] = $accessToken; + } + return $user; } From 40c3c55cbfa907352b23572baa63bd2787b267c2 Mon Sep 17 00:00:00 2001 From: j-schumann <114239+j-schumann@users.noreply.github.com> Date: Tue, 19 Jan 2021 17:19:23 +0100 Subject: [PATCH 007/142] Fixes #47: static use of SaltFactory (#50) * fix: deprecated use of non-static method * upd: replace SaltFactory with PasswordHashFactory --- Classes/Service/AuthenticationService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 51540d4..340e947 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -304,7 +304,7 @@ protected function convertResourceOwner(array $info) ? (new \TYPO3\CMS\Core\Information\Typo3Version())->getBranch() : TYPO3_branch; if (version_compare($typo3Branch, '9.0', '>=')) { - $objInstanceSaltedPW = \TYPO3\CMS\Saltedpasswords\Salt\SaltFactory::getDefaultHashInstance(TYPO3_MODE); + $objInstanceSaltedPW = \TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory::getDefaultHashInstance(TYPO3_MODE); } else { $objInstanceSaltedPW = \TYPO3\CMS\Saltedpasswords\Salt\SaltFactory::getSaltingInstance(null, TYPO3_MODE); } From 2f95893aec654cd12031e14498146212d3d56758 Mon Sep 17 00:00:00 2001 From: Philipp Date: Tue, 19 Jan 2021 17:20:43 +0100 Subject: [PATCH 008/142] #52 handle groups coming from OIDC as array instead of comma separated list (#53) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Philipp Seßner --- Classes/Service/AuthenticationService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 340e947..2c175ef 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -368,7 +368,7 @@ protected function convertResourceOwner(array $info) ->execute() ->fetchAll(); - $roles = GeneralUtility::trimExplode(',', $info['Roles'], true); + $roles = is_array($info['Roles']) ? $info['Roles'] : GeneralUtility::trimExplode(',', $info['Roles'], true); $roles = ',' . implode(',', $roles) . ','; foreach ($typo3Roles as $typo3Role) { From dbaafb602a4396899051506de6bad15e848c9bd8 Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Tue, 9 Feb 2021 09:14:25 +0100 Subject: [PATCH 009/142] [BUGFIX] Fix compatibility with helhum/typo3-secure-web --- .../Public/Icons/Extension.png | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename ext_icon.png => Resources/Public/Icons/Extension.png (100%) diff --git a/ext_icon.png b/Resources/Public/Icons/Extension.png similarity index 100% rename from ext_icon.png rename to Resources/Public/Icons/Extension.png From 362315b578e431d20fe2491c8b1cc44013748f22 Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Tue, 9 Feb 2021 10:38:29 +0100 Subject: [PATCH 010/142] [BUGFIX] Prevent non-static method call with PasswordHashing --- Classes/Service/AuthenticationService.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 2c175ef..235b596 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -304,7 +304,8 @@ protected function convertResourceOwner(array $info) ? (new \TYPO3\CMS\Core\Information\Typo3Version())->getBranch() : TYPO3_branch; if (version_compare($typo3Branch, '9.0', '>=')) { - $objInstanceSaltedPW = \TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory::getDefaultHashInstance(TYPO3_MODE); + $passwordHashFactory = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory::class); + $objInstanceSaltedPW = $passwordHashFactory->getDefaultHashInstance(TYPO3_MODE); } else { $objInstanceSaltedPW = \TYPO3\CMS\Saltedpasswords\Salt\SaltFactory::getSaltingInstance(null, TYPO3_MODE); } From ec29415b39207857814cb1932c3565058a767d5e Mon Sep 17 00:00:00 2001 From: r3h6 Date: Tue, 9 Feb 2021 14:14:48 +0100 Subject: [PATCH 011/142] [BUGFIX] Make authentication service compatible with TYPO3 v10 Resolves: #59 --- Classes/Service/AuthenticationService.php | 46 ++++++++++++++++++----- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 235b596..4e5343f 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -14,18 +14,21 @@ namespace Causal\Oidc\Service; -use Causal\Oidc\Service\OAuthService; use League\OAuth2\Client\Token\AccessToken; +use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction; use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction; use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction; +use TYPO3\CMS\Core\Http\ServerRequestFactory; +use TYPO3\CMS\Core\Routing\SiteMatcher; +use TYPO3\CMS\Core\TypoScript\TemplateService; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Core\Utility\HttpUtility; use TYPO3\CMS\Core\Utility\RootlineUtility; use TYPO3\CMS\Extbase\Object\ObjectManager; use TYPO3\CMS\Extbase\SignalSlot\Dispatcher; use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication; +use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; /** * OpenID Connect authentication service. @@ -303,7 +306,7 @@ protected function convertResourceOwner(array $info) $typo3Branch = class_exists(\TYPO3\CMS\Core\Information\Typo3Version::class) ? (new \TYPO3\CMS\Core\Information\Typo3Version())->getBranch() : TYPO3_branch; - if (version_compare($typo3Branch, '9.0', '>=')) { + if (version_compare($typo3Branch, '9.5', '>=')) { $passwordHashFactory = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory::class); $objInstanceSaltedPW = $passwordHashFactory->getDefaultHashInstance(TYPO3_MODE); } else { @@ -666,15 +669,40 @@ protected function getTypoScriptSetup() $GLOBALS['TCA'][$table] = include($file); } - /** @var \TYPO3\CMS\Frontend\Page\PageRepository $pageRepository */ - $pageRepository = GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\Page\PageRepository::class); - $pageRepository->init(false); - - /** @var \TYPO3\CMS\Core\TypoScript\TemplateService $templateService */ - $templateService = GeneralUtility::makeInstance(\TYPO3\CMS\Core\TypoScript\TemplateService::class); $typo3Branch = class_exists(\TYPO3\CMS\Core\Information\Typo3Version::class) ? (new \TYPO3\CMS\Core\Information\Typo3Version())->getBranch() : TYPO3_branch; + + if (version_compare($typo3Branch, '10.0', '>=')) { + /** @var ServerRequestInterface $request */ + $request = $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals(); + /** @var SiteMatcher $siteMatcher */ + $siteMatcher = GeneralUtility::makeInstance(SiteMatcher::class); + $routeResult = $siteMatcher->matchRequest($request); + $site = $routeResult->getSite(); + $pageArguments = $site->getRouter()->matchRequest($request, $routeResult); + $currentPage = $pageArguments->getPageId(); + + $frontendUser = GeneralUtility::makeInstance(FrontendUserAuthentication::class); + $localTSFE = GeneralUtility::makeInstance(TypoScriptFrontendController::class, null, $site, $routeResult->getLanguage(), $pageArguments, $frontendUser); + + /** @var TemplateService $templateService */ + $templateService = GeneralUtility::makeInstance(TemplateService::class, null, null, $localTSFE); + + $rootLine = GeneralUtility::makeInstance(RootlineUtility::class, (int)$currentPage)->get(); + $templateService->start($rootLine); + $setup = $templateService->setup; + return $setup; + } + + /** @var \TYPO3\CMS\Frontend\Page\PageRepository $pageRepository */ + $pageRepository = GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\Page\PageRepository::class); + if (version_compare($typo3Branch, '9.0', '<')) { + $pageRepository->init(false); + } + + /** @var TemplateService $templateService */ + $templateService = GeneralUtility::makeInstance(TemplateService::class); if (version_compare($typo3Branch, '9.0', '<')) { $templateService->init(); } From 509315d54d4734566e6e65b0118cc9a8ea89a986 Mon Sep 17 00:00:00 2001 From: cjanody Date: Tue, 9 Feb 2021 08:17:42 -0500 Subject: [PATCH 012/142] [TASK] Add TYPO3 Core editorconfig file (#61) Co-authored-by: Cyril Janody --- .editorconfig | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d37a2c5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,52 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +# TS/JS-Files +[*.{ts,js}] +indent_size = 2 + +# JSON-Files +[*.json] +indent_style = tab + +# ReST-Files +[*.rst] +indent_size = 3 +max_line_length = 80 + +# YAML-Files +[*.{yaml,yml}] +indent_size = 2 + +# package.json +# .travis.yml +[{package.json,.travis.yml}] +indent_size = 2 + +# TypoScript +[*.{typoscript,tsconfig}] +indent_size = 2 + +# XLF-Files +[*.xlf] +indent_style = tab + +# SQL-Files +[*.sql] +indent_style = tab +indent_size = 2 + +# .htaccess +[{_.htaccess,.htaccess}] +indent_style = tab From 1aeec275c96094943a4aec66d2287329d44bd571 Mon Sep 17 00:00:00 2001 From: Felix Althaus Date: Tue, 9 Feb 2021 14:21:37 +0100 Subject: [PATCH 013/142] Add PKCE support (#56) * [FEATURE] Add PKCE Proof of Key for Code Exchange enableCodeVerifier switch in extension configuration is used to enable the PKCE flow. No further configuration is needed. Implemented for authentication without password only. * [FEATURE] Make redirect URI configurable You can now configure the URI that will be called back by the authorization server (oidcRedirectUri). If left blank the standard callback script is used. --- Classes/Controller/LoginController.php | 31 +++++++++++++++++++-- Classes/Service/AuthenticationService.php | 18 ++++++++++-- Classes/Service/OAuthService.php | 18 +++++++----- README.md | 16 +++++++---- Resources/Private/Language/locallang_db.xlf | 6 ++++ ext_conf_template.txt | 6 ++++ 6 files changed, 78 insertions(+), 17 deletions(-) diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php index 78a364c..7c5935c 100644 --- a/Classes/Controller/LoginController.php +++ b/Classes/Controller/LoginController.php @@ -82,11 +82,16 @@ protected function performRedirectToLogin() $service = GeneralUtility::makeInstance(\Causal\Oidc\Service\OAuthService::class); $service->setSettings($this->settings); - $authorizationUrl = $service->getAuthorizationUrl(); - if (session_id() === '') { session_start(); } + if ($this->settings['enableCodeVerifier']) { + $codeVerifier = $this->generateCodeVerifier(); + $codeChallenge = $this->convertVerifierToChallenge($codeVerifier); + $options = $this->addCodeChallengeToOptions($codeChallenge); + $_SESSION['oidc_code_verifier'] = $codeVerifier; + } + $authorizationUrl = $service->getAuthorizationUrl($options?: []); $state = $service->getState(); $_SESSION['oidc_state'] = $state; @@ -118,4 +123,26 @@ protected function determineRedirectUrl() return '/'; } + + protected function generateCodeVerifier(): string + { + return bin2hex(random_bytes(64)); + } + + protected function convertVerifierToChallenge($codeVerifier) + { + return rtrim(strtr(base64_encode(hash('sha256', $codeVerifier, true)), '+/', '-_'), '='); + } + + protected function addCodeChallengeToOptions($codeChallenge, array $options = []): array + { + return array_merge( + $options, + [ + 'code_challenge' => $codeChallenge, + 'code_challenge_method' => 'S256', + ] + ); + } + } diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 4e5343f..7c1f7e2 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -78,6 +78,14 @@ public function __construct() } } + protected function getCodeVerifierFromSession() + { + if (session_id() === '') { + session_start(); + } + return @$_SESSION['oidc_code_verifier']; + } + /** * Finds a user. * @@ -100,7 +108,11 @@ public function getUser() } if ($code !== null) { - $user = $this->authenticateWithAuhorizationCode($code); + $codeVerifier = null; + if ($this->config['enableCodeVerifier']) { + $codeVerifier = $this->getCodeVerifierFromSession(); + } + $user = $this->authenticateWithAuhorizationCode($code, $codeVerifier); } elseif (!(empty($username) || empty($password))) { $user = $this->authenticateWithResourceOwnerPasswordCredentials($username, $password); } @@ -123,7 +135,7 @@ public function getUser() * @param string $code * @return array|bool */ - protected function authenticateWithAuhorizationCode($code) + protected function authenticateWithAuhorizationCode($code, $codeVerifier = null) { static::getLogger()->debug('Initializing OpenID Connect service'); @@ -134,7 +146,7 @@ protected function authenticateWithAuhorizationCode($code) // Try to get an access token using the authorization code grant try { static::getLogger()->debug('Retrieving an access token'); - $accessToken = $service->getAccessToken($code); + $accessToken = $service->getAccessToken($code, null, $codeVerifier); static::getLogger()->debug('Access token retrieved', $accessToken->jsonSerialize()); } catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) { // Probably a "server_error", meaning the code is not valid anymore diff --git a/Classes/Service/OAuthService.php b/Classes/Service/OAuthService.php index e00f6d1..e87b190 100644 --- a/Classes/Service/OAuthService.php +++ b/Classes/Service/OAuthService.php @@ -86,15 +86,19 @@ public function getState() * credentials grant. * * @param string $codeOrUsername Either a code or the username (if password is provided) - * @param string $password Optional parameter if authenticating with authorization code grant + * @param null $password Optional parameter if authenticating with authorization code grant + * @param null $codeVerifier Code verifier for PKCE * @return AccessToken + * @throws \League\OAuth2\Client\Provider\Exception\IdentityProviderException */ - public function getAccessToken($codeOrUsername, $password = null) + public function getAccessToken($codeOrUsername, $password = null, $codeVerifier = null) { if ($password === null) { - $accessToken = $this->getProvider()->getAccessToken('authorization_code', [ - 'code' => $codeOrUsername, - ]); + $options = ['code' => $codeOrUsername]; + if ($codeVerifier !== null) { + $options['code_verifier'] = $codeVerifier; + } + $accessToken = $this->getProvider()->getAccessToken('authorization_code', $options); } else { $accessToken = $this->getProvider()->getAccessToken('password', [ 'username' => $codeOrUsername, @@ -118,7 +122,7 @@ public function getAccessToken($codeOrUsername, $password = null) */ public function getAccessTokenWithRequestPathAuthentication($username, $password) { - $redirectUri = GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST') . '/typo3conf/ext/oidc/Resources/Public/callback.php'; + $redirectUri = $this->settings['oidcRedirectUri'] ?: GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST') . '/typo3conf/ext/oidc/Resources/Public/callback.php'; $url = $this->settings['oidcEndpointAuthorize'] . '?'. http_build_query([ 'response_type' => 'code', 'client_id' => $this->settings['oidcClientKey'], @@ -206,7 +210,7 @@ public function revokeToken(AccessToken $token) protected function getProvider() { if ($this->provider === null) { - $redirectUri = GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST') . '/typo3conf/ext/oidc/Resources/Public/callback.php'; + $redirectUri = $this->settings['oidcRedirectUri'] ?: GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST') . '/typo3conf/ext/oidc/Resources/Public/callback.php'; $this->provider = new \League\OAuth2\Client\Provider\GenericProvider([ 'clientId' => $this->settings['oidcClientKey'], diff --git a/README.md b/README.md index c6cb742..42ef690 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ the authorization endpoint of the authorization server. ## OIDC Login -If openid_connect is your only means of frontend login, you can use the included "OIDC Login" plugin. Add it to your -login page, where you would normally add the felogin box. After adding the OIDC Login plugin, requests to the login +If openid_connect is your only means of frontend login, you can use the included "OIDC Login" plugin. Add it to your +login page, where you would normally add the felogin box. After adding the OIDC Login plugin, requests to the login page will immediately be redirected to the authorization server. After the login process, the user will be redirected: @@ -26,7 +26,13 @@ After the login process, the user will be redirected: - If no parameter is set, OIDC Login will redirect the user to the page configured at `plugin.tx_oidc_login.defaultRedirectPid`. - If that configuration is not set either, the user will be redirected to '/'. - + +## PKCE (Proof of Key for Code Exchange) + +If your OIDC Login supports _Proof of Key for Code Exchange_ you can enable it by +checking `enableCodeVerifier` in the extension configuration. A shared secret will +be sent along preventing _Authorization Code Interception Attacks_. See +https://tools.ietf.org/html/rfc7636 for details. ## Configuring @@ -69,8 +75,8 @@ After the login process, the user will be redirected: ### OIDC Login -- `plugin.tx_oidc_login.defaultRedirectPid` UID of the page that users will be redirected to, if no `redirect_url` -parameter is set. +- `plugin.tx_oidc_login.defaultRedirectPid` UID of the page that users will be redirected to, if no `redirect_url` +parameter is set. ## Logging diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index 68d4b27..38c0be6 100755 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -55,9 +55,15 @@ Disable CSRF attack mitigation: CAUTION! This is a security protection which checks the return state with the expected value. Disable this protection at your own risk. + + Enable PKCE: Enable PKCE flow. Code challenge and code verifier will be sent along. + Case-insensitive pattern to match OpenID Connect role names ("*" matches every character, "|" to separate expressions) + + Redirect URI: The authentication server callback will point to this URI. + OpenID Connect Identifier diff --git a/ext_conf_template.txt b/ext_conf_template.txt index 7cc09e8..bd97d71 100644 --- a/ext_conf_template.txt +++ b/ext_conf_template.txt @@ -10,12 +10,18 @@ undeleteFrontendUsers = 0 # cat=basic/enable/4; type=boolean; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.frontendUserMustExistLocally frontendUserMustExistLocally = 0 +# cat=basic/enable/5; type=boolean; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.enableCodeVerifier +enableCodeVerifier = 0 + # cat=basic//1; type=int; label=Storage Pid: The Storage Pid of the Page, where the fe_users should be stored usersStoragePid = # cat=basic//2; type=string; label=Default user group(s) (comma-separated list of UIDs) usersDefaultGroup = +# cat=basic//2a; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcRedirectUri +oidcRedirectUri = + # cat=basic//3; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcClientKey oidcClientKey = From c1af3ef79d5cc65139d87a2417dfd1360b249e1f Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Tue, 9 Feb 2021 14:59:35 +0100 Subject: [PATCH 014/142] [TASK] Migrate TCA registration --- Configuration/TCA/Overrides/sys_template.php | 16 ++++++++++++++++ ext_tables.php | 6 ------ 2 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 Configuration/TCA/Overrides/sys_template.php diff --git a/Configuration/TCA/Overrides/sys_template.php b/Configuration/TCA/Overrides/sys_template.php new file mode 100644 index 0000000..96b1df7 --- /dev/null +++ b/Configuration/TCA/Overrides/sys_template.php @@ -0,0 +1,16 @@ + Date: Tue, 9 Feb 2021 15:38:43 +0100 Subject: [PATCH 015/142] [BUGFIX] Respect usersStoragePid Resolves #62 --- Classes/Service/AuthenticationService.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 2c175ef..02309a5 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -271,6 +271,7 @@ protected function convertResourceOwner(array $info) ->select('*') ->from($userTable) ->where( + $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter((int)$this->config['usersStoragePid'], \PDO::PARAM_STR)), $queryBuilder->expr()->eq('tx_oidc', $queryBuilder->createNamedParameter($info['sub'], \PDO::PARAM_STR)) ) ->execute() From 96c63e3c632fcd40b13f0f504200dc953e1e4346 Mon Sep 17 00:00:00 2001 From: Hannes Lau Date: Wed, 10 Feb 2021 10:32:04 +0100 Subject: [PATCH 016/142] [FEATURE] Login plugin: Allow to configure authorization URL parameters (#64) * [FEATURE] Login plugin: Allow to configure authorization URL parameters Allow to add query parameters to the authorization redirect URL via TypoScript. * [TASK] Add authorizationUrlOptions sample --- Classes/Controller/LoginController.php | 7 +++---- Configuration/TypoScript/setup.txt | 4 ++++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php index 7c5935c..ea75a23 100644 --- a/Classes/Controller/LoginController.php +++ b/Classes/Controller/LoginController.php @@ -72,11 +72,10 @@ public function login($_ = '', $pluginConfiguration) // performRedirectAfterLogin stops flow by emitting a redirect $this->performRedirectAfterLogin(); } - - $this->performRedirectToLogin(); + $this->performRedirectToLogin($pluginConfiguration['authorizationUrlOptions.']); } - protected function performRedirectToLogin() + protected function performRedirectToLogin(array $authorizationUrlOptions = []) { /** @var \Causal\Oidc\Service\OAuthService $service */ $service = GeneralUtility::makeInstance(\Causal\Oidc\Service\OAuthService::class); @@ -88,7 +87,7 @@ protected function performRedirectToLogin() if ($this->settings['enableCodeVerifier']) { $codeVerifier = $this->generateCodeVerifier(); $codeChallenge = $this->convertVerifierToChallenge($codeVerifier); - $options = $this->addCodeChallengeToOptions($codeChallenge); + $options = $this->addCodeChallengeToOptions($codeChallenge, $authorizationUrlOptions); $_SESSION['oidc_code_verifier'] = $codeVerifier; } $authorizationUrl = $service->getAuthorizationUrl($options?: []); diff --git a/Configuration/TypoScript/setup.txt b/Configuration/TypoScript/setup.txt index 790a620..f42bb97 100644 --- a/Configuration/TypoScript/setup.txt +++ b/Configuration/TypoScript/setup.txt @@ -40,4 +40,8 @@ plugin.tx_oidc.mapping { plugin.tx_oidc_login { userFunc = Causal\Oidc\Controller\LoginController->login defaultRedirectPid = + # Additional URL parameters for the authorization URL of the identity server + authorizationUrlOptions { + # login_theme = dark + } } From cdeb7891f56183fff5a093ef4fe012ad371e89d8 Mon Sep 17 00:00:00 2001 From: r3h6 Date: Wed, 10 Feb 2021 12:14:19 +0100 Subject: [PATCH 017/142] [BUGFIX] Use correct parameter type --- Classes/Service/AuthenticationService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 02309a5..b2d9b6c 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -271,7 +271,7 @@ protected function convertResourceOwner(array $info) ->select('*') ->from($userTable) ->where( - $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter((int)$this->config['usersStoragePid'], \PDO::PARAM_STR)), + $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter((int)$this->config['usersStoragePid'], \PDO::PARAM_INT)), $queryBuilder->expr()->eq('tx_oidc', $queryBuilder->createNamedParameter($info['sub'], \PDO::PARAM_STR)) ) ->execute() From 7407599af5800256cf756c9db879a9f1173b3c32 Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Wed, 10 Feb 2021 12:37:55 +0100 Subject: [PATCH 018/142] [TASK] Raise version to 1.1.0 (-dev) --- composer.json | 2 +- ext_emconf.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 45a1075..e25fc30 100644 --- a/composer.json +++ b/composer.json @@ -66,7 +66,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.1.x-dev" }, "typo3/cms": { "extension-key": "oidc", diff --git a/ext_emconf.php b/ext_emconf.php index fbde817..d016651 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -18,7 +18,7 @@ 'modify_tables' => '', 'clearCacheOnLoad' => 0, 'lockType' => '', - 'version' => '1.0.0', + 'version' => '1.1.0-dev', 'constraints' => [ 'depends' => [ 'php' => '7.0.0-7.4.99', From 3e418c213f087e22146b53dc02a94d6f6f89f706 Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Wed, 10 Feb 2021 12:40:41 +0100 Subject: [PATCH 019/142] [TASK] Clean-up meta information --- composer.json | 11 +---------- ext_emconf.php | 7 ------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/composer.json b/composer.json index e25fc30..55a9c89 100644 --- a/composer.json +++ b/composer.json @@ -34,15 +34,7 @@ "replace": { "typo3-ter/oidc": "self.version" }, - "config": { - "vendor-dir": ".Build/vendor", - "bin-dir": ".Build/bin" - }, "scripts": { - "post-autoload-dump": [ - "mkdir -p .Build/Web/typo3conf/ext/", - "[ -L .Build/Web/typo3conf/ext/oidc ] || ln -snvf ../../../../. .Build/Web/typo3conf/ext/oidc" - ], "extension-create-libs": [ "mkdir -p Libraries/temp", "[ -f $HOME/.composer/vendor/bin/phar-composer ] || composer global require clue/phar-composer", @@ -70,8 +62,7 @@ }, "typo3/cms": { "extension-key": "oidc", - "cms-package-dir": "{$vendor-dir}/typo3/cms", - "web-dir": ".Build/Web" + "cms-package-dir": "{$vendor-dir}/typo3/cms" } } } diff --git a/ext_emconf.php b/ext_emconf.php index d016651..703bd95 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -6,18 +6,11 @@ 'author' => 'Xavier Perseguers', 'author_company' => 'Causal Sàrl', 'author_email' => 'xavier@causal.ch', - 'shy' => '', - 'dependencies' => '', - 'conflicts' => '', - 'priority' => '', - 'module' => '', 'state' => 'stable', - 'internal' => '', 'uploadfolder' => 0, 'createDirs' => '', 'modify_tables' => '', 'clearCacheOnLoad' => 0, - 'lockType' => '', 'version' => '1.1.0-dev', 'constraints' => [ 'depends' => [ From 3e96511931d248dce697c149f01b8db5a6d7c5d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Remo=20H=C3=A4usler?= Date: Thu, 11 Feb 2021 16:08:23 +0100 Subject: [PATCH 020/142] [BUGFIX] Use makeInstance for creating hook objects Resolves #65 --- Classes/Service/AuthenticationService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 172c803..ead3e7a 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -465,7 +465,7 @@ protected function convertResourceOwner(array $info) if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['oidc']['resourceOwner'])) { foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['oidc']['resourceOwner'] as $className) { /** @var \Causal\Oidc\Service\ResourceOwnerHookInterface $postProcessor */ - $postProcessor = GeneralUtility::getUserObj($className); + $postProcessor = GeneralUtility::makeInstance($className); if ($postProcessor instanceof \Causal\Oidc\Service\ResourceOwnerHookInterface) { $postProcessor->postProcessUser(TYPO3_MODE, $user, $info); $reloadUserRecord = true; From 2960034a83884eab9b91a19844ca48482a90af9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Lademann?= Date: Mon, 28 Jun 2021 11:33:18 +0200 Subject: [PATCH 021/142] Just add translation to German --- .../Private/Language/de.locallang_db.xlf | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 Resources/Private/Language/de.locallang_db.xlf diff --git a/Resources/Private/Language/de.locallang_db.xlf b/Resources/Private/Language/de.locallang_db.xlf new file mode 100644 index 0000000..998e209 --- /dev/null +++ b/Resources/Private/Language/de.locallang_db.xlf @@ -0,0 +1,72 @@ + + + +
+ + + OIDC-Anmeldung + + + + Frontend-Authentifizierung: Aktivieren Sie die OpenID Connect-Authentifizierung für das Frontend. + + + Frontend-Benutzer wieder aktivieren: Wenn angekreuzt, werden als "deaktiviert" markierte Frontend-Benutzer nach erfolgreicher Authentifizierung automatisch wieder aktiviert. + + + Frontend-Benutzer wiederherstellen: Wenn angekreuzt, werden als "gelöscht" markierte Frontend-Benutzer nach erfolgreicher Authentifizierung automatisch wiederhergestellt. + + + Frontend-Benutzer muss vorhanden sein: Wenn dieses Häkchen gesetzt ist, können sich nur Frontend User, die lokal in TYPO3 vorhanden sind, mit OpenID Connect authentifizieren. Möglicherweise müssen Sie die Logs beobachten, um Benutzer zu finden, die sich nicht authentifizieren konnten. + + + Client ID + + + Client-Secret: + + + Client Scopes: + "> + + Name des Sprachparameters für die Autorisierungsanfrage + + + Endpunkt-URI für Autorisierung + + + Endpunkt-URI zum Abrufen eines Tokens + + + Endpunkt-URI für das Abrufen von Benutzerinformationen + + + Endpunkt-URI für Logout + + + Endpunkt-URI für das Widerrufen des Tokens + + + Anfragepfad-Authentifizierung verwenden: Wenn dieser Wert angekreuzt ist, wird die Request Path Authentication anstelle der standardmäßigen Password Grant verwendet. + + + Zugriffstoken von TYPO3 am Ende des Anmeldevorgangs widerrufen + + + Deaktivieren Sie die Abschwächung von CSRF-Angriffen: ACHTUNG! Dies ist ein Sicherheitsschutz, der den Rückgabewert mit dem erwarteten Wert abgleicht. Deaktivieren Sie diesen Schutz auf Ihr eigenes Risiko. + + + PKCE aktivieren: Aktiviert den PKCE-Fluss. Code-Challenge und Code-Verifier werden mitgeschickt. + + + Groß-/Kleinschreibung-unempfindliches Muster zum Abgleichen von OpenID Connect-Rollennamen ("*" passt zu jedem Zeichen, "|" zu getrennten Ausdrücken) + + + Redirect URI: Der Callback des Authentifizierungsservers wird auf diese URI verweisen. + + + OpenID Connect Identifier + + + + From c1c12a33e86be3cce00d01308dfa839228170120 Mon Sep 17 00:00:00 2001 From: Georg Ringer Date: Thu, 7 Oct 2021 08:05:53 +0200 Subject: [PATCH 022/142] [FEATURE] LinkViewHelper for felogin based on extbase If the extbased version of felogin is used, this ViewHelper can be used to retrieve the link --- Classes/ViewHelpers/OidcLinkViewHelper.php | 157 +++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 Classes/ViewHelpers/OidcLinkViewHelper.php diff --git a/Classes/ViewHelpers/OidcLinkViewHelper.php b/Classes/ViewHelpers/OidcLinkViewHelper.php new file mode 100644 index 0000000..5566110 --- /dev/null +++ b/Classes/ViewHelpers/OidcLinkViewHelper.php @@ -0,0 +1,157 @@ +debug('Post-processing markers for felogin form', ['request' => $requestId]); + $link = ''; + + $typo3Branch = class_exists(Typo3Version::class) + ? (new Typo3Version())->getBranch() + : TYPO3_branch; + if (version_compare($typo3Branch, '9.0', '<')) { + $settings = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['oidc']); + } else { + $settings = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['oidc'] ?? []; + } + + if (empty($settings['oidcClientKey']) + || empty($settings['oidcClientSecret']) + || empty($settings['oidcEndpointAuthorize']) + || empty($settings['oidcEndpointToken']) + ) { + $link = 'Invalid OpenID Connect configuration'; + } else { + if (session_id() === '') { // If no session exists, start a new one + static::getLogger()->debug('No PHP session found'); + session_start(); + } + + if (empty($_SESSION['requestId']) || $_SESSION['requestId'] !== $requestId) { + self::prepareAuthorizationUrl($settings); + $_SESSION['requestId'] = $requestId; + $_SESSION['oidc_redirect_url'] = GeneralUtility::_GP('redirect_url'); + + static::getLogger()->debug('PHP session is available', [ + 'id' => session_id(), + 'data' => $_SESSION, + ]); + } else { + static::getLogger()->debug('Reusing same authorization URL and state'); + } + + $link = $_SESSION['oidc_authorization_url']; + } + + return $link; + } + + /** + * Prepares the authorization URL and corresponding expected state (to mitigate CSRF attack) + * and stores information into the session. + * + * @param array $settings + * @return void + */ + protected static function prepareAuthorizationUrl(array $settings) + { + /** @var \Causal\Oidc\Service\OAuthService $service */ + $service = GeneralUtility::makeInstance(\Causal\Oidc\Service\OAuthService::class); + $service->setSettings($settings); + $authorizationUrl = $service->getAuthorizationUrl(); + + // Store the state + $state = $service->getState(); + + static::getLogger()->debug('Generating authorization URL', [ + 'url' => $authorizationUrl, + 'state' => $state, + ]); + + $loginUrl = GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL'); + // Sanitize the URL + $parts = parse_url($loginUrl); + $queryParts = array_filter(explode('&', $parts['query']), function ($v) { + [$k,] = explode('=', $v, 2); + + return !in_array($k, ['logintype', 'tx_oidc[code]']); + }); + $parts['query'] = implode('&', $queryParts); + $loginUrl = $parts['scheme'] . '://' . $parts['host'] . $parts['path']; + if (!empty($parts['query'])) { + $loginUrl .= '?' . $parts['query']; + } + + $_SESSION['oidc_state'] = $state; + $_SESSION['oidc_login_url'] = $loginUrl; + $_SESSION['oidc_authorization_url'] = $authorizationUrl; + } + + /** + * Returns a unique ID for the current processed request. + * + * This is supposed to be independent of the actual web server (Nginx or Apache) and + * the way PHP was built and unique enough for our use case, as opposed to using: + * + * - zend_thread_id() which requires PHP to be built with Zend Thread Safety - ZTS - support and debug mode + * - apache_getenv('UNIQUE_ID') which requires Apache as web server and mod_unique_id + * + * @return string + */ + protected static function getUniqueId() + { + $uniqueId = sprintf('%08x', abs(crc32($_SERVER['REMOTE_ADDR'] . $_SERVER['REQUEST_TIME'] . $_SERVER['REMOTE_PORT']))); + + return $uniqueId; + } + + /** + * Returns a logger. + * + * @return Logger + */ + protected static function getLogger() + { + /** @var Logger $logger */ + static $logger = null; + if ($logger === null) { + $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__); + } + + return $logger; + } +} From 2d04f401d48c03671549973e2040208dd7261e2c Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Fri, 25 Feb 2022 09:42:14 +0100 Subject: [PATCH 023/142] [TASK] Raise version to 1.1.0 --- ext_emconf.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext_emconf.php b/ext_emconf.php index 703bd95..5276a99 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -11,7 +11,7 @@ 'createDirs' => '', 'modify_tables' => '', 'clearCacheOnLoad' => 0, - 'version' => '1.1.0-dev', + 'version' => '1.1.0', 'constraints' => [ 'depends' => [ 'php' => '7.0.0-7.4.99', From 75d07508a24a486c937eafbbf9a60b4ed25b75a4 Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Fri, 25 Mar 2022 13:51:55 +0100 Subject: [PATCH 024/142] [TASK] Adapt documentation for writing to debug log --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 42ef690..c8c41a0 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ entries with level "WARNING" or above to the system log, you may add following c $GLOBALS['TYPO3_CONF_VARS']['LOG']['Causal']['Oidc']['writerConfiguration'] = [ \TYPO3\CMS\Core\Log\LogLevel::DEBUG => [ \TYPO3\CMS\Core\Log\Writer\FileWriter::class => [ - 'logFile' => 'typo3temp/logs/oidc.log' + 'logFile' => \TYPO3\CMS\Core\Core\Environment::getVarPath() . '/log/oidc.log' ], ], From a3dedd79a3af3e97bb179f3f7fd2ac77d91f9c29 Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Fri, 25 Mar 2022 13:56:33 +0100 Subject: [PATCH 025/142] [TASK] Adapt documentation for location of logs in recent versions of TYPO3 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c8c41a0..c7ceb03 100644 --- a/README.md +++ b/README.md @@ -88,12 +88,12 @@ As an administrator, what you should know is that the TYPO3 Logger forwards log log record. By default, with a vanilla TYPO3 installation, messages are written to the default log file -(`typo3temp/logs/typo3_*.log`). +(`var/log/typo3_*.log`). ### Dedicated Log File for OpenID Connect -If you want to redirect every logging information from this extension to `typo3temp/logs/oidc.log` and send log +If you want to redirect every logging information from this extension to `var/log/oidc.log` and send log entries with level "WARNING" or above to the system log, you may add following configuration to `typo3conf/AdditionalConfiguration.php`: From 2e78a77b09c73ad3255093c1b6c879e73148d70d Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Fri, 25 Mar 2022 14:03:13 +0100 Subject: [PATCH 026/142] [TASK] Minor cleanup in README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c7ceb03..4ac3170 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ page will immediately be redirected to the authorization server. After the login process, the user will be redirected: -- The OIDC Login supports the same redirect_url parameter as the felogin box +- The OIDC Login supports the same `redirect_url` parameter as the felogin box - If no parameter is set, OIDC Login will redirect the user to the page configured at `plugin.tx_oidc_login.defaultRedirectPid`. - If that configuration is not set either, the user will be redirected to '/'. @@ -51,8 +51,8 @@ https://tools.ietf.org/html/rfc7636 for details. name = , ``` -- Support for [stdWrap](https://docs.typo3.org/m/typo3/reference-typoscript/master/en-us/Functions/Stdwrap.html) in field - definition, e.g., +- Support for [stdWrap](https://docs.typo3.org/m/typo3/reference-typoscript/master/en-us/Functions/Stdwrap.html) in + field definition, e.g., ``` name = From b580e8cfb242d38a966bf2e3d61dcbea1e9b84c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Remo=20H=C3=A4usler?= Date: Mon, 16 May 2022 08:48:15 +0200 Subject: [PATCH 027/142] Make extension compatible with v11 --- Classes/Controller/LoginController.php | 24 +++++++++++++-- Classes/Service/AuthenticationService.php | 4 ++- composer.json | 2 +- ext_emconf.php | 4 +-- ext_localconf.php | 36 ++++++++++++++++------- 5 files changed, 53 insertions(+), 17 deletions(-) diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php index ea75a23..a9dbf37 100644 --- a/Classes/Controller/LoginController.php +++ b/Classes/Controller/LoginController.php @@ -14,6 +14,8 @@ namespace Causal\Oidc\Controller; +use TYPO3\CMS\Core\Http\PropagateResponseException; +use TYPO3\CMS\Core\Http\RedirectResponse; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\HttpUtility; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; @@ -51,6 +53,11 @@ public function __construct() } } + public function setContentObjectRenderer(ContentObjectRenderer $cObj) + { + $this->cObj = $cObj; + } + /** * Main entry point for the OIDC plugin. * @@ -98,13 +105,13 @@ protected function performRedirectToLogin(array $authorizationUrlOptions = []) $_SESSION['oidc_authorization_url'] = $authorizationUrl; unset($_SESSION['oidc_redirect_url']); // The redirect will be handled by this plugin - HttpUtility::redirect($authorizationUrl); + $this->redirect($authorizationUrl); } protected function performRedirectAfterLogin() { $redirectUrl = $this->determineRedirectUrl(); - HttpUtility::redirect($redirectUrl); + $this->redirect($redirectUrl); } protected function determineRedirectUrl() @@ -123,6 +130,19 @@ protected function determineRedirectUrl() return '/'; } + protected function redirect(string $redirectUrl): void + { + $typo3Branch = class_exists(\TYPO3\CMS\Core\Information\Typo3Version::class) + ? (new \TYPO3\CMS\Core\Information\Typo3Version())->getBranch() + : TYPO3_branch; + + if (version_compare($typo3Branch, '11.0', '<')) { + HttpUtility::redirect($redirectUrl); + } + + throw new PropagateResponseException(new RedirectResponse($redirectUrl)); + } + protected function generateCodeVerifier(): string { return bin2hex(random_bytes(64)); diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index ead3e7a..1dbfe5d 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -29,6 +29,7 @@ use TYPO3\CMS\Extbase\SignalSlot\Dispatcher; use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; +use TYPO3\CMS\Core\Context\Context; /** * OpenID Connect authentication service. @@ -697,7 +698,8 @@ protected function getTypoScriptSetup() $currentPage = $pageArguments->getPageId(); $frontendUser = GeneralUtility::makeInstance(FrontendUserAuthentication::class); - $localTSFE = GeneralUtility::makeInstance(TypoScriptFrontendController::class, null, $site, $routeResult->getLanguage(), $pageArguments, $frontendUser); + $context = GeneralUtility::makeInstance(Context::class); + $localTSFE = GeneralUtility::makeInstance(TypoScriptFrontendController::class, $context, $site, $routeResult->getLanguage(), $pageArguments, $frontendUser); /** @var TemplateService $templateService */ $templateService = GeneralUtility::makeInstance(TemplateService::class, null, null, $localTSFE); diff --git a/composer.json b/composer.json index 55a9c89..b890d40 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "license": "GPL-2.0-or-later", "require": { "php": ">= 7.0.0, <= 7.4.99", - "typo3/cms-core": "^8.7 || ^9.5 || ^10.4", + "typo3/cms-core": "^8.7 || ^9.5 || ^10.4 || ^11.5", "league/oauth2-client": "^2.0" }, "autoload": { diff --git a/ext_emconf.php b/ext_emconf.php index 5276a99..1c28934 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -11,11 +11,11 @@ 'createDirs' => '', 'modify_tables' => '', 'clearCacheOnLoad' => 0, - 'version' => '1.1.0', + 'version' => '1.2.0-dev', 'constraints' => [ 'depends' => [ 'php' => '7.0.0-7.4.99', - 'typo3' => '8.7.0-10.4.99', + 'typo3' => '8.7.0-11.5.99', ], 'conflicts' => [], 'suggests' => [], diff --git a/ext_localconf.php b/ext_localconf.php index f560d36..ec94b6c 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -48,17 +48,31 @@ class_alias(\TYPO3\CMS\Core\Authentication\AuthenticationService::class, \TYPO3\ ] ); - \TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin( - 'Causal.' . $_EXTKEY, - 'Pi1', - [ - 'Authentication' => 'connect', - ], - // non-cacheable actions - [ - 'Authentication' => 'connect' - ] - ); + if (version_compare($typo3Branch, '10.0', '<')) { + \TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin( + 'Causal.' . $_EXTKEY, + 'Pi1', + [ + 'Authentication' => 'connect', + ], + // non-cacheable actions + [ + 'Authentication' => 'connect' + ] + ); + } else { + \TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin( + $_EXTKEY, + 'Pi1', + [ + \Causal\Oidc\Controller\AuthenticationController::class => 'connect', + ], + // non-cacheable actions + [ + \Causal\Oidc\Controller\AuthenticationController::class => 'connect' + ] + ); + } if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('felogin')) { $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['postProcContent'][$_EXTKEY] = \Causal\Oidc\Hooks\FeloginHook::class . '->postProcContent'; From 682363fbbbb66485f50fc5b6f9a35965d67623c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Remo=20H=C3=A4usler?= Date: Mon, 16 May 2022 10:43:25 +0200 Subject: [PATCH 028/142] Add authentication get user event --- Classes/Event/AuthenticationGetUserEvent.php | 48 ++++++++++++++++++++ Classes/Service/AuthenticationService.php | 14 ++++++ 2 files changed, 62 insertions(+) create mode 100644 Classes/Event/AuthenticationGetUserEvent.php diff --git a/Classes/Event/AuthenticationGetUserEvent.php b/Classes/Event/AuthenticationGetUserEvent.php new file mode 100644 index 0000000..b7c32cf --- /dev/null +++ b/Classes/Event/AuthenticationGetUserEvent.php @@ -0,0 +1,48 @@ +user = $user; + } + + /** + * Array with user if authentication was successfull or false on failure. + * @return array|bool + */ + public function getUser() + { + return $this->user; + } + + /** + * @param array|bool $user + */ + public function setUser($user): void + { + $this->user = $user; + } +} diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 1dbfe5d..6535f3d 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -14,7 +14,9 @@ namespace Causal\Oidc\Service; +use Causal\Oidc\Event\AuthenticationGetUserEvent; use League\OAuth2\Client\Token\AccessToken; +use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction; @@ -123,6 +125,18 @@ public function getUser() // provided by the authentication server $dispatcher = GeneralUtility::makeInstance(ObjectManager::class)->get(Dispatcher::class); $dispatcher->dispatch(__CLASS__, 'getUser', ['user' => $user]); + + $typo3Branch = class_exists(\TYPO3\CMS\Core\Information\Typo3Version::class) + ? (new \TYPO3\CMS\Core\Information\Typo3Version())->getBranch() + : TYPO3_branch; + + if (version_compare($typo3Branch, '10.2', '>=')) { + $event = new AuthenticationGetUserEvent($user); + $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); + $eventDispatcher->dispatch($event); + $user = $event->getUser(); + } + if (is_array($user)) { unset($user['accessToken']); } From e8228aeaba95605ae600fd3a3873263c6ff1149a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Remo=20H=C3=A4usler?= Date: Mon, 16 May 2022 10:44:57 +0200 Subject: [PATCH 029/142] Make oauth provider configurable --- .../Factory/GenericOAuthProviderFactory.php | 35 +++++++++++++++++++ .../Factory/OAuthProviderFactoryInterface.php | 22 ++++++++++++ Classes/Service/OAuthService.php | 28 +++++++-------- Resources/Private/Language/locallang_db.xlf | 3 ++ ext_conf_template.txt | 3 ++ 5 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 Classes/Factory/GenericOAuthProviderFactory.php create mode 100644 Classes/Factory/OAuthProviderFactoryInterface.php diff --git a/Classes/Factory/GenericOAuthProviderFactory.php b/Classes/Factory/GenericOAuthProviderFactory.php new file mode 100644 index 0000000..4dce01a --- /dev/null +++ b/Classes/Factory/GenericOAuthProviderFactory.php @@ -0,0 +1,35 @@ + $settings['oidcClientKey'], + 'clientSecret' => $settings['oidcClientSecret'], + 'redirectUri' => $settings['oidcRedirectUri'], + 'urlAuthorize' => $settings['oidcEndpointAuthorize'], + 'urlAccessToken' => $settings['oidcEndpointToken'], + 'urlResourceOwnerDetails' => $settings['oidcEndpointUserInfo'], + 'scopes' => GeneralUtility::trimExplode(',', $settings['oidcClientScopes'], true), + ]); + } +} diff --git a/Classes/Factory/OAuthProviderFactoryInterface.php b/Classes/Factory/OAuthProviderFactoryInterface.php new file mode 100644 index 0000000..fb18e92 --- /dev/null +++ b/Classes/Factory/OAuthProviderFactoryInterface.php @@ -0,0 +1,22 @@ +provider === null) { - $redirectUri = $this->settings['oidcRedirectUri'] ?: GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST') . '/typo3conf/ext/oidc/Resources/Public/callback.php'; - - $this->provider = new \League\OAuth2\Client\Provider\GenericProvider([ - 'clientId' => $this->settings['oidcClientKey'], - 'clientSecret' => $this->settings['oidcClientSecret'], - 'redirectUri' => $redirectUri, - 'urlAuthorize' => $this->settings['oidcEndpointAuthorize'], - 'urlAccessToken' => $this->settings['oidcEndpointToken'], - 'urlResourceOwnerDetails' => $this->settings['oidcEndpointUserInfo'], - 'scopes' => GeneralUtility::trimExplode(',', $this->settings['oidcClientScopes'], true), - ]); + $factoryClass = $this->settings['oauthProviderFactory'] ?: GenericOAuthProviderFactory::class; + if (!is_a($factoryClass, OAuthProviderFactoryInterface::class, true)) { + throw new \RuntimeException('OAuth provider factory class must implement the OAuthProviderFactoryInterface', 1652689564769); + } + + $settings = $this->settings; + $settings['oidcRedirectUri'] = $this->settings['oidcRedirectUri'] ?: GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST') . '/typo3conf/ext/oidc/Resources/Public/callback.php'; + + /** @var OAuthProviderFactoryInterface $factory */ + $factory = GeneralUtility::makeInstance($factoryClass); + $this->provider = $factory->create($settings); } return $this->provider; diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index 38c0be6..9e8755a 100755 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -67,6 +67,9 @@ OpenID Connect Identifier + + OAuth Provider Factory: Fully qulified class name (empty for generic provider). + diff --git a/ext_conf_template.txt b/ext_conf_template.txt index bd97d71..dc673f8 100644 --- a/ext_conf_template.txt +++ b/ext_conf_template.txt @@ -57,3 +57,6 @@ oidcRevokeAccessTokenAfterLogin = 0 # cat=advanced/enable/2; type=boolean; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcDisableCSRFProtection oidcDisableCSRFProtection = 0 + +# cat=advanced//3; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oauthProviderFactory +oauthProviderFactory = From be4f8450e0f782124a5252b49586c86f5829a7cc Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Thu, 14 Jul 2022 09:20:48 +0200 Subject: [PATCH 030/142] [BUGFIX] Ensure SSL is properly detected when using a reverse proxy --- Classes/Service/OAuthService.php | 14 ++++++++++++-- Resources/Public/callback.php | 7 +++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Classes/Service/OAuthService.php b/Classes/Service/OAuthService.php index e87b190..6655aa4 100644 --- a/Classes/Service/OAuthService.php +++ b/Classes/Service/OAuthService.php @@ -122,7 +122,12 @@ public function getAccessToken($codeOrUsername, $password = null, $codeVerifier */ public function getAccessTokenWithRequestPathAuthentication($username, $password) { - $redirectUri = $this->settings['oidcRedirectUri'] ?: GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST') . '/typo3conf/ext/oidc/Resources/Public/callback.php'; + $redirectUri = $this->settings['oidcRedirectUri']; + if (empty($redirectUri)) { + $redirectUri = GeneralUtility::getIndpEnv('TYPO3_SSL') ? 'https://' : 'http://'; + $redirectUri .= GeneralUtility::getIndpEnv('HTTP_HOST'); + $redirectUri .= '/typo3conf/ext/oidc/Resources/Public/callback.php'; + } $url = $this->settings['oidcEndpointAuthorize'] . '?'. http_build_query([ 'response_type' => 'code', 'client_id' => $this->settings['oidcClientKey'], @@ -210,7 +215,12 @@ public function revokeToken(AccessToken $token) protected function getProvider() { if ($this->provider === null) { - $redirectUri = $this->settings['oidcRedirectUri'] ?: GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST') . '/typo3conf/ext/oidc/Resources/Public/callback.php'; + $redirectUri = $this->settings['oidcRedirectUri']; + if (empty($redirectUri)) { + $redirectUri = GeneralUtility::getIndpEnv('TYPO3_SSL') ? 'https://' : 'http://'; + $redirectUri .= GeneralUtility::getIndpEnv('HTTP_HOST'); + $redirectUri .= '/typo3conf/ext/oidc/Resources/Public/callback.php'; + } $this->provider = new \League\OAuth2\Client\Provider\GenericProvider([ 'clientId' => $this->settings['oidcClientKey'], diff --git a/Resources/Public/callback.php b/Resources/Public/callback.php index eacb8d6..6c9bfd9 100644 --- a/Resources/Public/callback.php +++ b/Resources/Public/callback.php @@ -14,8 +14,11 @@ // see https://github.com/thephpleague/oauth2-client if (!(empty($_GET['state']) || empty($_GET['code']))) { - $schema = (@$_SERVER['HTTPS'] === 'on') ? 'https://' : 'http://'; - $currentUrl = $schema . $_SERVER['SERVER_NAME']; + $schema = $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''; + if (empty($schema)) { + $schema = (@$_SERVER['HTTPS'] === 'on') ? 'https' : 'http'; + } + $currentUrl = $schema . '://' . $_SERVER['SERVER_NAME']; if ($_SERVER['SERVER_PORT'] !== '80' && $_SERVER['SERVER_PORT'] !== '443') { $currentUrl .= ':' . $_SERVER['SERVER_PORT']; } From 0bad9b7416ec36eeaca3abf34602d39b8588ad0b Mon Sep 17 00:00:00 2001 From: der-peer Date: Thu, 14 Jul 2022 10:11:30 +0200 Subject: [PATCH 031/142] [BUGFIX] Fix URL mismatch when using a reverse proxy Resolves: #44 --- Resources/Public/callback.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/Resources/Public/callback.php b/Resources/Public/callback.php index 6c9bfd9..d274205 100644 --- a/Resources/Public/callback.php +++ b/Resources/Public/callback.php @@ -19,9 +19,6 @@ $schema = (@$_SERVER['HTTPS'] === 'on') ? 'https' : 'http'; } $currentUrl = $schema . '://' . $_SERVER['SERVER_NAME']; - if ($_SERVER['SERVER_PORT'] !== '80' && $_SERVER['SERVER_PORT'] !== '443') { - $currentUrl .= ':' . $_SERVER['SERVER_PORT']; - } $currentUrl .= $_SERVER['REQUEST_URI']; if (($pos = strpos($currentUrl, 'typo3conf/ext/oidc/Resources/Public/callback.php')) !== false) { From 3bb08a5201c8d9dc67c21b5a3aba495c8542ac9d Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Thu, 14 Jul 2022 10:24:51 +0200 Subject: [PATCH 032/142] [TASK] Minor code cleanup --- Classes/Controller/LoginController.php | 6 +++--- Classes/Service/AuthenticationService.php | 2 +- Classes/Service/OAuthService.php | 15 ++++++++------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php index ea75a23..658e2b7 100644 --- a/Classes/Controller/LoginController.php +++ b/Classes/Controller/LoginController.php @@ -90,7 +90,7 @@ protected function performRedirectToLogin(array $authorizationUrlOptions = []) $options = $this->addCodeChallengeToOptions($codeChallenge, $authorizationUrlOptions); $_SESSION['oidc_code_verifier'] = $codeVerifier; } - $authorizationUrl = $service->getAuthorizationUrl($options?: []); + $authorizationUrl = $service->getAuthorizationUrl($options ?: []); $state = $service->getState(); $_SESSION['oidc_state'] = $state; @@ -109,13 +109,13 @@ protected function performRedirectAfterLogin() protected function determineRedirectUrl() { - if (! empty(GeneralUtility::_GP('redirect_url'))) { + if (!empty(GeneralUtility::_GP('redirect_url'))) { return GeneralUtility::_GP('redirect_url'); } if (isset($this->pluginConfiguration['defaultRedirectPid'])) { $defaultRedirectPid = $this->pluginConfiguration['defaultRedirectPid']; - if ((int) $defaultRedirectPid > 0) { + if ((int)$defaultRedirectPid > 0) { return $this->cObj->typoLink_URL(['parameter' => $defaultRedirectPid]); } } diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index ead3e7a..7400a2b 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -232,7 +232,7 @@ protected function getUserFromAccessToken(OAuthService $service, AccessToken $ac $service->revokeToken($accessToken); throw new \RuntimeException( 'Resource owner does not have a sub part: ' . json_encode($resourceOwner) - . '. Your access token has been revoked. Please try again.', + . '. Your access token has been revoked. Please try again.', 1490086626 ); } diff --git a/Classes/Service/OAuthService.php b/Classes/Service/OAuthService.php index 6655aa4..f8f6d08 100644 --- a/Classes/Service/OAuthService.php +++ b/Classes/Service/OAuthService.php @@ -56,7 +56,7 @@ public function setSettings(array $settings) */ public function getAuthorizationUrl(array $options = []) { - if (! empty($this->settings['oidcAuthorizeLanguageParameter'])) { + if (!empty($this->settings['oidcAuthorizeLanguageParameter'])) { $languageOption = $this->settings['oidcAuthorizeLanguageParameter']; if (isset($GLOBALS['TSFE']->lang)) { @@ -107,6 +107,7 @@ public function getAccessToken($codeOrUsername, $password = null, $codeVerifier 'scope' => implode(',', $this->getProvider()->getDefaultScopes()), ]); } + return $accessToken; } @@ -128,12 +129,12 @@ public function getAccessTokenWithRequestPathAuthentication($username, $password $redirectUri .= GeneralUtility::getIndpEnv('HTTP_HOST'); $redirectUri .= '/typo3conf/ext/oidc/Resources/Public/callback.php'; } - $url = $this->settings['oidcEndpointAuthorize'] . '?'. http_build_query([ - 'response_type' => 'code', - 'client_id' => $this->settings['oidcClientKey'], - 'scope' => $this->settings['oidcClientScopes'], - 'redirect_uri' => $redirectUri, - ]); + $url = $this->settings['oidcEndpointAuthorize'] . '?' . http_build_query([ + 'response_type' => 'code', + 'client_id' => $this->settings['oidcClientKey'], + 'scope' => $this->settings['oidcClientScopes'], + 'redirect_uri' => $redirectUri, + ]); $ch = curl_init(); curl_setopt($ch, CURLOPT_HTTPHEADER, [ From 287c82524f1da25ce3d2a1a0edc4263a1e9a0918 Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Thu, 14 Jul 2022 09:20:48 +0200 Subject: [PATCH 033/142] [BUGFIX] Ensure SSL is properly detected when using a reverse proxy --- Classes/Service/OAuthService.php | 15 +++++++++++++-- Resources/Public/callback.php | 7 +++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Classes/Service/OAuthService.php b/Classes/Service/OAuthService.php index 0d2c0d5..ecfdaf4 100644 --- a/Classes/Service/OAuthService.php +++ b/Classes/Service/OAuthService.php @@ -122,7 +122,12 @@ public function getAccessToken($codeOrUsername, $password = null, $codeVerifier */ public function getAccessTokenWithRequestPathAuthentication($username, $password) { - $redirectUri = $this->settings['oidcRedirectUri'] ?: GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST') . '/typo3conf/ext/oidc/Resources/Public/callback.php'; + $redirectUri = $this->settings['oidcRedirectUri']; + if (empty($redirectUri)) { + $redirectUri = GeneralUtility::getIndpEnv('TYPO3_SSL') ? 'https://' : 'http://'; + $redirectUri .= GeneralUtility::getIndpEnv('HTTP_HOST'); + $redirectUri .= '/typo3conf/ext/oidc/Resources/Public/callback.php'; + } $url = $this->settings['oidcEndpointAuthorize'] . '?'. http_build_query([ 'response_type' => 'code', 'client_id' => $this->settings['oidcClientKey'], @@ -216,7 +221,13 @@ protected function getProvider() } $settings = $this->settings; - $settings['oidcRedirectUri'] = $this->settings['oidcRedirectUri'] ?: GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST') . '/typo3conf/ext/oidc/Resources/Public/callback.php'; + $settings['oidcRedirectUri'] = $this->settings['oidcRedirectUri']; + if (empty($settings['oidcRedirectUri'])) { + $redirectUri = GeneralUtility::getIndpEnv('TYPO3_SSL') ? 'https://' : 'http://'; + $redirectUri .= GeneralUtility::getIndpEnv('HTTP_HOST'); + $redirectUri .= '/typo3conf/ext/oidc/Resources/Public/callback.php'; + $settings['oidcRedirectUri'] = $redirectUri; + } /** @var OAuthProviderFactoryInterface $factory */ $factory = GeneralUtility::makeInstance($factoryClass); diff --git a/Resources/Public/callback.php b/Resources/Public/callback.php index eacb8d6..6c9bfd9 100644 --- a/Resources/Public/callback.php +++ b/Resources/Public/callback.php @@ -14,8 +14,11 @@ // see https://github.com/thephpleague/oauth2-client if (!(empty($_GET['state']) || empty($_GET['code']))) { - $schema = (@$_SERVER['HTTPS'] === 'on') ? 'https://' : 'http://'; - $currentUrl = $schema . $_SERVER['SERVER_NAME']; + $schema = $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''; + if (empty($schema)) { + $schema = (@$_SERVER['HTTPS'] === 'on') ? 'https' : 'http'; + } + $currentUrl = $schema . '://' . $_SERVER['SERVER_NAME']; if ($_SERVER['SERVER_PORT'] !== '80' && $_SERVER['SERVER_PORT'] !== '443') { $currentUrl .= ':' . $_SERVER['SERVER_PORT']; } From 81be428e219db9ec8f8f5b6ffc515e9cf9d7c7c0 Mon Sep 17 00:00:00 2001 From: der-peer Date: Thu, 14 Jul 2022 10:11:30 +0200 Subject: [PATCH 034/142] [BUGFIX] Fix URL mismatch when using a reverse proxy Resolves: #44 --- Resources/Public/callback.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/Resources/Public/callback.php b/Resources/Public/callback.php index 6c9bfd9..d274205 100644 --- a/Resources/Public/callback.php +++ b/Resources/Public/callback.php @@ -19,9 +19,6 @@ $schema = (@$_SERVER['HTTPS'] === 'on') ? 'https' : 'http'; } $currentUrl = $schema . '://' . $_SERVER['SERVER_NAME']; - if ($_SERVER['SERVER_PORT'] !== '80' && $_SERVER['SERVER_PORT'] !== '443') { - $currentUrl .= ':' . $_SERVER['SERVER_PORT']; - } $currentUrl .= $_SERVER['REQUEST_URI']; if (($pos = strpos($currentUrl, 'typo3conf/ext/oidc/Resources/Public/callback.php')) !== false) { From df4c7893fa36a100d05c1e57e58c11e083279a52 Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Thu, 14 Jul 2022 10:24:51 +0200 Subject: [PATCH 035/142] [TASK] Minor code cleanup --- Classes/Controller/LoginController.php | 6 +++--- Classes/Service/AuthenticationService.php | 2 +- Classes/Service/OAuthService.php | 15 ++++++++------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php index a9dbf37..62f19fc 100644 --- a/Classes/Controller/LoginController.php +++ b/Classes/Controller/LoginController.php @@ -97,7 +97,7 @@ protected function performRedirectToLogin(array $authorizationUrlOptions = []) $options = $this->addCodeChallengeToOptions($codeChallenge, $authorizationUrlOptions); $_SESSION['oidc_code_verifier'] = $codeVerifier; } - $authorizationUrl = $service->getAuthorizationUrl($options?: []); + $authorizationUrl = $service->getAuthorizationUrl($options ?: []); $state = $service->getState(); $_SESSION['oidc_state'] = $state; @@ -116,13 +116,13 @@ protected function performRedirectAfterLogin() protected function determineRedirectUrl() { - if (! empty(GeneralUtility::_GP('redirect_url'))) { + if (!empty(GeneralUtility::_GP('redirect_url'))) { return GeneralUtility::_GP('redirect_url'); } if (isset($this->pluginConfiguration['defaultRedirectPid'])) { $defaultRedirectPid = $this->pluginConfiguration['defaultRedirectPid']; - if ((int) $defaultRedirectPid > 0) { + if ((int)$defaultRedirectPid > 0) { return $this->cObj->typoLink_URL(['parameter' => $defaultRedirectPid]); } } diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 6535f3d..162afcc 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -247,7 +247,7 @@ protected function getUserFromAccessToken(OAuthService $service, AccessToken $ac $service->revokeToken($accessToken); throw new \RuntimeException( 'Resource owner does not have a sub part: ' . json_encode($resourceOwner) - . '. Your access token has been revoked. Please try again.', + . '. Your access token has been revoked. Please try again.', 1490086626 ); } diff --git a/Classes/Service/OAuthService.php b/Classes/Service/OAuthService.php index ecfdaf4..3c47e0a 100644 --- a/Classes/Service/OAuthService.php +++ b/Classes/Service/OAuthService.php @@ -56,7 +56,7 @@ public function setSettings(array $settings) */ public function getAuthorizationUrl(array $options = []) { - if (! empty($this->settings['oidcAuthorizeLanguageParameter'])) { + if (!empty($this->settings['oidcAuthorizeLanguageParameter'])) { $languageOption = $this->settings['oidcAuthorizeLanguageParameter']; if (isset($GLOBALS['TSFE']->lang)) { @@ -107,6 +107,7 @@ public function getAccessToken($codeOrUsername, $password = null, $codeVerifier 'scope' => implode(',', $this->getProvider()->getDefaultScopes()), ]); } + return $accessToken; } @@ -128,12 +129,12 @@ public function getAccessTokenWithRequestPathAuthentication($username, $password $redirectUri .= GeneralUtility::getIndpEnv('HTTP_HOST'); $redirectUri .= '/typo3conf/ext/oidc/Resources/Public/callback.php'; } - $url = $this->settings['oidcEndpointAuthorize'] . '?'. http_build_query([ - 'response_type' => 'code', - 'client_id' => $this->settings['oidcClientKey'], - 'scope' => $this->settings['oidcClientScopes'], - 'redirect_uri' => $redirectUri, - ]); + $url = $this->settings['oidcEndpointAuthorize'] . '?' . http_build_query([ + 'response_type' => 'code', + 'client_id' => $this->settings['oidcClientKey'], + 'scope' => $this->settings['oidcClientScopes'], + 'redirect_uri' => $redirectUri, + ]); $ch = curl_init(); curl_setopt($ch, CURLOPT_HTTPHEADER, [ From 3389970897a0c75de2564f1b81a4025f494e57d0 Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Thu, 14 Jul 2022 10:35:35 +0200 Subject: [PATCH 036/142] [TASK] Streamline usage of locallang_db.xlf --- Resources/Private/Language/locallang_db.xlf | 8 +++++++- ext_conf_template.txt | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index 9e8755a..f7d24d0 100755 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -58,6 +58,12 @@ Enable PKCE: Enable PKCE flow. Code challenge and code verifier will be sent along. + + Storage Pid: The Storage Pid of the Page, where the fe_users should be stored + + + Default user group(s) (comma-separated list of UIDs) + Case-insensitive pattern to match OpenID Connect role names ("*" matches every character, "|" to separate expressions) @@ -68,7 +74,7 @@ OpenID Connect Identifier - OAuth Provider Factory: Fully qulified class name (empty for generic provider). + OAuth Provider Factory: Fully qualified class name (empty for generic provider). diff --git a/ext_conf_template.txt b/ext_conf_template.txt index dc673f8..2406c30 100644 --- a/ext_conf_template.txt +++ b/ext_conf_template.txt @@ -13,10 +13,10 @@ frontendUserMustExistLocally = 0 # cat=basic/enable/5; type=boolean; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.enableCodeVerifier enableCodeVerifier = 0 -# cat=basic//1; type=int; label=Storage Pid: The Storage Pid of the Page, where the fe_users should be stored +# cat=basic//1; type=int; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.usersStoragePid usersStoragePid = -# cat=basic//2; type=string; label=Default user group(s) (comma-separated list of UIDs) +# cat=basic//2; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.usersDefaultGroup usersDefaultGroup = # cat=basic//2a; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcRedirectUri From 7541ca2fc551274176097b98693532b7dd5fa80d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roman=20B=C3=BCchler?= Date: Thu, 14 Jul 2022 10:54:32 +0200 Subject: [PATCH 037/142] [FEATURE] Extend user storage As userStoragePid, a comma-separated list can now be provided. The first item is used to store new users. Resolves: #79 --- Classes/Service/AuthenticationService.php | 55 +++++++++++++-------- Resources/Private/Language/locallang_db.xlf | 2 +- 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 162afcc..8242ad5 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -19,6 +19,7 @@ use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Database\Query\QueryBuilder; use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction; use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction; use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction; @@ -301,7 +302,10 @@ protected function convertResourceOwner(array $info) ->select('*') ->from($userTable) ->where( - $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter((int)$this->config['usersStoragePid'], \PDO::PARAM_INT)), + $queryBuilder->expr()->in('pid', $queryBuilder->createNamedParameter( + GeneralUtility::intExplode(',', $this->config['usersStoragePid']), + \TYPO3\CMS\Core\Database\Connection::PARAM_INT_ARRAY + )), $queryBuilder->expr()->eq('tx_oidc', $queryBuilder->createNamedParameter($info['sub'], \PDO::PARAM_STR)) ) ->execute() @@ -355,7 +359,7 @@ protected function convertResourceOwner(array $info) ); $newUserGroups = []; - $defaultUserGroups = GeneralUtility::intExplode(',', $this->config['usersDefaultGroup'], true); + $defaultUserGroups = GeneralUtility::intExplode(',', $this->config['usersDefaultGroup']); if (!empty($row)) { $currentUserGroups = GeneralUtility::intExplode(',', $row['usergroup'], true); @@ -451,7 +455,7 @@ protected function convertResourceOwner(array $info) } static::getLogger()->info('New user detected, creating a TYPO3 user'); $data = array_merge($data, [ - 'pid' => $this->config['usersStoragePid'], + 'pid' => GeneralUtility::intExplode(',', $this->config['usersStoragePid'], true)[0], 'usergroup' => implode(',', $newUserGroups), 'crdate' => $GLOBALS['EXEC_TIME'], 'tx_oidc' => $info['sub'], @@ -462,15 +466,7 @@ protected function convertResourceOwner(array $info) ); $userUid = $tableConnection->lastInsertId(); // Retrieve the created user from database to get all columns - $user = $tableConnection - ->select( - ['*'], - $userTable, - [ - 'uid' => $userUid, - ] - ) - ->fetch(); + $user = $this->getUserByUidAndTable((int)$userUid, $userTable); } static::getLogger()->debug('Authentication user record processed', $user); @@ -497,15 +493,7 @@ protected function convertResourceOwner(array $info) } if ($reloadUserRecord) { - $user = $tableConnection - ->select( - ['*'], - $userTable, - [ - 'uid' => (int)$user['uid'], - ] - ) - ->fetch(); + $user = $this->getUserByUidAndTable((int)$user['uid'], $userTable); static::getLogger()->debug('User record reloaded', $user); } @@ -515,6 +503,31 @@ protected function convertResourceOwner(array $info) return $user; } + protected function getUserByUidAndTable(int $uid, string $table): array + { + $user = []; + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); + $queryBuilder->getRestrictions()->removeAll(); + $queryResult = $queryBuilder + ->select('*') + ->from($table) + ->where($queryBuilder->expr()->eq( + 'uid', + $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)) + ) + ->execute(); + if ($queryResult instanceof \Doctrine\DBAL\ForwardCompatibility\Result) { + $user = $queryResult->fetchAssociative(); + } + if ($user === [] && $queryResult instanceof \Doctrine\DBAL\Driver\Statement) { + $user = $queryResult->fetch(); + } + if (!is_array($user) || $user === []) { + throw new \LogicException('The user record could not be obtained', 1643452557); + } + return $user; + } + /** * Merges info from OIDC to TYPO3 using a mapping configuration. * diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index f7d24d0..2638e73 100755 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -59,7 +59,7 @@ Enable PKCE: Enable PKCE flow. Code challenge and code verifier will be sent along. - Storage Pid: The Storage Pid of the Page, where the fe_users should be stored + Storage Pid: Comma-separated list of uid's from pages where fe_users are located. The first uid is used to store new users. Default user group(s) (comma-separated list of UIDs) From 9517cf5ca8125b7baef2ab9d527b72724f38c43b Mon Sep 17 00:00:00 2001 From: Christian Weiske Date: Thu, 14 Jul 2022 11:03:01 +0200 Subject: [PATCH 038/142] [BUGFIX] Fix callback redirect for nginx catch-all host name When nginx is running in a docker container, the server_name is often "_" [1]: > In catch-all server examples the strange name "_" can be seen: > server { > listen 80 default_server; > server_name _; > return 444; > } > There is nothing special about this name, it is just one of a myriad > of invalid domain names which never intersect with any real name. Since callback.php uses $_SERVER['SERVER_NAME'] and thus redirects to http://_/?type=1489657462&state=... This patch detects that special host name and falls back to using the HTTP "Host" header instead. When a non-standard port is used, the Host header also contains the port number[2], so no further port detection is needed. [1] https://nginx.org/en/docs/http/server_names.html [2] https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.23 --- Resources/Public/callback.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Resources/Public/callback.php b/Resources/Public/callback.php index d274205..7d1ac8a 100644 --- a/Resources/Public/callback.php +++ b/Resources/Public/callback.php @@ -18,7 +18,13 @@ if (empty($schema)) { $schema = (@$_SERVER['HTTPS'] === 'on') ? 'https' : 'http'; } - $currentUrl = $schema . '://' . $_SERVER['SERVER_NAME']; + if ($_SERVER['SERVER_NAME'] !== '_') { + $currentUrl = $schema . '://' . $_SERVER['SERVER_NAME']; + } else { + // Nginx catch all server name + // Rely on HTTP Host header, which contains non-standard ports as well + $currentUrl = $schema . $_SERVER['HTTP_HOST']; + } $currentUrl .= $_SERVER['REQUEST_URI']; if (($pos = strpos($currentUrl, 'typo3conf/ext/oidc/Resources/Public/callback.php')) !== false) { From 64a553fe82f99e07c6f0596451986581a6b9b506 Mon Sep 17 00:00:00 2001 From: Florian Krueger Date: Thu, 14 Jul 2022 11:31:10 +0200 Subject: [PATCH 039/142] [TASK] Add support for ports at the login url --- Classes/Hooks/FeloginHook.php | 6 +++++- Classes/ViewHelpers/OidcLinkViewHelper.php | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Classes/Hooks/FeloginHook.php b/Classes/Hooks/FeloginHook.php index c8454ed..34478f9 100644 --- a/Classes/Hooks/FeloginHook.php +++ b/Classes/Hooks/FeloginHook.php @@ -110,7 +110,11 @@ protected function prepareAuthorizationUrl(array $settings) return !in_array($k, ['logintype', 'tx_oidc[code]']); }); $parts['query'] = implode('&', $queryParts); - $loginUrl = $parts['scheme'] . '://' . $parts['host'] . $parts['path']; + $loginUrl = $parts['scheme'] . '://' . $parts['host']; + if (!empty($parts['port']) && !in_array((int)$parts['port'], [80, 443], true)) { + $loginUrl .= ':' . $parts['port']; + } + $loginUrl .= $parts['path']; if (!empty($parts['query'])) { $loginUrl .= '?' . $parts['query']; } diff --git a/Classes/ViewHelpers/OidcLinkViewHelper.php b/Classes/ViewHelpers/OidcLinkViewHelper.php index 5566110..53efb37 100644 --- a/Classes/ViewHelpers/OidcLinkViewHelper.php +++ b/Classes/ViewHelpers/OidcLinkViewHelper.php @@ -111,7 +111,11 @@ protected static function prepareAuthorizationUrl(array $settings) return !in_array($k, ['logintype', 'tx_oidc[code]']); }); $parts['query'] = implode('&', $queryParts); - $loginUrl = $parts['scheme'] . '://' . $parts['host'] . $parts['path']; + $loginUrl = $parts['scheme'] . '://' . $parts['host']; + if (!empty($parts['port']) && !in_array((int)$parts['port'], [80, 443], true)) { + $loginUrl .= ':' . $parts['port']; + } + $loginUrl .= $parts['path']; if (!empty($parts['query'])) { $loginUrl .= '?' . $parts['query']; } From 6ce392def4457df33e5b2239cf1f81301925d54e Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Fri, 25 Nov 2022 16:58:40 +0100 Subject: [PATCH 040/142] [TASK] Mark extension as compatible with PHP 8.x Possible problems/PHP warnings will be tackled with as they pop up. Related: #84 --- composer.json | 2 +- ext_emconf.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index b890d40..ed6e00c 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ ], "license": "GPL-2.0-or-later", "require": { - "php": ">= 7.0.0, <= 7.4.99", + "php": ">= 7.0.0, <= 8.1.99", "typo3/cms-core": "^8.7 || ^9.5 || ^10.4 || ^11.5", "league/oauth2-client": "^2.0" }, diff --git a/ext_emconf.php b/ext_emconf.php index 1c28934..8068659 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -14,7 +14,7 @@ 'version' => '1.2.0-dev', 'constraints' => [ 'depends' => [ - 'php' => '7.0.0-7.4.99', + 'php' => '7.0.0-8.1.99', 'typo3' => '8.7.0-11.5.99', ], 'conflicts' => [], From fc620bd089e0f6772384c3f0c255ddd59987cfe2 Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Fri, 25 Nov 2022 17:04:24 +0100 Subject: [PATCH 041/142] [TASK] Prevent obvious PHP 8 bugs when extension is unconfigured Related: #84 --- Configuration/TCA/Overrides/fe_users.php | 2 +- ext_localconf.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Configuration/TCA/Overrides/fe_users.php b/Configuration/TCA/Overrides/fe_users.php index e404d06..39c4259 100644 --- a/Configuration/TCA/Overrides/fe_users.php +++ b/Configuration/TCA/Overrides/fe_users.php @@ -17,7 +17,7 @@ 'config' => [ 'type' => 'input', 'size' => 30, - 'readOnly' => (bool)$settings['frontendUserMustExistLocally'] ? 0 : 1, + 'readOnly' => (bool)($settings['frontendUserMustExistLocally'] ?? '') ? 0 : 1, ] ], ]; diff --git a/ext_localconf.php b/ext_localconf.php index ec94b6c..45b10a6 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -20,7 +20,7 @@ class_alias(\TYPO3\CMS\Core\Authentication\AuthenticationService::class, \TYPO3\ // Service configuration $subTypesArr = []; $subTypes = ''; - if ((bool)$settings['enableFrontendAuthentication']) { + if ((bool)($settings['enableFrontendAuthentication'] ?? '')) { $subTypesArr[] = 'getUserFE'; $subTypesArr[] = 'authUserFE'; $subTypesArr[] = 'getGroupsFE'; From 42711b523c92bd5105c0638919de1b7352baf70b Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Mon, 28 Nov 2022 17:34:45 +0100 Subject: [PATCH 042/142] [TASK] Use proper file extension for TypoScript configuration files --- .../TypoScript/felogin/{constants.txt => constants.typoscript} | 0 .../TypoScript/felogin/{setup.txt => setup.typoscript} | 2 +- Configuration/TypoScript/{setup.txt => setup.typoscript} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename Configuration/TypoScript/felogin/{constants.txt => constants.typoscript} (100%) rename Configuration/TypoScript/felogin/{setup.txt => setup.typoscript} (83%) rename Configuration/TypoScript/{setup.txt => setup.typoscript} (100%) diff --git a/Configuration/TypoScript/felogin/constants.txt b/Configuration/TypoScript/felogin/constants.typoscript similarity index 100% rename from Configuration/TypoScript/felogin/constants.txt rename to Configuration/TypoScript/felogin/constants.typoscript diff --git a/Configuration/TypoScript/felogin/setup.txt b/Configuration/TypoScript/felogin/setup.typoscript similarity index 83% rename from Configuration/TypoScript/felogin/setup.txt rename to Configuration/TypoScript/felogin/setup.typoscript index d916934..7b07594 100644 --- a/Configuration/TypoScript/felogin/setup.txt +++ b/Configuration/TypoScript/felogin/setup.typoscript @@ -1,4 +1,4 @@ -plugin.tx_felogin_pi1{ +plugin.tx_felogin_pi1 { oidc { wrap = OpenID Connect } diff --git a/Configuration/TypoScript/setup.txt b/Configuration/TypoScript/setup.typoscript similarity index 100% rename from Configuration/TypoScript/setup.txt rename to Configuration/TypoScript/setup.typoscript From c1c788e3215f86011147026b4b6f6fb35bcc86ab Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Mon, 28 Nov 2022 18:17:58 +0100 Subject: [PATCH 043/142] [TASK] Hook into EXT:felogin (Fluid-based template) --- Classes/Controller/LoginController.php | 2 +- .../FrontendLoginEventListener.php | 142 ++++++++++++++++++ Classes/Hooks/FeloginHook.php | 1 + Configuration/Services.yaml | 12 ++ .../TypoScript/felogin/constants.typoscript | 3 + .../TypoScript/felogin/setup.typoscript | 1 + Resources/Private/Templates/Login/Login.html | 103 +++++++++++++ 7 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 Classes/EventListener/FrontendLoginEventListener.php create mode 100644 Configuration/Services.yaml create mode 100644 Resources/Private/Templates/Login/Login.html diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php index 62f19fc..ff19b90 100644 --- a/Classes/Controller/LoginController.php +++ b/Classes/Controller/LoginController.php @@ -30,7 +30,7 @@ class LoginController protected $settings; /** - * TypoScript configuratoin of this plugin + * TypoScript configuration of this plugin * * @var array */ diff --git a/Classes/EventListener/FrontendLoginEventListener.php b/Classes/EventListener/FrontendLoginEventListener.php new file mode 100644 index 0000000..6cb5f8e --- /dev/null +++ b/Classes/EventListener/FrontendLoginEventListener.php @@ -0,0 +1,142 @@ +get('oidc') ?? []; + + if (empty($settings['oidcClientKey']) + || empty($settings['oidcClientSecret']) + || empty($settings['oidcEndpointAuthorize']) + || empty($settings['oidcEndpointToken']) + ) { + return; + } + + $requestId = $this->getUniqueId(); + static::getLogger()->debug('Post-processing felogin form', ['request' => $requestId]); + + if (session_id() === '') { // If no session exists, start a new one + static::getLogger()->debug('No PHP session found'); + session_start(); + } + + if (empty($_SESSION['requestId']) || $_SESSION['requestId'] !== $requestId) { + $this->prepareAuthorizationUrl($settings); + $_SESSION['requestId'] = $requestId; + $_SESSION['oidc_redirect_url'] = GeneralUtility::_GP('redirect_url'); + + static::getLogger()->debug('PHP session is available', [ + 'id' => session_id(), + 'data' => $_SESSION, + ]); + } else { + static::getLogger()->debug('Reusing same authorization URL and state'); + } + + $event->getView()->assign('openidConnectUri', $_SESSION['oidc_authorization_url']); + } + + /** + * Prepares the authorization URL and corresponding expected state (to mitigate CSRF attack) + * and stores information into the session. + * + * @param array $settings + */ + protected function prepareAuthorizationUrl(array $settings): void + { + /** @var OAuthService $service */ + $service = GeneralUtility::makeInstance(OAuthService::class); + $service->setSettings($settings); + $authorizationUrl = $service->getAuthorizationUrl(); + + // Store the state + $state = $service->getState(); + + static::getLogger()->debug('Generating authorization URL', [ + 'url' => $authorizationUrl, + 'state' => $state, + ]); + + $loginUrl = GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL'); + // Sanitize the URL + $parts = parse_url($loginUrl); + $queryParts = array_filter(explode('&', $parts['query'] ?? ''), function ($v) { + list ($k,) = explode('=', $v, 2); + + return !in_array($k, ['logintype', 'tx_oidc[code]']); + }); + $parts['query'] = implode('&', $queryParts); + $loginUrl = $parts['scheme'] . '://' . $parts['host']; + if (!empty($parts['port']) && !in_array((int)$parts['port'], [80, 443], true)) { + $loginUrl .= ':' . $parts['port']; + } + $loginUrl .= $parts['path']; + if (!empty($parts['query'])) { + $loginUrl .= '?' . $parts['query']; + } + + $_SESSION['oidc_state'] = $state; + $_SESSION['oidc_login_url'] = $loginUrl; + $_SESSION['oidc_authorization_url'] = $authorizationUrl; + } + + /** + * Returns a unique ID for the current processed request. + * + * This is supposed to be independent of the actual web server (Nginx or Apache) and + * the way PHP was built and unique enough for our use case, as opposed to using: + * + * - zend_thread_id() which requires PHP to be built with Zend Thread Safety - ZTS - support and debug mode + * - apache_getenv('UNIQUE_ID') which requires Apache as web server and mod_unique_id + * + * @return string + */ + protected function getUniqueId(): string + { + $uniqueId = sprintf('%08x', abs(crc32($_SERVER['REMOTE_ADDR'] . $_SERVER['REQUEST_TIME'] . $_SERVER['REMOTE_PORT']))); + + return $uniqueId; + } + + /** + * Returns a logger. + * + * @return Logger + */ + protected static function getLogger(): Logger + { + /** @var Logger $logger */ + static $logger = null; + if ($logger === null) { + $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__); + } + + return $logger; + } +} diff --git a/Classes/Hooks/FeloginHook.php b/Classes/Hooks/FeloginHook.php index 34478f9..baf624a 100644 --- a/Classes/Hooks/FeloginHook.php +++ b/Classes/Hooks/FeloginHook.php @@ -18,6 +18,7 @@ /** * Hooks into EXT:felogin to support custom markers. + * @deprecated */ class FeloginHook { diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml new file mode 100644 index 0000000..424ab17 --- /dev/null +++ b/Configuration/Services.yaml @@ -0,0 +1,12 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + Causal\Oidc\EventListener\FrontendLoginEventListener: + tags: + - name: event.listener + identifier: 'causal/oidc' + method: 'modifyLoginFormView' + event: TYPO3\CMS\FrontendLogin\Event\ModifyLoginFormViewEvent diff --git a/Configuration/TypoScript/felogin/constants.typoscript b/Configuration/TypoScript/felogin/constants.typoscript index 957e7cb..4395a31 100644 --- a/Configuration/TypoScript/felogin/constants.typoscript +++ b/Configuration/TypoScript/felogin/constants.typoscript @@ -1 +1,4 @@ +plugin.tx_felogin_login.view.templateRootPath = EXT:oidc/Resources/Private/Templates/ + +# Old EXT:felogin template (TYPO3 v9) styles.content.loginform.templateFile = EXT:oidc/Resources/Private/Templates/FrontendLogin.html diff --git a/Configuration/TypoScript/felogin/setup.typoscript b/Configuration/TypoScript/felogin/setup.typoscript index 7b07594..ea8fd73 100644 --- a/Configuration/TypoScript/felogin/setup.typoscript +++ b/Configuration/TypoScript/felogin/setup.typoscript @@ -1,3 +1,4 @@ +# deprecated plugin.tx_felogin_pi1 { oidc { wrap = OpenID Connect diff --git a/Resources/Private/Templates/Login/Login.html b/Resources/Private/Templates/Login/Login.html new file mode 100644 index 0000000..187f397 --- /dev/null +++ b/Resources/Private/Templates/Login/Login.html @@ -0,0 +1,103 @@ + + + + + + + + +

+ +

+

+ +

+
+ + + + + + + + + + + + + + + + + + + + +
+ + + +
+ +
+
+ +
+
+ + + OpenID Connect + + + Invalid OpenID Connect configuration + + +
+ + +
+ +
+
+ +
+ +
+ +
+ + + + + + + + + + + + + + +
+
+
+ From 7f92434f6cf2ecdee4ae33cc80bfd7867c027191 Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Mon, 5 Dec 2022 10:08:19 +0100 Subject: [PATCH 044/142] [BUGFIX] Prevent PHP warning with hook post-processing with PHP 8 Resolves: #85 --- Classes/Service/AuthenticationService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 8242ad5..8420c9e 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -473,7 +473,7 @@ protected function convertResourceOwner(array $info) // Hook for post-processing the user record $reloadUserRecord = false; - if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['oidc']['resourceOwner'])) { + if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['oidc']['resourceOwner'] ?? null)) { foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['oidc']['resourceOwner'] as $className) { /** @var \Causal\Oidc\Service\ResourceOwnerHookInterface $postProcessor */ $postProcessor = GeneralUtility::makeInstance($className); From 1e24505376b4584df7257114f195d620c21601a1 Mon Sep 17 00:00:00 2001 From: Daniel Siepmann Date: Thu, 9 Feb 2023 07:51:09 +0100 Subject: [PATCH 045/142] [BUGFIX] Prevent PHP warning in AuthenticationController with PHP 8 Resolves: #87 --- Classes/Controller/AuthenticationController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Classes/Controller/AuthenticationController.php b/Classes/Controller/AuthenticationController.php index 406e6a4..a8c524e 100644 --- a/Classes/Controller/AuthenticationController.php +++ b/Classes/Controller/AuthenticationController.php @@ -68,9 +68,9 @@ public function connectAction() 'data' => $_SESSION, ]); - if ($_GET['state'] !== $_SESSION['oidc_state']) { + if ($_GET['state'] !== ($_SESSION['oidc_state'] ?? null)) { static::getLogger()->error('Invalid returning state detected', [ - 'expected' => $_SESSION['oidc_state'], + 'expected' => $_SESSION['oidc_state'] ?? null, 'actual' => $_GET['state'], ]); if (!(bool)$this->globalSettings['oidcDisableCSRFProtection']) { From 877ef3ddb73315e16265e85c14cecc1fd2d99aac Mon Sep 17 00:00:00 2001 From: Daniel Siepmann Date: Thu, 9 Feb 2023 08:28:16 +0100 Subject: [PATCH 046/142] [BUGFIX] Prevent PHP warning in LoginController with PHP 8 Resolves: #89 --- Classes/Controller/LoginController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php index ff19b90..c60fe45 100644 --- a/Classes/Controller/LoginController.php +++ b/Classes/Controller/LoginController.php @@ -91,13 +91,14 @@ protected function performRedirectToLogin(array $authorizationUrlOptions = []) if (session_id() === '') { session_start(); } + $options = []; if ($this->settings['enableCodeVerifier']) { $codeVerifier = $this->generateCodeVerifier(); $codeChallenge = $this->convertVerifierToChallenge($codeVerifier); $options = $this->addCodeChallengeToOptions($codeChallenge, $authorizationUrlOptions); $_SESSION['oidc_code_verifier'] = $codeVerifier; } - $authorizationUrl = $service->getAuthorizationUrl($options ?: []); + $authorizationUrl = $service->getAuthorizationUrl($options); $state = $service->getState(); $_SESSION['oidc_state'] = $state; From 0ca5d6386490f589b176ed0bb79f0075643e44fa Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Thu, 9 Feb 2023 22:36:01 +0100 Subject: [PATCH 047/142] [TASK] Raise version to v1.2.0 --- composer.json | 6 +++--- ext_emconf.php | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index ed6e00c..33d178c 100644 --- a/composer.json +++ b/composer.json @@ -22,8 +22,8 @@ ], "license": "GPL-2.0-or-later", "require": { - "php": ">= 7.0.0, <= 8.1.99", - "typo3/cms-core": "^8.7 || ^9.5 || ^10.4 || ^11.5", + "php": ">= 7.0.0, <= 8.2.99", + "typo3/cms-core": "^8.7 || ^9.5 || ^10 || ^11", "league/oauth2-client": "^2.0" }, "autoload": { @@ -58,7 +58,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "1.2.x-dev" }, "typo3/cms": { "extension-key": "oidc", diff --git a/ext_emconf.php b/ext_emconf.php index 8068659..6937c87 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -11,10 +11,10 @@ 'createDirs' => '', 'modify_tables' => '', 'clearCacheOnLoad' => 0, - 'version' => '1.2.0-dev', + 'version' => '1.2.0', 'constraints' => [ 'depends' => [ - 'php' => '7.0.0-8.1.99', + 'php' => '7.0.0-8.2.99', 'typo3' => '8.7.0-11.5.99', ], 'conflicts' => [], From e5cc4b675e63be5b913bb61c4e45ad14a35f9965 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Wed, 9 Feb 2022 14:01:19 +0100 Subject: [PATCH 048/142] [FEATURE] Use HTTP_REFERER as oidc_redirect_url --- Classes/Hooks/FeloginHook.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Hooks/FeloginHook.php b/Classes/Hooks/FeloginHook.php index baf624a..8e4535f 100644 --- a/Classes/Hooks/FeloginHook.php +++ b/Classes/Hooks/FeloginHook.php @@ -57,7 +57,7 @@ public function postProcContent(array $params, \TYPO3\CMS\Felogin\Controller\Fro if (empty($_SESSION['requestId']) || $_SESSION['requestId'] !== $requestId) { $this->prepareAuthorizationUrl($settings); $_SESSION['requestId'] = $requestId; - $_SESSION['oidc_redirect_url'] = GeneralUtility::_GP('redirect_url'); + $_SESSION['oidc_redirect_url'] = GeneralUtility::_GP('redirect_url') ?? GeneralUtility::getIndpEnv('HTTP_REFERER') ?? ''; static::getLogger()->debug('PHP session is available', [ 'id' => session_id(), From e3942b20d9301e4e2a109e53998b1ab8eb7575f8 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Fri, 21 Apr 2023 11:02:53 +0200 Subject: [PATCH 049/142] [TASK] Use logFileInfix for logging configuration example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ac3170..216ac62 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ entries with level "WARNING" or above to the system log, you may add following c $GLOBALS['TYPO3_CONF_VARS']['LOG']['Causal']['Oidc']['writerConfiguration'] = [ \TYPO3\CMS\Core\Log\LogLevel::DEBUG => [ \TYPO3\CMS\Core\Log\Writer\FileWriter::class => [ - 'logFile' => \TYPO3\CMS\Core\Core\Environment::getVarPath() . '/log/oidc.log' + 'logFileInfix' => 'oidc' ], ], From 05b956e99cf263671d592aa18888a7f9ed4d0c55 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Fri, 21 Apr 2023 11:10:34 +0200 Subject: [PATCH 050/142] [TASK] Make xlf files crowdin compatible --- .../Private/Language/de.locallang_db.xlf | 63 ++++++++++++------- Resources/Private/Language/locallang_db.xlf | 48 +++++++------- 2 files changed, 66 insertions(+), 45 deletions(-) diff --git a/Resources/Private/Language/de.locallang_db.xlf b/Resources/Private/Language/de.locallang_db.xlf index 998e209..bb6a46d 100644 --- a/Resources/Private/Language/de.locallang_db.xlf +++ b/Resources/Private/Language/de.locallang_db.xlf @@ -3,68 +3,89 @@
- + + OIDC Login OIDC-Anmeldung - + + Frontend Authentication: Enable OpenID Connect authentication for the frontend. Frontend-Authentifizierung: Aktivieren Sie die OpenID Connect-Authentifizierung für das Frontend. - + + Re-enable Frontend Users: If ticked, will automatically re-enable Frontend users marked as "disabled" upon successful authentication. Frontend-Benutzer wieder aktivieren: Wenn angekreuzt, werden als "deaktiviert" markierte Frontend-Benutzer nach erfolgreicher Authentifizierung automatisch wieder aktiviert. - + + Undelete Frontend Users: If ticked, will automatically restore Frontend users marked as "deleted" upon successful authentication. Frontend-Benutzer wiederherstellen: Wenn angekreuzt, werden als "gelöscht" markierte Frontend-Benutzer nach erfolgreicher Authentifizierung automatisch wiederhergestellt. - + + Frontend User Must Exist: If ticked, only Frontend Users who are present locally in TYPO3 will be able to authenticate with OpenID Connect. You may need to watch logs to find users who could not authenticate. Frontend-Benutzer muss vorhanden sein: Wenn dieses Häkchen gesetzt ist, können sich nur Frontend User, die lokal in TYPO3 vorhanden sind, mit OpenID Connect authentifizieren. Möglicherweise müssen Sie die Logs beobachten, um Benutzer zu finden, die sich nicht authentifizieren konnten. - + + Client Key Client ID - + + Client Secret: Client-Secret: - + + Client Scopes: Client Scopes: "> - + + Authorize request language parameter name Name des Sprachparameters für die Autorisierungsanfrage - + + Endpoint URI for authorization Endpunkt-URI für Autorisierung - + + Endpoint URI for retrieving a token Endpunkt-URI zum Abrufen eines Tokens - + + Endpoint URI for fetching user information Endpunkt-URI für das Abrufen von Benutzerinformationen - + + Endpoint URI for logout Endpunkt-URI für Logout - + + Endpoint URI for revoking the token Endpunkt-URI für das Widerrufen des Tokens - + + Use Request Path Authentication: When ticked, this value will use Request Path Authentication instead of standard Password Grant. Anfragepfad-Authentifizierung verwenden: Wenn dieser Wert angekreuzt ist, wird die Request Path Authentication anstelle der standardmäßigen Password Grant verwendet. - + + Revoke TYPO3's access token at the end of the login process Zugriffstoken von TYPO3 am Ende des Anmeldevorgangs widerrufen - + + Disable CSRF attack mitigation: CAUTION! This is a security protection which checks the return state with the expected value. Disable this protection at your own risk. Deaktivieren Sie die Abschwächung von CSRF-Angriffen: ACHTUNG! Dies ist ein Sicherheitsschutz, der den Rückgabewert mit dem erwarteten Wert abgleicht. Deaktivieren Sie diesen Schutz auf Ihr eigenes Risiko. - + + Enable PKCE: Enable PKCE flow. Code challenge and code verifier will be sent along. PKCE aktivieren: Aktiviert den PKCE-Fluss. Code-Challenge und Code-Verifier werden mitgeschickt. - + + Case-insensitive pattern to match OpenID Connect role names ("*" matches every character, "|" to separate expressions) Groß-/Kleinschreibung-unempfindliches Muster zum Abgleichen von OpenID Connect-Rollennamen ("*" passt zu jedem Zeichen, "|" zu getrennten Ausdrücken) - + + Redirect URI: The authentication server callback will point to this URI. Redirect URI: Der Callback des Authentifizierungsservers wird auf diese URI verweisen. - + + OpenID Connect Identifier OpenID Connect Identifier diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index 2638e73..dc7652b 100755 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -3,77 +3,77 @@
- + OIDC Login - + Frontend Authentication: Enable OpenID Connect authentication for the frontend. - + Re-enable Frontend Users: If ticked, will automatically re-enable Frontend users marked as "disabled" upon successful authentication. - + Undelete Frontend Users: If ticked, will automatically restore Frontend users marked as "deleted" upon successful authentication. - + Frontend User Must Exist: If ticked, only Frontend Users who are present locally in TYPO3 will be able to authenticate with OpenID Connect. You may need to watch logs to find users who could not authenticate. - + Client Key - + Client Secret: - + Client Scopes: "> - + Authorize request language parameter name - + Endpoint URI for authorization - + Endpoint URI for retrieving a token - + Endpoint URI for fetching user information - + Endpoint URI for logout - + Endpoint URI for revoking the token - + Use Request Path Authentication: When ticked, this value will use Request Path Authentication instead of standard Password Grant. - + Revoke TYPO3's access token at the end of the login process - + Disable CSRF attack mitigation: CAUTION! This is a security protection which checks the return state with the expected value. Disable this protection at your own risk. - + Enable PKCE: Enable PKCE flow. Code challenge and code verifier will be sent along. - + Storage Pid: Comma-separated list of uid's from pages where fe_users are located. The first uid is used to store new users. - + Default user group(s) (comma-separated list of UIDs) - + Case-insensitive pattern to match OpenID Connect role names ("*" matches every character, "|" to separate expressions) - + Redirect URI: The authentication server callback will point to this URI. - + OpenID Connect Identifier - + OAuth Provider Factory: Fully qualified class name (empty for generic provider). From a3a4a685519fdc7afe5a7aaae719d000fddca18f Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Fri, 21 Apr 2023 11:11:41 +0200 Subject: [PATCH 051/142] [TASK] Sort English locallang_db.xlf using t3ll --- Resources/Private/Language/locallang_db.xlf | 81 ++++++++++----------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index dc7652b..b9cd758 100755 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -1,80 +1,79 @@ - - -
+ + +
- - OIDC Login + + Case-insensitive pattern to match OpenID Connect role names ("*" matches every character, "|" to separate expressions) - - - Frontend Authentication: Enable OpenID Connect authentication for the frontend. + + OpenID Connect Identifier - - Re-enable Frontend Users: If ticked, will automatically re-enable Frontend users marked as "disabled" upon successful authentication. + + Enable PKCE: Enable PKCE flow. Code challenge and code verifier will be sent along. - - Undelete Frontend Users: If ticked, will automatically restore Frontend users marked as "deleted" upon successful authentication. + + Frontend Authentication: Enable OpenID Connect authentication for the frontend. Frontend User Must Exist: If ticked, only Frontend Users who are present locally in TYPO3 will be able to authenticate with OpenID Connect. You may need to watch logs to find users who could not authenticate. + + OAuth Provider Factory: Fully qualified class name (empty for generic provider). + + + Authorize request language parameter name + Client Key + + Client Scopes: + Client Secret: - - Client Scopes: - "> - - Authorize request language parameter name + + Disable CSRF attack mitigation: CAUTION! This is a security protection which checks the return state with the expected value. Disable this protection at your own risk. Endpoint URI for authorization + + Endpoint URI for logout + + + Endpoint URI for revoking the token + Endpoint URI for retrieving a token Endpoint URI for fetching user information - - Endpoint URI for logout - - - Endpoint URI for revoking the token + + Redirect URI: The authentication server callback will point to this URI. Use Request Path Authentication: When ticked, this value will use Request Path Authentication instead of standard Password Grant. - - Revoke TYPO3's access token at the end of the login process - - - Disable CSRF attack mitigation: CAUTION! This is a security protection which checks the return state with the expected value. Disable this protection at your own risk. + + Re-enable Frontend Users: If ticked, will automatically re-enable Frontend users marked as "disabled" upon successful authentication. - - Enable PKCE: Enable PKCE flow. Code challenge and code verifier will be sent along. + + Revoke TYPO3's access token at the end of the login process - - Storage Pid: Comma-separated list of uid's from pages where fe_users are located. The first uid is used to store new users. + + Undelete Frontend Users: If ticked, will automatically restore Frontend users marked as "deleted" upon successful authentication. Default user group(s) (comma-separated list of UIDs) - - Case-insensitive pattern to match OpenID Connect role names ("*" matches every character, "|" to separate expressions) - - - Redirect URI: The authentication server callback will point to this URI. - - - OpenID Connect Identifier + + Storage Pid: Comma-separated list of uid's from pages where fe_users are located. The first uid is used to store new users. - - OAuth Provider Factory: Fully qualified class name (empty for generic provider). + + OIDC Login
From 9ce93fbf91a5915bc63463e38bc80c8466133979 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Fri, 21 Apr 2023 11:15:43 +0200 Subject: [PATCH 052/142] [TASK] Update German translation using t3ll --- .../Private/Language/de.locallang_db.xlf | 103 ++++++++++-------- Resources/Private/Language/locallang_db.xlf | 4 +- 2 files changed, 59 insertions(+), 48 deletions(-) diff --git a/Resources/Private/Language/de.locallang_db.xlf b/Resources/Private/Language/de.locallang_db.xlf index bb6a46d..a0cebd1 100644 --- a/Resources/Private/Language/de.locallang_db.xlf +++ b/Resources/Private/Language/de.locallang_db.xlf @@ -1,49 +1,64 @@ - - -
+ + +
- - OIDC Login - OIDC-Anmeldung + + Case-insensitive pattern to match OpenID Connect role names ("*" matches every character, "|" to separate expressions) + Groß-/Kleinschreibung-unempfindliches Muster zum Abgleichen von OpenID Connect-Rollennamen ("*" passt zu jedem Zeichen, "|" zu getrennten Ausdrücken) + + + OpenID Connect Identifier + OpenID Connect Identifier + + + Enable PKCE: Enable PKCE flow. Code challenge and code verifier will be sent along. + PKCE aktivieren: Aktiviert den PKCE-Fluss. Code-Challenge und Code-Verifier werden mitgeschickt. - Frontend Authentication: Enable OpenID Connect authentication for the frontend. Frontend-Authentifizierung: Aktivieren Sie die OpenID Connect-Authentifizierung für das Frontend. - - Re-enable Frontend Users: If ticked, will automatically re-enable Frontend users marked as "disabled" upon successful authentication. - Frontend-Benutzer wieder aktivieren: Wenn angekreuzt, werden als "deaktiviert" markierte Frontend-Benutzer nach erfolgreicher Authentifizierung automatisch wieder aktiviert. - - - Undelete Frontend Users: If ticked, will automatically restore Frontend users marked as "deleted" upon successful authentication. - Frontend-Benutzer wiederherstellen: Wenn angekreuzt, werden als "gelöscht" markierte Frontend-Benutzer nach erfolgreicher Authentifizierung automatisch wiederhergestellt. - Frontend User Must Exist: If ticked, only Frontend Users who are present locally in TYPO3 will be able to authenticate with OpenID Connect. You may need to watch logs to find users who could not authenticate. Frontend-Benutzer muss vorhanden sein: Wenn dieses Häkchen gesetzt ist, können sich nur Frontend User, die lokal in TYPO3 vorhanden sind, mit OpenID Connect authentifizieren. Möglicherweise müssen Sie die Logs beobachten, um Benutzer zu finden, die sich nicht authentifizieren konnten. + + OAuth Provider Factory: Fully qualified class name (empty for generic provider). + OAuth Provider Factory: Voll qualifizierter Klassenname (leer für generic provider) + + + Authorize request language parameter name + Name des Sprachparameters für die Autorisierungsanfrage + Client Key Client ID + + Client Scopes: + Client Scopes: + Client Secret: Client-Secret: - - Client Scopes: - Client Scopes: - "> - - Authorize request language parameter name - Name des Sprachparameters für die Autorisierungsanfrage + + Disable CSRF attack mitigation: CAUTION! This is a security protection which checks the return state with the expected value. Disable this protection at your own risk. + Deaktivieren Sie die Abschwächung von CSRF-Angriffen: ACHTUNG! Dies ist ein Sicherheitsschutz, der den Rückgabewert mit dem erwarteten Wert abgleicht. Deaktivieren Sie diesen Schutz auf Ihr eigenes Risiko. Endpoint URI for authorization Endpunkt-URI für Autorisierung + + Endpoint URI for logout + Endpunkt-URI für Logout + + + Endpoint URI for revoking the token + Endpunkt-URI für das Widerrufen des Tokens + Endpoint URI for retrieving a token Endpunkt-URI zum Abrufen eines Tokens @@ -52,41 +67,37 @@ Endpoint URI for fetching user information Endpunkt-URI für das Abrufen von Benutzerinformationen - - Endpoint URI for logout - Endpunkt-URI für Logout - - - Endpoint URI for revoking the token - Endpunkt-URI für das Widerrufen des Tokens + + Redirect URI: The authentication server callback will point to this URI. + Redirect URI: Der Callback des Authentifizierungsservers wird auf diese URI verweisen. Use Request Path Authentication: When ticked, this value will use Request Path Authentication instead of standard Password Grant. Anfragepfad-Authentifizierung verwenden: Wenn dieser Wert angekreuzt ist, wird die Request Path Authentication anstelle der standardmäßigen Password Grant verwendet. + + Re-enable Frontend Users: If ticked, will automatically re-enable Frontend users marked as "disabled" upon successful authentication. + Frontend-Benutzer wieder aktivieren: Wenn angekreuzt, werden als "deaktiviert" markierte Frontend-Benutzer nach erfolgreicher Authentifizierung automatisch wieder aktiviert. + - Revoke TYPO3's access token at the end of the login process + Revoke TYPO3's access token at the end of the login process Zugriffstoken von TYPO3 am Ende des Anmeldevorgangs widerrufen - - Disable CSRF attack mitigation: CAUTION! This is a security protection which checks the return state with the expected value. Disable this protection at your own risk. - Deaktivieren Sie die Abschwächung von CSRF-Angriffen: ACHTUNG! Dies ist ein Sicherheitsschutz, der den Rückgabewert mit dem erwarteten Wert abgleicht. Deaktivieren Sie diesen Schutz auf Ihr eigenes Risiko. - - - Enable PKCE: Enable PKCE flow. Code challenge and code verifier will be sent along. - PKCE aktivieren: Aktiviert den PKCE-Fluss. Code-Challenge und Code-Verifier werden mitgeschickt. + + Undelete Frontend Users: If ticked, will automatically restore Frontend users marked as "deleted" upon successful authentication. + Frontend-Benutzer wiederherstellen: Wenn angekreuzt, werden als "gelöscht" markierte Frontend-Benutzer nach erfolgreicher Authentifizierung automatisch wiederhergestellt. - - Case-insensitive pattern to match OpenID Connect role names ("*" matches every character, "|" to separate expressions) - Groß-/Kleinschreibung-unempfindliches Muster zum Abgleichen von OpenID Connect-Rollennamen ("*" passt zu jedem Zeichen, "|" zu getrennten Ausdrücken) + + Default user group(s) (comma-separated list of UIDs) + Standard-Benutzergruppen (komma-separierte Liste von UIDs) - - Redirect URI: The authentication server callback will point to this URI. - Redirect URI: Der Callback des Authentifizierungsservers wird auf diese URI verweisen. + + Storage Pid: Comma-separated list of page UIDs where fe_users are located. The first UID is used to store new users. + Ablage Pid: Komma-separierte Liste der Seiten UIDs, wo fe_users gespeichert werden. Die erste UID wird zur Speicherung neuer Benutzer verwendet. - - OpenID Connect Identifier - OpenID Connect Identifier + + OIDC Login + OIDC-Anmeldung
diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index b9cd758..ae2886b 100755 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -1,6 +1,6 @@ - +
@@ -70,7 +70,7 @@ Default user group(s) (comma-separated list of UIDs) - Storage Pid: Comma-separated list of uid's from pages where fe_users are located. The first uid is used to store new users. + Storage Pid: Comma-separated list of page UIDs where fe_users are located. The first UID is used to store new users. OIDC Login From 51fb90ce2824f79d6f87ca81db98a56f5583b350 Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Mon, 24 Apr 2023 10:14:26 +0200 Subject: [PATCH 053/142] [TASK] Drop support for old TYPO3 versions --- .../Controller/AuthenticationController.php | 10 +- Classes/Controller/LoginController.php | 18 +--- Classes/Hooks/FeloginHook.php | 10 +- Classes/Service/AuthenticationService.php | 102 ++++-------------- Classes/ViewHelpers/OidcLinkViewHelper.php | 10 +- Configuration/TCA/Overrides/fe_users.php | 10 +- composer.json | 10 +- ext_emconf.php | 6 +- ext_localconf.php | 46 +++----- 9 files changed, 53 insertions(+), 169 deletions(-) diff --git a/Classes/Controller/AuthenticationController.php b/Classes/Controller/AuthenticationController.php index a8c524e..52b2fd8 100644 --- a/Classes/Controller/AuthenticationController.php +++ b/Classes/Controller/AuthenticationController.php @@ -36,14 +36,8 @@ class AuthenticationController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionC */ public function initializeAction() { - $typo3Branch = class_exists(\TYPO3\CMS\Core\Information\Typo3Version::class) - ? (new \TYPO3\CMS\Core\Information\Typo3Version())->getBranch() - : TYPO3_branch; - if (version_compare($typo3Branch, '9.0', '<')) { - $this->globalSettings = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['oidc']); - } else { - $this->globalSettings = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['oidc'] ?? []; - } + // TODO: Use proper TYPO3 API + $this->globalSettings = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['oidc'] ?? []; } /** diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php index c60fe45..94bc6a0 100644 --- a/Classes/Controller/LoginController.php +++ b/Classes/Controller/LoginController.php @@ -43,14 +43,8 @@ class LoginController public function __construct() { - $typo3Branch = class_exists(\TYPO3\CMS\Core\Information\Typo3Version::class) - ? (new \TYPO3\CMS\Core\Information\Typo3Version())->getBranch() - : TYPO3_branch; - if (version_compare($typo3Branch, '9.0', '<')) { - $this->settings = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['oidc']); - } else { - $this->settings = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['oidc'] ?? []; - } + // TODO: Use proper TYPO3 API + $this->settings = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['oidc'] ?? []; } public function setContentObjectRenderer(ContentObjectRenderer $cObj) @@ -133,14 +127,6 @@ protected function determineRedirectUrl() protected function redirect(string $redirectUrl): void { - $typo3Branch = class_exists(\TYPO3\CMS\Core\Information\Typo3Version::class) - ? (new \TYPO3\CMS\Core\Information\Typo3Version())->getBranch() - : TYPO3_branch; - - if (version_compare($typo3Branch, '11.0', '<')) { - HttpUtility::redirect($redirectUrl); - } - throw new PropagateResponseException(new RedirectResponse($redirectUrl)); } diff --git a/Classes/Hooks/FeloginHook.php b/Classes/Hooks/FeloginHook.php index baf624a..76f80a9 100644 --- a/Classes/Hooks/FeloginHook.php +++ b/Classes/Hooks/FeloginHook.php @@ -33,14 +33,8 @@ public function postProcContent(array $params, \TYPO3\CMS\Felogin\Controller\Fro static::getLogger()->debug('Post-processing markers for felogin form', ['request' => $requestId]); $markerArray['###OPENID_CONNECT###'] = ''; - $typo3Branch = class_exists(\TYPO3\CMS\Core\Information\Typo3Version::class) - ? (new \TYPO3\CMS\Core\Information\Typo3Version())->getBranch() - : TYPO3_branch; - if (version_compare($typo3Branch, '9.0', '<')) { - $settings = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['oidc']); - } else { - $settings = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['oidc'] ?? []; - } + // TODO: Use proper TYPO3 API + $settings = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['oidc'] ?? []; if (empty($settings['oidcClientKey']) || empty($settings['oidcClientSecret']) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 8420c9e..b333271 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -72,14 +72,8 @@ class AuthenticationService extends \TYPO3\CMS\Sv\AuthenticationService */ public function __construct() { - $typo3Branch = class_exists(\TYPO3\CMS\Core\Information\Typo3Version::class) - ? (new \TYPO3\CMS\Core\Information\Typo3Version())->getBranch() - : TYPO3_branch; - if (version_compare($typo3Branch, '9.0', '<')) { - $this->config = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['oidc']); - } else { - $this->config = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['oidc'] ?? []; - } + // TODO: Use proper TYPO3 API + $this->config = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['oidc'] ?? []; } protected function getCodeVerifierFromSession() @@ -127,16 +121,10 @@ public function getUser() $dispatcher = GeneralUtility::makeInstance(ObjectManager::class)->get(Dispatcher::class); $dispatcher->dispatch(__CLASS__, 'getUser', ['user' => $user]); - $typo3Branch = class_exists(\TYPO3\CMS\Core\Information\Typo3Version::class) - ? (new \TYPO3\CMS\Core\Information\Typo3Version())->getBranch() - : TYPO3_branch; - - if (version_compare($typo3Branch, '10.2', '>=')) { - $event = new AuthenticationGetUserEvent($user); - $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); - $eventDispatcher->dispatch($event); - $user = $event->getUser(); - } + $event = new AuthenticationGetUserEvent($user); + $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); + $eventDispatcher->dispatch($event); + $user = $event->getUser(); if (is_array($user)) { unset($user['accessToken']); @@ -335,15 +323,8 @@ protected function convertResourceOwner(array $info) } /** @var $objInstanceSaltedPW \TYPO3\CMS\Saltedpasswords\Salt\SaltInterface */ - $typo3Branch = class_exists(\TYPO3\CMS\Core\Information\Typo3Version::class) - ? (new \TYPO3\CMS\Core\Information\Typo3Version())->getBranch() - : TYPO3_branch; - if (version_compare($typo3Branch, '9.5', '>=')) { - $passwordHashFactory = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory::class); - $objInstanceSaltedPW = $passwordHashFactory->getDefaultHashInstance(TYPO3_MODE); - } else { - $objInstanceSaltedPW = \TYPO3\CMS\Saltedpasswords\Salt\SaltFactory::getSaltingInstance(null, TYPO3_MODE); - } + $passwordHashFactory = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory::class); + $objInstanceSaltedPW = $passwordHashFactory->getDefaultHashInstance(TYPO3_MODE); $password = substr(str_shuffle('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$'), 0, 20); $hashedPassword = $objInstanceSaltedPW->getHashedPassword($password); @@ -710,63 +691,24 @@ protected function getTypoScriptSetup() $GLOBALS['TCA'][$table] = include($file); } - $typo3Branch = class_exists(\TYPO3\CMS\Core\Information\Typo3Version::class) - ? (new \TYPO3\CMS\Core\Information\Typo3Version())->getBranch() - : TYPO3_branch; - - if (version_compare($typo3Branch, '10.0', '>=')) { - /** @var ServerRequestInterface $request */ - $request = $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals(); - /** @var SiteMatcher $siteMatcher */ - $siteMatcher = GeneralUtility::makeInstance(SiteMatcher::class); - $routeResult = $siteMatcher->matchRequest($request); - $site = $routeResult->getSite(); - $pageArguments = $site->getRouter()->matchRequest($request, $routeResult); - $currentPage = $pageArguments->getPageId(); - - $frontendUser = GeneralUtility::makeInstance(FrontendUserAuthentication::class); - $context = GeneralUtility::makeInstance(Context::class); - $localTSFE = GeneralUtility::makeInstance(TypoScriptFrontendController::class, $context, $site, $routeResult->getLanguage(), $pageArguments, $frontendUser); - - /** @var TemplateService $templateService */ - $templateService = GeneralUtility::makeInstance(TemplateService::class, null, null, $localTSFE); - - $rootLine = GeneralUtility::makeInstance(RootlineUtility::class, (int)$currentPage)->get(); - $templateService->start($rootLine); - $setup = $templateService->setup; - return $setup; - } + /** @var ServerRequestInterface $request */ + $request = $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals(); + /** @var SiteMatcher $siteMatcher */ + $siteMatcher = GeneralUtility::makeInstance(SiteMatcher::class); + $routeResult = $siteMatcher->matchRequest($request); + $site = $routeResult->getSite(); + $pageArguments = $site->getRouter()->matchRequest($request, $routeResult); + $currentPage = $pageArguments->getPageId(); - /** @var \TYPO3\CMS\Frontend\Page\PageRepository $pageRepository */ - $pageRepository = GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\Page\PageRepository::class); - if (version_compare($typo3Branch, '9.0', '<')) { - $pageRepository->init(false); - } + $frontendUser = GeneralUtility::makeInstance(FrontendUserAuthentication::class); + $context = GeneralUtility::makeInstance(Context::class); + $localTSFE = GeneralUtility::makeInstance(TypoScriptFrontendController::class, $context, $site, $routeResult->getLanguage(), $pageArguments, $frontendUser); /** @var TemplateService $templateService */ - $templateService = GeneralUtility::makeInstance(TemplateService::class); - if (version_compare($typo3Branch, '9.0', '<')) { - $templateService->init(); - } - $templateService->tt_track = false; - - $currentPage = $GLOBALS['TSFE']->id; - if ($currentPage === null) { - // root page is not yet populated - $localTSFE = clone $GLOBALS['TSFE']; - if (version_compare($typo3Branch, '9.5', '>=')) { - $localTSFE->fe_user = GeneralUtility::makeInstance(FrontendUserAuthentication::class); - } - $localTSFE->determineId(); - $currentPage = $localTSFE->id; - } - if (version_compare($typo3Branch, '9.5', '>=')) { - $rootLine = GeneralUtility::makeInstance(RootlineUtility::class, (int)$currentPage)->get(); - } else { - $rootLine = $pageRepository->getRootLine((int)$currentPage); - } - $templateService->start($rootLine); + $templateService = GeneralUtility::makeInstance(TemplateService::class, null, null, $localTSFE); + $rootLine = GeneralUtility::makeInstance(RootlineUtility::class, (int)$currentPage)->get(); + $templateService->start($rootLine); $setup = $templateService->setup; return $setup; } diff --git a/Classes/ViewHelpers/OidcLinkViewHelper.php b/Classes/ViewHelpers/OidcLinkViewHelper.php index 53efb37..d0ddfa7 100644 --- a/Classes/ViewHelpers/OidcLinkViewHelper.php +++ b/Classes/ViewHelpers/OidcLinkViewHelper.php @@ -40,14 +40,8 @@ public static function renderStatic(array $arguments, \Closure $renderChildrenCl static::getLogger()->debug('Post-processing markers for felogin form', ['request' => $requestId]); $link = ''; - $typo3Branch = class_exists(Typo3Version::class) - ? (new Typo3Version())->getBranch() - : TYPO3_branch; - if (version_compare($typo3Branch, '9.0', '<')) { - $settings = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['oidc']); - } else { - $settings = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['oidc'] ?? []; - } + // TODO: Use proper TYPO3 API + $settings = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['oidc'] ?? []; if (empty($settings['oidcClientKey']) || empty($settings['oidcClientSecret']) diff --git a/Configuration/TCA/Overrides/fe_users.php b/Configuration/TCA/Overrides/fe_users.php index 39c4259..e6393ff 100644 --- a/Configuration/TCA/Overrides/fe_users.php +++ b/Configuration/TCA/Overrides/fe_users.php @@ -1,14 +1,8 @@ getBranch() - : TYPO3_branch; -if (version_compare($typo3Branch, '9.0', '<')) { - $settings = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['oidc']); -} else { - $settings = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['oidc'] ?? []; -} +// TODO: Use proper TYPO3 API +$settings = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['oidc'] ?? []; $tempColumns = [ 'tx_oidc' => [ diff --git a/composer.json b/composer.json index 33d178c..cb0b611 100644 --- a/composer.json +++ b/composer.json @@ -22,8 +22,8 @@ ], "license": "GPL-2.0-or-later", "require": { - "php": ">= 7.0.0, <= 8.2.99", - "typo3/cms-core": "^8.7 || ^9.5 || ^10 || ^11", + "php": ">= 7.4.0, <= 8.2.99", + "typo3/cms-core": "^11", "league/oauth2-client": "^2.0" }, "autoload": { @@ -58,11 +58,11 @@ }, "extra": { "branch-alias": { - "dev-master": "1.2.x-dev" + "dev-TYPO3_8-11": "1.2.x-dev", + "dev-master": "1.3.x-dev" }, "typo3/cms": { - "extension-key": "oidc", - "cms-package-dir": "{$vendor-dir}/typo3/cms" + "extension-key": "oidc" } } } diff --git a/ext_emconf.php b/ext_emconf.php index 6937c87..4e248d0 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -11,11 +11,11 @@ 'createDirs' => '', 'modify_tables' => '', 'clearCacheOnLoad' => 0, - 'version' => '1.2.0', + 'version' => '1.3.0-dev', 'constraints' => [ 'depends' => [ - 'php' => '7.0.0-8.2.99', - 'typo3' => '8.7.0-11.5.99', + 'php' => '7.4.0-8.2.99', + 'typo3' => '11.5.0-11.5.99', ], 'conflicts' => [], 'suggests' => [], diff --git a/ext_localconf.php b/ext_localconf.php index 45b10a6..ceb8852 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -8,14 +8,8 @@ class_alias(\TYPO3\CMS\Core\Authentication\AuthenticationService::class, \TYPO3\ } // Configuration of authentication service - $typo3Branch = class_exists(\TYPO3\CMS\Core\Information\Typo3Version::class) - ? (new \TYPO3\CMS\Core\Information\Typo3Version())->getBranch() - : TYPO3_branch; - if (version_compare($typo3Branch, '9.0', '<')) { - $settings = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'][$_EXTKEY]); - } else { - $settings = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$_EXTKEY] ?? []; - } + // TODO: Use proper TYPO3 API + $settings = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$_EXTKEY] ?? []; // Service configuration $subTypesArr = []; @@ -48,31 +42,17 @@ class_alias(\TYPO3\CMS\Core\Authentication\AuthenticationService::class, \TYPO3\ ] ); - if (version_compare($typo3Branch, '10.0', '<')) { - \TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin( - 'Causal.' . $_EXTKEY, - 'Pi1', - [ - 'Authentication' => 'connect', - ], - // non-cacheable actions - [ - 'Authentication' => 'connect' - ] - ); - } else { - \TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin( - $_EXTKEY, - 'Pi1', - [ - \Causal\Oidc\Controller\AuthenticationController::class => 'connect', - ], - // non-cacheable actions - [ - \Causal\Oidc\Controller\AuthenticationController::class => 'connect' - ] - ); - } + \TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin( + $_EXTKEY, + 'Pi1', + [ + \Causal\Oidc\Controller\AuthenticationController::class => 'connect', + ], + // non-cacheable actions + [ + \Causal\Oidc\Controller\AuthenticationController::class => 'connect' + ] + ); if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('felogin')) { $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['postProcContent'][$_EXTKEY] = \Causal\Oidc\Hooks\FeloginHook::class . '->postProcContent'; From 9e46d2e28d43c2cc38808ed766ac888cc630fd4b Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Mon, 24 Apr 2023 10:23:21 +0200 Subject: [PATCH 054/142] [TASK] Drop usage of TYPO3_MODE --- Classes/Service/AuthenticationService.php | 13 ++++++++----- Configuration/TCA/Overrides/fe_groups.php | 2 +- Configuration/TCA/Overrides/fe_users.php | 2 +- Configuration/TCA/Overrides/sys_template.php | 2 +- Configuration/TCA/Overrides/tt_content.php | 2 +- ext_localconf.php | 2 +- ext_tables.php | 2 +- 7 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index b333271..2fcc500 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -23,6 +23,7 @@ use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction; use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction; use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction; +use TYPO3\CMS\Core\Http\ApplicationType; use TYPO3\CMS\Core\Http\ServerRequestFactory; use TYPO3\CMS\Core\Routing\SiteMatcher; use TYPO3\CMS\Core\TypoScript\TemplateService; @@ -275,10 +276,12 @@ public function authUser(array $user): int */ protected function convertResourceOwner(array $info) { - if (TYPO3_MODE === 'FE') { + if (ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend()) { + $mode = 'FE'; $userTable = 'fe_users'; $userGroupTable = 'fe_groups'; } else { + $mode = 'BE'; $userTable = 'be_users'; $userGroupTable = 'be_groups'; } @@ -322,9 +325,9 @@ protected function convertResourceOwner(array $info) return false; } - /** @var $objInstanceSaltedPW \TYPO3\CMS\Saltedpasswords\Salt\SaltInterface */ $passwordHashFactory = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory::class); - $objInstanceSaltedPW = $passwordHashFactory->getDefaultHashInstance(TYPO3_MODE); + /** @var $objInstanceSaltedPW \TYPO3\CMS\Saltedpasswords\Salt\SaltInterface */ + $objInstanceSaltedPW = $passwordHashFactory->getDefaultHashInstance($mode); $password = substr(str_shuffle('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$'), 0, 20); $hashedPassword = $objInstanceSaltedPW->getHashedPassword($password); @@ -459,7 +462,7 @@ protected function convertResourceOwner(array $info) /** @var \Causal\Oidc\Service\ResourceOwnerHookInterface $postProcessor */ $postProcessor = GeneralUtility::makeInstance($className); if ($postProcessor instanceof \Causal\Oidc\Service\ResourceOwnerHookInterface) { - $postProcessor->postProcessUser(TYPO3_MODE, $user, $info); + $postProcessor->postProcessUser($mode, $user, $info); $reloadUserRecord = true; } else { throw new \InvalidArgumentException( @@ -572,7 +575,7 @@ protected function applyMapping($table, array $oidc, array $typo3, array $baseDa // Instantiation of TypoScriptFrontendController instantiates PageRenderer which // sets backPath to TYPO3_mainDir which is very bad in the Backend. Therefore, // we must set it back to null to not get frontend-prefixed asset URLs. - if (TYPO3_MODE === 'BE') { + if (ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend()) { $pageRenderer = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Page\PageRenderer::class); $pageRenderer->setBackPath(null); } diff --git a/Configuration/TCA/Overrides/fe_groups.php b/Configuration/TCA/Overrides/fe_groups.php index 6d21bd5..0938cd5 100644 --- a/Configuration/TCA/Overrides/fe_groups.php +++ b/Configuration/TCA/Overrides/fe_groups.php @@ -1,5 +1,5 @@ [ diff --git a/Configuration/TCA/Overrides/fe_users.php b/Configuration/TCA/Overrides/fe_users.php index e6393ff..f5f64aa 100644 --- a/Configuration/TCA/Overrides/fe_users.php +++ b/Configuration/TCA/Overrides/fe_users.php @@ -1,5 +1,5 @@ Date: Mon, 24 Apr 2023 10:25:41 +0200 Subject: [PATCH 055/142] [TASK] Use current location for AuthenticationService Resolves: #96 --- Classes/Service/AuthenticationService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 2fcc500..a3e3d19 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -38,7 +38,7 @@ /** * OpenID Connect authentication service. */ -class AuthenticationService extends \TYPO3\CMS\Sv\AuthenticationService +class AuthenticationService extends \TYPO3\CMS\Core\Authentication\AuthenticationService { /** From 46edb7c1fb5ec507efd64153e4651c0d0fc12163 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Fri, 21 Apr 2023 12:27:04 +0200 Subject: [PATCH 056/142] [TASK] Cleanup AuthenticationService --- .editorconfig | 16 +- Classes/Service/AuthenticationService.php | 217 ++++++++++++---------- composer.json | 132 ++++++------- 3 files changed, 201 insertions(+), 164 deletions(-) diff --git a/.editorconfig b/.editorconfig index d37a2c5..0acfb72 100644 --- a/.editorconfig +++ b/.editorconfig @@ -21,17 +21,25 @@ indent_size = 2 indent_style = tab # ReST-Files -[*.rst] -indent_size = 3 +[*.{rst,rst.txt}] +indent_size = 4 +max_line_length = 80 + +# Markdown-Files +[*.md] max_line_length = 80 # YAML-Files [*.{yaml,yml}] indent_size = 2 +# NEON-Files +[*.neon] +indent_size = 2 +indent_style = tab + # package.json -# .travis.yml -[{package.json,.travis.yml}] +[package.json] indent_size = 2 # TypoScript diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index a3e3d19..512d0ca 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -1,4 +1,7 @@ login['uname']) ? $this->login['uname'] : null; + $code = $params['code'] ?? null; + $username = $this->login['uname'] ?? null; if (isset($this->login['uident_text'])) { $password = $this->login['uident_text']; @@ -111,7 +130,7 @@ public function getUser() if ($this->config['enableCodeVerifier']) { $codeVerifier = $this->getCodeVerifierFromSession(); } - $user = $this->authenticateWithAuhorizationCode($code, $codeVerifier); + $user = $this->authenticateWithAuthorizationCode($code, $codeVerifier); } elseif (!(empty($username) || empty($password))) { $user = $this->authenticateWithResourceOwnerPasswordCredentials($username, $password); } @@ -119,6 +138,7 @@ public function getUser() // dispatch a signal (containing the user with his access token if auth was successful) // so other extensions can use them to make further requests to an API // provided by the authentication server + /** @var Dispatcher $dispatcher */ $dispatcher = GeneralUtility::makeInstance(ObjectManager::class)->get(Dispatcher::class); $dispatcher->dispatch(__CLASS__, 'getUser', ['user' => $user]); @@ -127,7 +147,7 @@ public function getUser() $eventDispatcher->dispatch($event); $user = $event->getUser(); - if (is_array($user)) { + if (isset($user['accessToken'])) { unset($user['accessToken']); } @@ -138,9 +158,10 @@ public function getUser() * Authenticates a user using authorization code grant. * * @param string $code + * @param string|null $codeVerifier * @return array|bool */ - protected function authenticateWithAuhorizationCode($code, $codeVerifier = null) + protected function authenticateWithAuthorizationCode(string $code, string $codeVerifier = null) { static::getLogger()->debug('Initializing OpenID Connect service'); @@ -153,7 +174,7 @@ protected function authenticateWithAuhorizationCode($code, $codeVerifier = null) static::getLogger()->debug('Retrieving an access token'); $accessToken = $service->getAccessToken($code, null, $codeVerifier); static::getLogger()->debug('Access token retrieved', $accessToken->jsonSerialize()); - } catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) { + } catch (IdentityProviderException $e) { // Probably a "server_error", meaning the code is not valid anymore static::getLogger()->error('Possibly replay: code has been refused by the authentication server', [ 'code' => $code, @@ -177,7 +198,7 @@ protected function authenticateWithAuhorizationCode($code, $codeVerifier = null) * @param string $password * @return array|bool */ - protected function authenticateWithResourceOwnerPasswordCredentials($username, $password) + protected function authenticateWithResourceOwnerPasswordCredentials(string $username, string $password) { $user = false; static::getLogger()->debug('Initializing OpenID Connect service'); @@ -186,8 +207,9 @@ protected function authenticateWithResourceOwnerPasswordCredentials($username, $ $service = GeneralUtility::makeInstance(OAuthService::class); $service->setSettings($this->config); + $accessToken = ''; try { - if ((bool)$this->config['oidcUseRequestPathAuthentication']) { + if ($this->config['oidcUseRequestPathAuthentication']) { static::getLogger()->debug('Retrieving an access token using request path authentication'); $accessToken = $service->getAccessTokenWithRequestPathAuthentication($username, $password); } else { @@ -198,7 +220,7 @@ protected function authenticateWithResourceOwnerPasswordCredentials($username, $ static::getLogger()->debug('Access token retrieved', $accessToken->jsonSerialize()); $user = $this->getUserFromAccessToken($service, $accessToken); } - } catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) { + } catch (IdentityProviderException $e) { static::getLogger()->error('Authentication has been refused by the authentication server', [ 'username' => $username, 'message' => $e->getMessage(), @@ -226,7 +248,7 @@ protected function getUserFromAccessToken(OAuthService $service, AccessToken $ac static::getLogger()->debug('Retrieving resource owner'); $resourceOwner = $service->getResourceOwner($accessToken)->toArray(); static::getLogger()->debug('Resource owner retrieved', $resourceOwner); - } catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) { + } catch (IdentityProviderException $e) { static::getLogger()->error('Could not retrieve resource owner', [ 'message' => $e->getMessage(), ]); @@ -235,7 +257,7 @@ protected function getUserFromAccessToken(OAuthService $service, AccessToken $ac if (empty($resourceOwner['sub'])) { static::getLogger()->error('No "sub" found in resource owner, revoking access token'); $service->revokeToken($accessToken); - throw new \RuntimeException( + throw new RuntimeException( 'Resource owner does not have a sub part: ' . json_encode($resourceOwner) . '. Your access token has been revoked. Please try again.', 1490086626 @@ -258,13 +280,10 @@ protected function getUserFromAccessToken(OAuthService $service, AccessToken $ac */ public function authUser(array $user): int { - $status = static::STATUS_AUTHENTICATION_FAILURE_CONTINUE; - if (!empty($user['tx_oidc'])) { - $status = static::STATUS_AUTHENTICATION_SUCCESS_BREAK; + return static::STATUS_AUTHENTICATION_SUCCESS_BREAK; } - - return $status; + return static::STATUS_AUTHENTICATION_FAILURE_CONTINUE; } /** @@ -272,7 +291,7 @@ public function authUser(array $user): int * * @param array $info * @return array|bool - * @throws \InvalidArgumentException + * @throws InvalidArgumentException */ protected function convertResourceOwner(array $info) { @@ -295,24 +314,24 @@ protected function convertResourceOwner(array $info) ->where( $queryBuilder->expr()->in('pid', $queryBuilder->createNamedParameter( GeneralUtility::intExplode(',', $this->config['usersStoragePid']), - \TYPO3\CMS\Core\Database\Connection::PARAM_INT_ARRAY + Connection::PARAM_INT_ARRAY )), - $queryBuilder->expr()->eq('tx_oidc', $queryBuilder->createNamedParameter($info['sub'], \PDO::PARAM_STR)) + $queryBuilder->expr()->eq('tx_oidc', $queryBuilder->createNamedParameter($info['sub'])) ) ->execute() - ->fetch(); + ->fetchAssociative(); $reEnableUser = (bool)$this->config['reEnableFrontendUsers']; $undeleteUser = (bool)$this->config['undeleteFrontendUsers']; $frontendUserMustExistLocally = (bool)$this->config['frontendUserMustExistLocally']; - if (!empty($row) && (bool)$row['deleted'] && !$undeleteUser) { + if (!empty($row) && $row['deleted'] && !$undeleteUser) { // User was manually deleted, it should not get automatically restored static::getLogger()->info('User was manually deleted, denying access', ['user' => $row]); return false; } - if (!empty($row) && (bool)$row['disable'] && !$reEnableUser) { + if (!empty($row) && $row['disable'] && !$reEnableUser) { // User was manually disabled, it should not get automatically re-enabled static::getLogger()->info('User was manually disabled, denying access', ['user' => $row]); @@ -325,18 +344,12 @@ protected function convertResourceOwner(array $info) return false; } - $passwordHashFactory = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory::class); - /** @var $objInstanceSaltedPW \TYPO3\CMS\Saltedpasswords\Salt\SaltInterface */ - $objInstanceSaltedPW = $passwordHashFactory->getDefaultHashInstance($mode); - $password = substr(str_shuffle('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$'), 0, 20); - $hashedPassword = $objInstanceSaltedPW->getHashedPassword($password); - $data = $this->applyMapping( $userTable, $info, $row ?: [], [ - 'password' => $hashedPassword, + 'password' => $this->generatePassword(), 'deleted' => 0, 'disable' => 0, ] @@ -363,7 +376,7 @@ protected function convertResourceOwner(array $info) $queryBuilder->expr()->neq('tx_oidc_pattern', $queryBuilder->quote('')) ) ->execute() - ->fetchAll(); + ->fetchAllAssociative(); $oidcUserGroups = []; foreach ($groups as $group) { @@ -386,7 +399,7 @@ protected function convertResourceOwner(array $info) $queryBuilder->expr()->neq('tx_oidc_pattern', $queryBuilder->quote('')) ) ->execute() - ->fetchAll(); + ->fetchAllAssociative(); $roles = is_array($info['Roles']) ? $info['Roles'] : GeneralUtility::trimExplode(',', $info['Roles'], true); $roles = ',' . implode(',', $roles) . ','; @@ -459,13 +472,13 @@ protected function convertResourceOwner(array $info) $reloadUserRecord = false; if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['oidc']['resourceOwner'] ?? null)) { foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['oidc']['resourceOwner'] as $className) { - /** @var \Causal\Oidc\Service\ResourceOwnerHookInterface $postProcessor */ + /** @var ResourceOwnerHookInterface $postProcessor */ $postProcessor = GeneralUtility::makeInstance($className); - if ($postProcessor instanceof \Causal\Oidc\Service\ResourceOwnerHookInterface) { + if ($postProcessor instanceof ResourceOwnerHookInterface) { $postProcessor->postProcessUser($mode, $user, $info); $reloadUserRecord = true; } else { - throw new \InvalidArgumentException( + throw new InvalidArgumentException( sprintf( 'Invalid post-processing class %s. It must implement the \\Causal\\Oidc\\Service\\ResourceOwnerHookInterface interface', $className @@ -497,17 +510,17 @@ protected function getUserByUidAndTable(int $uid, string $table): array ->from($table) ->where($queryBuilder->expr()->eq( 'uid', - $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)) + $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)) ) ->execute(); - if ($queryResult instanceof \Doctrine\DBAL\ForwardCompatibility\Result) { + if ($queryResult instanceof Result) { $user = $queryResult->fetchAssociative(); } - if ($user === [] && $queryResult instanceof \Doctrine\DBAL\Driver\Statement) { - $user = $queryResult->fetch(); + if ($user === [] && $queryResult instanceof Statement) { + $user = $queryResult->fetchAssociative(); } if (!is_array($user) || $user === []) { - throw new \LogicException('The user record could not be obtained', 1643452557); + throw new LogicException('The user record could not be obtained', 1643452557); } return $user; } @@ -521,9 +534,8 @@ protected function getUserByUidAndTable(int $uid, string $table): array * @param array $baseData * @param bool $reportErrors * @return array - * @see \Causal\IgLdapSsoAuth\Library\Authentication::merge() */ - protected function applyMapping($table, array $oidc, array $typo3, array $baseData = [], $reportErrors = false) + protected function applyMapping(string $table, array $oidc, array $typo3, array $baseData = [], bool $reportErrors = false): array { $out = array_merge($typo3, $baseData); $typoScriptKeys = []; @@ -535,7 +547,7 @@ protected function applyMapping($table, array $oidc, array $typo3, array $baseDa if ($field !== 'usergroup' && $field !== 'parentGroup') { try { $out = $this->mergeSimple($oidc, $out, $field, $value); - } catch (\UnexpectedValueException $uve) { + } catch (UnexpectedValueException $uve) { if ($reportErrors) { $out['__errors'][] = $uve->getMessage(); } @@ -547,40 +559,26 @@ protected function applyMapping($table, array $oidc, array $typo3, array $baseDa } if (count($typoScriptKeys) > 0) { - $backupTSFE = $GLOBALS['TSFE']; - - // Advanced stdWrap methods require a valid $GLOBALS['TSFE'] => create the most lightweight one - $GLOBALS['TSFE'] = GeneralUtility::makeInstance( - \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::class, - $GLOBALS['TYPO3_CONF_VARS'], - 0, - '' - ); - $GLOBALS['TSFE']->initTemplate(); - $GLOBALS['TSFE']->renderCharset = 'utf-8'; + $backupTSFE = $GLOBALS['TSFE'] ?? null; - /** @var $contentObj \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer */ - $contentObj = GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::class); - $contentObj->start($oidc, ''); + $GLOBALS['TSFE'] = $this->getLocalTSFE(); + + /** @var $contentObj ContentObjectRenderer */ + $contentObj = GeneralUtility::makeInstance(ContentObjectRenderer::class); + $contentObj->start($oidc); // Process every TypoScript definition foreach ($typoScriptKeys as $typoScriptKey) { // Remove the trailing period to get corresponding field name $field = substr($typoScriptKey, 0, -1); - $value = isset($out[$field]) ? $out[$field] : ''; + $value = $out[$field] ?? ''; $value = $contentObj->stdWrap($value, $mapping[$typoScriptKey]); $out = $this->mergeSimple([$field => $value], $out, $field, $value); } - // Instantiation of TypoScriptFrontendController instantiates PageRenderer which - // sets backPath to TYPO3_mainDir which is very bad in the Backend. Therefore, - // we must set it back to null to not get frontend-prefixed asset URLs. - if (ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend()) { - $pageRenderer = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Page\PageRenderer::class); - $pageRenderer->setBackPath(null); + if ($backupTSFE) { + $GLOBALS['TSFE'] = $backupTSFE; } - - $GLOBALS['TSFE'] = $backupTSFE; } return $out; @@ -597,12 +595,10 @@ protected function applyMapping($table, array $oidc, array $typo3, array $baseDa * @param string $field * @param string $value * @return array Modified $typo3 array - * @throws \UnexpectedValueException - * @see \Causal\IgLdapSsoAuth\Library\Authentication::mergeSimple() - * @see \Causal\IgLdapSsoAuth\Library\Authentication::replaceLdapMarkers() + * @throws UnexpectedValueException * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::getFieldVal() */ - protected function mergeSimple(array $oidc, array $typo3, $field, $value) + protected function mergeSimple(array $oidc, array $typo3, string $field, string $value): array { // Constant by default $mappedValue = $value; @@ -651,7 +647,7 @@ protected function mergeSimple(array $oidc, array $typo3, $field, $value) * @param string $table * @return array */ - protected function getMapping($table) + protected function getMapping(string $table): array { $mapping = []; @@ -684,7 +680,7 @@ protected function getMapping($table) * * @return array the raw TypoScript setup */ - protected function getTypoScriptSetup() + protected function getTypoScriptSetup(): array { // This is needed for the PageRepository $files = ['EXT:core/Configuration/TCA/pages.php']; @@ -694,42 +690,75 @@ protected function getTypoScriptSetup() $GLOBALS['TCA'][$table] = include($file); } - /** @var ServerRequestInterface $request */ - $request = $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals(); - /** @var SiteMatcher $siteMatcher */ - $siteMatcher = GeneralUtility::makeInstance(SiteMatcher::class); - $routeResult = $siteMatcher->matchRequest($request); - $site = $routeResult->getSite(); - $pageArguments = $site->getRouter()->matchRequest($request, $routeResult); - $currentPage = $pageArguments->getPageId(); - - $frontendUser = GeneralUtility::makeInstance(FrontendUserAuthentication::class); - $context = GeneralUtility::makeInstance(Context::class); - $localTSFE = GeneralUtility::makeInstance(TypoScriptFrontendController::class, $context, $site, $routeResult->getLanguage(), $pageArguments, $frontendUser); + $localTSFE = $this->getLocalTSFE(); - /** @var TemplateService $templateService */ $templateService = GeneralUtility::makeInstance(TemplateService::class, null, null, $localTSFE); - $rootLine = GeneralUtility::makeInstance(RootlineUtility::class, (int)$currentPage)->get(); + $rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $localTSFE->getPageArguments()->getPageId())->get(); $templateService->start($rootLine); - $setup = $templateService->setup; - return $setup; + return $templateService->setup; } /** * Returns a logger. * - * @return \TYPO3\CMS\Core\Log\Logger + * @return Logger */ - protected static function getLogger() + protected static function getLogger(): Logger { - /** @var \TYPO3\CMS\Core\Log\Logger $logger */ + /** @var Logger $logger */ static $logger = null; if ($logger === null) { - $logger = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Log\LogManager::class)->getLogger(__CLASS__); + $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__); } return $logger; } + protected function generatePassword(): string + { + $mode = ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend() + ? 'FE' + : 'BE'; + $password = substr(str_shuffle('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$'), 0, 20); + + $passwordHashFactory = GeneralUtility::makeInstance(PasswordHashFactory::class); + try { + $objInstanceSaltedPW = $passwordHashFactory->getDefaultHashInstance($mode); + } catch (InvalidPasswordHashException $e) { + return ''; + } + return $objInstanceSaltedPW->getHashedPassword($password); + } + + + protected function getLocalTSFE(): TypoScriptFrontendController + { + /** @var ServerRequestInterface $request */ + $request = $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals(); + $siteMatcher = GeneralUtility::makeInstance(SiteMatcher::class); + $routeResult = $siteMatcher->matchRequest($request); + if ($routeResult instanceof SiteRouteResult) { + $site = $routeResult->getSite(); + if ($site instanceof Site) { + try { + $pageArguments = $site->getRouter()->matchRequest($request, $routeResult); + if ($pageArguments instanceof PageArguments) { + $frontendUser = GeneralUtility::makeInstance(FrontendUserAuthentication::class); + $context = GeneralUtility::makeInstance(Context::class); + return GeneralUtility::makeInstance( + TypoScriptFrontendController::class, + $context, + $site, + $routeResult->getLanguage(), + $pageArguments, + $frontendUser + ); + } + } catch (RouteNotFoundException $e) { + } + } + } + throw new InvalidArgumentException('Failed to initialize TSFE'); + } } diff --git a/composer.json b/composer.json index cb0b611..f9b5c08 100644 --- a/composer.json +++ b/composer.json @@ -1,68 +1,68 @@ { - "name": "causal/oidc", - "type": "typo3-cms-extension", - "description": "This extension uses OpenID Connect to authenticate users.", - "keywords": [ - "TYPO3 CMS", - "OpenID", - "OIDC", - "Authentication" - ], - "homepage": "https://github.com/xperseguers/t3ext-oidc", - "support": { - "issues": "https://github.com/xperseguers/t3ext-oidc/issues" - }, - "authors": [ - { - "name": "Xavier Perseguers", - "email": "xavier@causal.ch", - "homepage": "https://www.causal.ch", - "role": "Developer" - } - ], - "license": "GPL-2.0-or-later", - "require": { - "php": ">= 7.4.0, <= 8.2.99", - "typo3/cms-core": "^11", - "league/oauth2-client": "^2.0" - }, - "autoload": { - "psr-4": { - "Causal\\Oidc\\": "Classes/" - } - }, - "replace": { - "typo3-ter/oidc": "self.version" - }, - "scripts": { - "extension-create-libs": [ - "mkdir -p Libraries/temp", - "[ -f $HOME/.composer/vendor/bin/phar-composer ] || composer global require clue/phar-composer", - "if [ ! -f Libraries/league-oauth2-client.phar ]; then cd Libraries/temp && composer require league/oauth2-client=^2.0 && composer config classmap-authoritative true && composer config prepend-autoloader false && composer dump-autoload; fi", - "[ -f Libraries/league-oauth2-client.phar ] || $HOME/.composer/vendor/bin/phar-composer build Libraries/temp/ Libraries/league-oauth2-client.phar", - "chmod -x Libraries/*.phar", - "rm -rf Libraries/temp" - ], - "extension-build": [ - "@extension-create-libs" - ], - "extension-release": [ - "@extension-build", - "rm -rf Tests/", - "rm .gitattributes", - "rm .gitignore" - ], - "extension-clean": [ - "rm -rf Libraries" - ] - }, - "extra": { - "branch-alias": { - "dev-TYPO3_8-11": "1.2.x-dev", - "dev-master": "1.3.x-dev" - }, - "typo3/cms": { - "extension-key": "oidc" - } - } + "name": "causal/oidc", + "type": "typo3-cms-extension", + "description": "This extension uses OpenID Connect to authenticate users.", + "keywords": [ + "TYPO3 CMS", + "OpenID", + "OIDC", + "Authentication" + ], + "homepage": "https://github.com/xperseguers/t3ext-oidc", + "support": { + "issues": "https://github.com/xperseguers/t3ext-oidc/issues" + }, + "authors": [ + { + "name": "Xavier Perseguers", + "email": "xavier@causal.ch", + "homepage": "https://www.causal.ch", + "role": "Developer" + } + ], + "license": "GPL-2.0-or-later", + "require": { + "php": ">= 7.4.0, <= 8.2.99", + "ext-json": "*", + "typo3/cms-core": "^11", + "league/oauth2-client": "^2.0" + }, + "autoload": { + "psr-4": { + "Causal\\Oidc\\": "Classes/" + } + }, + "replace": { + "typo3-ter/oidc": "self.version" + }, + "scripts": { + "extension-create-libs": [ + "mkdir -p Libraries/temp", + "[ -f $HOME/.composer/vendor/bin/phar-composer ] || composer global require clue/phar-composer", + "if [ ! -f Libraries/league-oauth2-client.phar ]; then cd Libraries/temp && composer require league/oauth2-client=^2.0 && composer config classmap-authoritative true && composer config prepend-autoloader false && composer dump-autoload; fi", + "[ -f Libraries/league-oauth2-client.phar ] || $HOME/.composer/vendor/bin/phar-composer build Libraries/temp/ Libraries/league-oauth2-client.phar", + "chmod -x Libraries/*.phar", + "rm -rf Libraries/temp" + ], + "extension-build": [ + "@extension-create-libs" + ], + "extension-release": [ + "@extension-build", + "rm -rf Tests/", + "rm .gitattributes", + "rm .gitignore" + ], + "extension-clean": [ + "rm -rf Libraries" + ] + }, + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + }, + "typo3/cms": { + "extension-key": "oidc" + } + } } From 30b85af3d6e505bb95aa772df63e410473f63eea Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Mon, 24 Apr 2023 17:03:52 +0200 Subject: [PATCH 057/142] [TASK] Use inherited logger --- Classes/Service/AuthenticationService.php | 74 ++++++++--------------- 1 file changed, 24 insertions(+), 50 deletions(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 512d0ca..c3b7f62 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -18,8 +18,6 @@ namespace Causal\Oidc\Service; use Causal\Oidc\Event\AuthenticationGetUserEvent; -use Doctrine\DBAL\Driver\Statement; -use Doctrine\DBAL\ForwardCompatibility\Result; use InvalidArgumentException; use League\OAuth2\Client\Provider\Exception\IdentityProviderException; use League\OAuth2\Client\Token\AccessToken; @@ -36,8 +34,6 @@ use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction; use TYPO3\CMS\Core\Http\ApplicationType; use TYPO3\CMS\Core\Http\ServerRequestFactory; -use TYPO3\CMS\Core\Log\Logger; -use TYPO3\CMS\Core\Log\LogManager; use TYPO3\CMS\Core\Routing\PageArguments; use TYPO3\CMS\Core\Routing\RouteNotFoundException; use TYPO3\CMS\Core\Routing\SiteMatcher; @@ -163,7 +159,7 @@ public function getUser() */ protected function authenticateWithAuthorizationCode(string $code, string $codeVerifier = null) { - static::getLogger()->debug('Initializing OpenID Connect service'); + $this->logger->debug('Initializing OpenID Connect service'); /** @var OAuthService $service */ $service = GeneralUtility::makeInstance(OAuthService::class); @@ -171,12 +167,12 @@ protected function authenticateWithAuthorizationCode(string $code, string $codeV // Try to get an access token using the authorization code grant try { - static::getLogger()->debug('Retrieving an access token'); + $this->logger->debug('Retrieving an access token'); $accessToken = $service->getAccessToken($code, null, $codeVerifier); - static::getLogger()->debug('Access token retrieved', $accessToken->jsonSerialize()); + $this->logger->debug('Access token retrieved', $accessToken->jsonSerialize()); } catch (IdentityProviderException $e) { // Probably a "server_error", meaning the code is not valid anymore - static::getLogger()->error('Possibly replay: code has been refused by the authentication server', [ + $this->logger->error('Possibly replay: code has been refused by the authentication server', [ 'code' => $code, 'message' => $e->getMessage(), ]); @@ -201,7 +197,7 @@ protected function authenticateWithAuthorizationCode(string $code, string $codeV protected function authenticateWithResourceOwnerPasswordCredentials(string $username, string $password) { $user = false; - static::getLogger()->debug('Initializing OpenID Connect service'); + $this->logger->debug('Initializing OpenID Connect service'); /** @var OAuthService $service */ $service = GeneralUtility::makeInstance(OAuthService::class); @@ -210,18 +206,18 @@ protected function authenticateWithResourceOwnerPasswordCredentials(string $user $accessToken = ''; try { if ($this->config['oidcUseRequestPathAuthentication']) { - static::getLogger()->debug('Retrieving an access token using request path authentication'); + $this->logger->debug('Retrieving an access token using request path authentication'); $accessToken = $service->getAccessTokenWithRequestPathAuthentication($username, $password); } else { - static::getLogger()->debug('Retrieving an access token using resource owner password credentials'); + $this->logger->debug('Retrieving an access token using resource owner password credentials'); $accessToken = $service->getAccessToken($username, $password); } if ($accessToken !== null) { - static::getLogger()->debug('Access token retrieved', $accessToken->jsonSerialize()); + $this->logger->debug('Access token retrieved', $accessToken->jsonSerialize()); $user = $this->getUserFromAccessToken($service, $accessToken); } } catch (IdentityProviderException $e) { - static::getLogger()->error('Authentication has been refused by the authentication server', [ + $this->logger->error('Authentication has been refused by the authentication server', [ 'username' => $username, 'message' => $e->getMessage(), ]); @@ -245,17 +241,17 @@ protected function getUserFromAccessToken(OAuthService $service, AccessToken $ac { // Using the access token, we may look up details about the resource owner try { - static::getLogger()->debug('Retrieving resource owner'); + $this->logger->debug('Retrieving resource owner'); $resourceOwner = $service->getResourceOwner($accessToken)->toArray(); - static::getLogger()->debug('Resource owner retrieved', $resourceOwner); + $this->logger->debug('Resource owner retrieved', $resourceOwner); } catch (IdentityProviderException $e) { - static::getLogger()->error('Could not retrieve resource owner', [ + $this->logger->error('Could not retrieve resource owner', [ 'message' => $e->getMessage(), ]); return false; } if (empty($resourceOwner['sub'])) { - static::getLogger()->error('No "sub" found in resource owner, revoking access token'); + $this->logger->error('No "sub" found in resource owner, revoking access token'); $service->revokeToken($accessToken); throw new RuntimeException( 'Resource owner does not have a sub part: ' . json_encode($resourceOwner) @@ -327,19 +323,19 @@ protected function convertResourceOwner(array $info) if (!empty($row) && $row['deleted'] && !$undeleteUser) { // User was manually deleted, it should not get automatically restored - static::getLogger()->info('User was manually deleted, denying access', ['user' => $row]); + $this->logger->info('User was manually deleted, denying access', ['user' => $row]); return false; } if (!empty($row) && $row['disable'] && !$reEnableUser) { // User was manually disabled, it should not get automatically re-enabled - static::getLogger()->info('User was manually disabled, denying access', ['user' => $row]); + $this->logger->info('User was manually disabled, denying access', ['user' => $row]); return false; } if (empty($row) && $frontendUserMustExistLocally) { // User does not exist locally, it should not be created on-the-fly - static::getLogger()->info('User does not exist locally, denying access', ['info' => $info]); + $this->logger->info('User does not exist locally, denying access', ['info' => $info]); return false; } @@ -426,11 +422,11 @@ protected function convertResourceOwner(array $info) ->getConnectionForTable($userTable); if (!empty($row)) { // fe_users record already exists => update it - static::getLogger()->info('Detected a returning user'); + $this->logger->info('Detected a returning user'); $data['usergroup'] = implode(',', $newUserGroups); $user = array_merge($row, $data); if ($user != $row) { - static::getLogger()->debug('Updating existing user', [ + $this->logger->debug('Updating existing user', [ 'old' => $row, 'new' => $user, ]); @@ -446,11 +442,11 @@ protected function convertResourceOwner(array $info) } else { // fe_users record does not already exist => create it if (empty($newUserGroups)) { // Somehow the user is not mapped to any local user group, we should not create the record - static::getLogger()->info('User has no associated local TYPO3 user group, denying access', ['user' => $row]); + $this->logger->info('User has no associated local TYPO3 user group, denying access', ['user' => $row]); return false; } - static::getLogger()->info('New user detected, creating a TYPO3 user'); + $this->logger->info('New user detected, creating a TYPO3 user'); $data = array_merge($data, [ 'pid' => GeneralUtility::intExplode(',', $this->config['usersStoragePid'], true)[0], 'usergroup' => implode(',', $newUserGroups), @@ -466,7 +462,7 @@ protected function convertResourceOwner(array $info) $user = $this->getUserByUidAndTable((int)$userUid, $userTable); } - static::getLogger()->debug('Authentication user record processed', $user); + $this->logger->debug('Authentication user record processed', $user); // Hook for post-processing the user record $reloadUserRecord = false; @@ -491,7 +487,7 @@ protected function convertResourceOwner(array $info) if ($reloadUserRecord) { $user = $this->getUserByUidAndTable((int)$user['uid'], $userTable); - static::getLogger()->debug('User record reloaded', $user); + $this->logger->debug('User record reloaded', $user); } // We need that for the upcoming call to authUser() @@ -502,7 +498,6 @@ protected function convertResourceOwner(array $info) protected function getUserByUidAndTable(int $uid, string $table): array { - $user = []; $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); $queryBuilder->getRestrictions()->removeAll(); $queryResult = $queryBuilder @@ -513,12 +508,8 @@ protected function getUserByUidAndTable(int $uid, string $table): array $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)) ) ->execute(); - if ($queryResult instanceof Result) { - $user = $queryResult->fetchAssociative(); - } - if ($user === [] && $queryResult instanceof Statement) { - $user = $queryResult->fetchAssociative(); - } + + $user = $queryResult->fetchAssociative(); if (!is_array($user) || $user === []) { throw new LogicException('The user record could not be obtained', 1643452557); } @@ -699,22 +690,6 @@ protected function getTypoScriptSetup(): array return $templateService->setup; } - /** - * Returns a logger. - * - * @return Logger - */ - protected static function getLogger(): Logger - { - /** @var Logger $logger */ - static $logger = null; - if ($logger === null) { - $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__); - } - - return $logger; - } - protected function generatePassword(): string { $mode = ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend() @@ -731,7 +706,6 @@ protected function generatePassword(): string return $objInstanceSaltedPW->getHashedPassword($password); } - protected function getLocalTSFE(): TypoScriptFrontendController { /** @var ServerRequestInterface $request */ From a1f92d0d622625edadb79a085c9042b1f45e0d6f Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Wed, 3 Nov 2021 16:23:22 +0100 Subject: [PATCH 058/142] [FEATURE] Merge with existing fe_user if username matches email --- Classes/Service/AuthenticationService.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 512d0ca..b18dc01 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -316,7 +316,10 @@ protected function convertResourceOwner(array $info) GeneralUtility::intExplode(',', $this->config['usersStoragePid']), Connection::PARAM_INT_ARRAY )), - $queryBuilder->expr()->eq('tx_oidc', $queryBuilder->createNamedParameter($info['sub'])) + $queryBuilder->expr()->orX( + $queryBuilder->expr()->eq('tx_oidc', $queryBuilder->createNamedParameter($info['sub'])), + $queryBuilder->expr()->eq('username', $queryBuilder->createNamedParameter($info['email'])) + ) ) ->execute() ->fetchAssociative(); @@ -349,12 +352,20 @@ protected function convertResourceOwner(array $info) $info, $row ?: [], [ + 'tx_oidc' => $info['sub'], 'password' => $this->generatePassword(), 'deleted' => 0, 'disable' => 0, ] ); + // preserve username and password for existing users + if ($row) { + unset($data['username']); + unset($data['email']); + unset($data['password']); + } + $newUserGroups = []; $defaultUserGroups = GeneralUtility::intExplode(',', $this->config['usersDefaultGroup']); From 8833c54e5ee9ca64c219e4c2a4c3aab11c59c479 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Mon, 24 Apr 2023 17:18:58 +0200 Subject: [PATCH 059/142] [TASK] Remove old felogin support --- Classes/Hooks/FeloginHook.php | 156 ---------------------------------- ext_localconf.php | 4 - 2 files changed, 160 deletions(-) delete mode 100644 Classes/Hooks/FeloginHook.php diff --git a/Classes/Hooks/FeloginHook.php b/Classes/Hooks/FeloginHook.php deleted file mode 100644 index 0a4e22d..0000000 --- a/Classes/Hooks/FeloginHook.php +++ /dev/null @@ -1,156 +0,0 @@ -getUniqueId(); - static::getLogger()->debug('Post-processing markers for felogin form', ['request' => $requestId]); - $markerArray['###OPENID_CONNECT###'] = ''; - - // TODO: Use proper TYPO3 API - $settings = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['oidc'] ?? []; - - if (empty($settings['oidcClientKey']) - || empty($settings['oidcClientSecret']) - || empty($settings['oidcEndpointAuthorize']) - || empty($settings['oidcEndpointToken']) - ) { - $markerArray['###OPENID_CONNECT###'] = 'Invalid OpenID Connect configuration'; - } else { - if (session_id() === '') { // If no session exists, start a new one - static::getLogger()->debug('No PHP session found'); - session_start(); - } - - if (empty($_SESSION['requestId']) || $_SESSION['requestId'] !== $requestId) { - $this->prepareAuthorizationUrl($settings); - $_SESSION['requestId'] = $requestId; - $_SESSION['oidc_redirect_url'] = GeneralUtility::_GP('redirect_url') ?? GeneralUtility::getIndpEnv('HTTP_REFERER') ?? ''; - - static::getLogger()->debug('PHP session is available', [ - 'id' => session_id(), - 'data' => $_SESSION, - ]); - } else { - static::getLogger()->debug('Reusing same authorization URL and state'); - } - - $wrap = $pObj->conf['oidc.']; - $linkTag = $pObj->cObj->stdWrap($_SESSION['oidc_authorization_url'], $wrap); - - $markerArray['###OPENID_CONNECT###'] = $linkTag; - } - - /** @var \TYPO3\CMS\Core\Service\MarkerBasedTemplateService $templateService */ - $templateService = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Service\MarkerBasedTemplateService::class); - $content = $templateService->substituteMarkerArrayCached($params['content'], $markerArray); - - return $content; - } - - /** - * Prepares the authorization URL and corresponding expected state (to mitigate CSRF attack) - * and stores information into the session. - * - * @param array $settings - * @return void - */ - protected function prepareAuthorizationUrl(array $settings) - { - /** @var \Causal\Oidc\Service\OAuthService $service */ - $service = GeneralUtility::makeInstance(\Causal\Oidc\Service\OAuthService::class); - $service->setSettings($settings); - $authorizationUrl = $service->getAuthorizationUrl(); - - // Store the state - $state = $service->getState(); - - static::getLogger()->debug('Generating authorization URL', [ - 'url' => $authorizationUrl, - 'state' => $state, - ]); - - $loginUrl = GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL'); - // Sanitize the URL - $parts = parse_url($loginUrl); - $queryParts = array_filter(explode('&', $parts['query']), function ($v) { - list ($k,) = explode('=', $v, 2); - - return !in_array($k, ['logintype', 'tx_oidc[code]']); - }); - $parts['query'] = implode('&', $queryParts); - $loginUrl = $parts['scheme'] . '://' . $parts['host']; - if (!empty($parts['port']) && !in_array((int)$parts['port'], [80, 443], true)) { - $loginUrl .= ':' . $parts['port']; - } - $loginUrl .= $parts['path']; - if (!empty($parts['query'])) { - $loginUrl .= '?' . $parts['query']; - } - - $_SESSION['oidc_state'] = $state; - $_SESSION['oidc_login_url'] = $loginUrl; - $_SESSION['oidc_authorization_url'] = $authorizationUrl; - } - - /** - * Returns a unique ID for the current processed request. - * - * This is supposed to be independent of the actual web server (Nginx or Apache) and - * the way PHP was built and unique enough for our use case, as opposed to using: - * - * - zend_thread_id() which requires PHP to be built with Zend Thread Safety - ZTS - support and debug mode - * - apache_getenv('UNIQUE_ID') which requires Apache as web server and mod_unique_id - * - * @return string - */ - protected function getUniqueId() - { - $uniqueId = sprintf('%08x', abs(crc32($_SERVER['REMOTE_ADDR'] . $_SERVER['REQUEST_TIME'] . $_SERVER['REMOTE_PORT']))); - - return $uniqueId; - } - - /** - * Returns a logger. - * - * @return \TYPO3\CMS\Core\Log\Logger - */ - protected static function getLogger() - { - /** @var \TYPO3\CMS\Core\Log\Logger $logger */ - static $logger = null; - if ($logger === null) { - $logger = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Log\LogManager::class)->getLogger(__CLASS__); - } - - return $logger; - } - -} diff --git a/ext_localconf.php b/ext_localconf.php index 660f0b6..9860ba2 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -54,10 +54,6 @@ class_alias(\TYPO3\CMS\Core\Authentication\AuthenticationService::class, \TYPO3\ ] ); - if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('felogin')) { - $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['postProcContent'][$_EXTKEY] = \Causal\Oidc\Hooks\FeloginHook::class . '->postProcContent'; - } - // Add typoscript for custom login plugin \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPItoST43('oidc', null, '_login'); From fa60599b7c8e0cbd3efa47b0eb4408211eec8ed5 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Mon, 24 Apr 2023 17:33:09 +0200 Subject: [PATCH 060/142] [TASK] Use logging via container --- .../Controller/AuthenticationController.php | 56 ++++++++----------- .../FrontendLoginEventListener.php | 38 ++++--------- 2 files changed, 33 insertions(+), 61 deletions(-) diff --git a/Classes/Controller/AuthenticationController.php b/Classes/Controller/AuthenticationController.php index 52b2fd8..c12b41b 100644 --- a/Classes/Controller/AuthenticationController.php +++ b/Classes/Controller/AuthenticationController.php @@ -1,4 +1,7 @@ globalSettings = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['oidc'] ?? []; + $this->globalSettings = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('oidc') ?? []; } /** @@ -47,30 +52,30 @@ public function initializeAction() */ public function connectAction() { - static::getLogger()->debug('Initiating the silent authentication'); + $this->logger->debug('Initiating the silent authentication'); if ((empty($_GET['state']) || empty($_GET['code']))) { - static::getLogger()->error('No state or code detected', ['GET' => $_GET]); - throw new \RuntimeException('No state or code detected', 1487001047); + $this->logger->error('No state or code detected', ['GET' => $_GET]); + throw new RuntimeException('No state or code detected', 1487001047); } if (session_id() === '') { - static::getLogger()->debug('No PHP session found'); + $this->logger->debug('No PHP session found'); session_start(); } - static::getLogger()->debug('PHP session is available', [ + $this->logger->debug('PHP session is available', [ 'id' => session_id(), 'data' => $_SESSION, ]); if ($_GET['state'] !== ($_SESSION['oidc_state'] ?? null)) { - static::getLogger()->error('Invalid returning state detected', [ + $this->logger->error('Invalid returning state detected', [ 'expected' => $_SESSION['oidc_state'] ?? null, 'actual' => $_GET['state'], ]); - if (!(bool)$this->globalSettings['oidcDisableCSRFProtection']) { - throw new \RuntimeException('Invalid state', 1489658206); + if (!$this->globalSettings['oidcDisableCSRFProtection']) { + throw new RuntimeException('Invalid state', 1489658206); } - static::getLogger()->warning('Bypassing CSRF attack mitigation protection according to the extension configuration'); + $this->logger->warning('Bypassing CSRF attack mitigation protection according to the extension configuration'); } $loginUrl = $_SESSION['oidc_login_url']; @@ -80,24 +85,7 @@ public function connectAction() $loginUrl .= '&redirect_url=' . urlencode($_SESSION['oidc_redirect_url']); } - static::getLogger()->info('Redirecting to login URL', ['url' => $loginUrl]); + $this->logger->info('Redirecting to login URL', ['url' => $loginUrl]); $this->redirectToUri($loginUrl); } - - /** - * Returns a logger. - * - * @return \TYPO3\CMS\Core\Log\Logger - */ - protected static function getLogger() - { - /** @var \TYPO3\CMS\Core\Log\Logger $logger */ - static $logger = null; - if ($logger === null) { - $logger = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Log\LogManager::class)->getLogger(__CLASS__); - } - - return $logger; - } - } diff --git a/Classes/EventListener/FrontendLoginEventListener.php b/Classes/EventListener/FrontendLoginEventListener.php index 6cb5f8e..0ae4dd5 100644 --- a/Classes/EventListener/FrontendLoginEventListener.php +++ b/Classes/EventListener/FrontendLoginEventListener.php @@ -18,14 +18,16 @@ namespace Causal\Oidc\EventListener; use Causal\Oidc\Service\OAuthService; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; -use TYPO3\CMS\Core\Log\Logger; -use TYPO3\CMS\Core\Log\LogManager; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\FrontendLogin\Event\ModifyLoginFormViewEvent; -class FrontendLoginEventListener +class FrontendLoginEventListener implements LoggerAwareInterface { + use LoggerAwareTrait; + public function modifyLoginFormView(ModifyLoginFormViewEvent $event): void { $settings = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('oidc') ?? []; @@ -39,10 +41,10 @@ public function modifyLoginFormView(ModifyLoginFormViewEvent $event): void } $requestId = $this->getUniqueId(); - static::getLogger()->debug('Post-processing felogin form', ['request' => $requestId]); + $this->logger->debug('Post-processing felogin form', ['request' => $requestId]); if (session_id() === '') { // If no session exists, start a new one - static::getLogger()->debug('No PHP session found'); + $this->logger->debug('No PHP session found'); session_start(); } @@ -51,12 +53,12 @@ public function modifyLoginFormView(ModifyLoginFormViewEvent $event): void $_SESSION['requestId'] = $requestId; $_SESSION['oidc_redirect_url'] = GeneralUtility::_GP('redirect_url'); - static::getLogger()->debug('PHP session is available', [ + $this->logger->debug('PHP session is available', [ 'id' => session_id(), 'data' => $_SESSION, ]); } else { - static::getLogger()->debug('Reusing same authorization URL and state'); + $this->logger->debug('Reusing same authorization URL and state'); } $event->getView()->assign('openidConnectUri', $_SESSION['oidc_authorization_url']); @@ -78,7 +80,7 @@ protected function prepareAuthorizationUrl(array $settings): void // Store the state $state = $service->getState(); - static::getLogger()->debug('Generating authorization URL', [ + $this->logger->debug('Generating authorization URL', [ 'url' => $authorizationUrl, 'state' => $state, ]); @@ -119,24 +121,6 @@ protected function prepareAuthorizationUrl(array $settings): void */ protected function getUniqueId(): string { - $uniqueId = sprintf('%08x', abs(crc32($_SERVER['REMOTE_ADDR'] . $_SERVER['REQUEST_TIME'] . $_SERVER['REMOTE_PORT']))); - - return $uniqueId; - } - - /** - * Returns a logger. - * - * @return Logger - */ - protected static function getLogger(): Logger - { - /** @var Logger $logger */ - static $logger = null; - if ($logger === null) { - $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__); - } - - return $logger; + return sprintf('%08x', abs(crc32($_SERVER['REMOTE_ADDR'] . $_SERVER['REQUEST_TIME'] . $_SERVER['REMOTE_PORT']))); } } From 4692ba12552c8338fd19fbfdadf4188ce6dbf569 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Mon, 24 Apr 2023 17:35:08 +0200 Subject: [PATCH 061/142] [TASK] Use Core ExtensionConfiguration class Remove direct access to globals. --- Classes/Controller/LoginController.php | 5 ++--- Classes/Service/AuthenticationService.php | 6 +++--- Classes/ViewHelpers/OidcLinkViewHelper.php | 5 ++--- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php index 94bc6a0..86957d2 100644 --- a/Classes/Controller/LoginController.php +++ b/Classes/Controller/LoginController.php @@ -14,10 +14,10 @@ namespace Causal\Oidc\Controller; +use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; use TYPO3\CMS\Core\Http\PropagateResponseException; use TYPO3\CMS\Core\Http\RedirectResponse; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Core\Utility\HttpUtility; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; class LoginController @@ -43,8 +43,7 @@ class LoginController public function __construct() { - // TODO: Use proper TYPO3 API - $this->settings = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['oidc'] ?? []; + $this->settings = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('oidc') ?? []; } public function setContentObjectRenderer(ContentObjectRenderer $cObj) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index c3b7f62..bf8e8ea 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -25,6 +25,7 @@ use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ServerRequestInterface; use RuntimeException; +use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException; use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory; use TYPO3\CMS\Core\Database\Connection; @@ -81,15 +82,14 @@ class AuthenticationService extends \TYPO3\CMS\Core\Authentication\Authenticatio * * @var array */ - protected $config; + protected array $config; /** * AuthenticationService constructor. */ public function __construct() { - // TODO: Use proper TYPO3 API - $this->config = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['oidc'] ?? []; + $this->config = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('oidc') ?? []; } protected function getCodeVerifierFromSession() diff --git a/Classes/ViewHelpers/OidcLinkViewHelper.php b/Classes/ViewHelpers/OidcLinkViewHelper.php index d0ddfa7..3c2f936 100644 --- a/Classes/ViewHelpers/OidcLinkViewHelper.php +++ b/Classes/ViewHelpers/OidcLinkViewHelper.php @@ -14,7 +14,7 @@ namespace Causal\Oidc\ViewHelpers; -use TYPO3\CMS\Core\Information\Typo3Version; +use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; use TYPO3\CMS\Core\Log\Logger; use TYPO3\CMS\Core\Log\LogManager; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -40,8 +40,7 @@ public static function renderStatic(array $arguments, \Closure $renderChildrenCl static::getLogger()->debug('Post-processing markers for felogin form', ['request' => $requestId]); $link = ''; - // TODO: Use proper TYPO3 API - $settings = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['oidc'] ?? []; + $settings = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('oidc') ?? []; if (empty($settings['oidcClientKey']) || empty($settings['oidcClientSecret']) From 28dcef62233d89f3437d999c84b348ae1c35a5e5 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Mon, 24 Apr 2023 17:21:09 +0200 Subject: [PATCH 062/142] [TASK] Cleanup extension config --- ext_emconf.php | 27 ------------ ext_localconf.php | 109 +++++++++++++++++++++++----------------------- ext_tables.php | 5 +-- ext_tables.sql | 12 ++--- 4 files changed, 59 insertions(+), 94 deletions(-) delete mode 100644 ext_emconf.php diff --git a/ext_emconf.php b/ext_emconf.php deleted file mode 100644 index 4e248d0..0000000 --- a/ext_emconf.php +++ /dev/null @@ -1,27 +0,0 @@ - 'OpenID Connect Authentication', - 'description' => 'This extension uses OpenID Connect to authenticate users.', - 'category' => 'services', - 'author' => 'Xavier Perseguers', - 'author_company' => 'Causal Sàrl', - 'author_email' => 'xavier@causal.ch', - 'state' => 'stable', - 'uploadfolder' => 0, - 'createDirs' => '', - 'modify_tables' => '', - 'clearCacheOnLoad' => 0, - 'version' => '1.3.0-dev', - 'constraints' => [ - 'depends' => [ - 'php' => '7.4.0-8.2.99', - 'typo3' => '11.5.0-11.5.99', - ], - 'conflicts' => [], - 'suggests' => [], - ], - 'autoload' => [ - 'psr-4' => ['Causal\\Oidc\\' => 'Classes'] - ], -]; - diff --git a/ext_localconf.php b/ext_localconf.php index 9860ba2..d1e2d34 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -1,65 +1,64 @@ get('oidc') ?? []; - // Service configuration - $subTypesArr = []; - $subTypes = ''; - if ((bool)($settings['enableFrontendAuthentication'] ?? '')) { - $subTypesArr[] = 'getUserFE'; - $subTypesArr[] = 'authUserFE'; - $subTypesArr[] = 'getGroupsFE'; - } - if (is_array($subTypesArr)) { - $subTypesArr = array_unique($subTypesArr); - $subTypes = implode(',', $subTypesArr); - } +// Service configuration +$subTypesArr = []; +$subTypes = ''; +if ($settings['enableFrontendAuthentication'] ?? '') { + $subTypesArr[] = 'getUserFE'; + $subTypesArr[] = 'authUserFE'; + $subTypesArr[] = 'getGroupsFE'; +} +if (is_array($subTypesArr)) { + $subTypesArr = array_unique($subTypesArr); + $subTypes = implode(',', $subTypesArr); +} - $authenticationClassName = \Causal\Oidc\Service\AuthenticationService::class; - \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addService( - $_EXTKEY, - 'auth' /* sv type */, - $authenticationClassName /* sv key */, - [ - 'title' => 'Authentication service', - 'description' => 'Authentication service for OpenID Connect.', - 'subtype' => $subTypes, - 'available' => true, - 'priority' => 82, /* will be called before default TYPO3 authentication service */ - 'quality' => 80, - 'os' => '', - 'exec' => '', - 'className' => $authenticationClassName, - ] - ); +$authenticationClassName = AuthenticationService::class; +ExtensionManagementUtility::addService( + 'oidc', + 'auth' /* sv type */, + $authenticationClassName /* sv key */, + [ + 'title' => 'Authentication service', + 'description' => 'Authentication service for OpenID Connect.', + 'subtype' => $subTypes, + 'available' => true, + 'priority' => 82, /* will be called before default TYPO3 authentication service */ + 'quality' => 80, + 'os' => '', + 'exec' => '', + 'className' => $authenticationClassName, + ] +); - \TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin( - $_EXTKEY, - 'Pi1', - [ - \Causal\Oidc\Controller\AuthenticationController::class => 'connect', - ], - // non-cacheable actions - [ - \Causal\Oidc\Controller\AuthenticationController::class => 'connect' - ] - ); +ExtensionUtility::configurePlugin( + 'oidc', + 'Pi1', + [ + AuthenticationController::class => 'connect', + ], + // non-cacheable actions + [ + AuthenticationController::class => 'connect' + ] +); // Add typoscript for custom login plugin \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPItoST43('oidc', null, '_login'); - // Require 3rd-party libraries, in case TYPO3 does not run in composer mode - $pharFileName = \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath($_EXTKEY) . 'Libraries/league-oauth2-client.phar'; - if (is_file($pharFileName)) { - @include 'phar://' . $pharFileName . '/vendor/autoload.php'; - } -})('oidc'); +// Require 3rd-party libraries, in case TYPO3 does not run in composer mode +$pharFileName = ExtensionManagementUtility::extPath('oidc') . 'Libraries/league-oauth2-client.phar'; +if (is_file($pharFileName)) { + @include 'phar://' . $pharFileName . '/vendor/autoload.php'; +} diff --git a/ext_tables.php b/ext_tables.php index 85e44e8..5e11f75 100644 --- a/ext_tables.php +++ b/ext_tables.php @@ -1,7 +1,4 @@ Date: Mon, 24 Apr 2023 18:26:20 +0200 Subject: [PATCH 063/142] [TASK] Various minor cleanups --- Classes/Controller/LoginController.php | 21 ++- Classes/Event/AuthenticationGetUserEvent.php | 3 + .../Factory/GenericOAuthProviderFactory.php | 6 +- .../Factory/OAuthProviderFactoryInterface.php | 3 + Classes/Hooks/DataHandler.php | 5 +- Classes/Service/OAuthService.php | 62 +++---- Classes/ViewHelpers/OidcLinkViewHelper.php | 4 +- Configuration/TCA/Overrides/fe_groups.php | 2 +- Configuration/TCA/Overrides/fe_users.php | 7 +- .../TypoScript/felogin/constants.typoscript | 4 +- .../TypoScript/felogin/setup.typoscript | 6 - .../Private/Templates/FrontendLogin.html | 174 ------------------ Resources/Public/callback.php | 6 +- composer.json | 1 + 14 files changed, 69 insertions(+), 235 deletions(-) delete mode 100644 Resources/Private/Templates/FrontendLogin.html diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php index 86957d2..1a84810 100644 --- a/Classes/Controller/LoginController.php +++ b/Classes/Controller/LoginController.php @@ -1,4 +1,7 @@ pluginConfiguration = $pluginConfiguration; @@ -77,8 +81,8 @@ public function login($_ = '', $pluginConfiguration) protected function performRedirectToLogin(array $authorizationUrlOptions = []) { - /** @var \Causal\Oidc\Service\OAuthService $service */ - $service = GeneralUtility::makeInstance(\Causal\Oidc\Service\OAuthService::class); + /** @var OAuthService $service */ + $service = GeneralUtility::makeInstance(OAuthService::class); $service->setSettings($this->settings); if (session_id() === '') { @@ -134,7 +138,7 @@ protected function generateCodeVerifier(): string return bin2hex(random_bytes(64)); } - protected function convertVerifierToChallenge($codeVerifier) + protected function convertVerifierToChallenge($codeVerifier): string { return rtrim(strtr(base64_encode(hash('sha256', $codeVerifier, true)), '+/', '-_'), '='); } @@ -149,5 +153,4 @@ protected function addCodeChallengeToOptions($codeChallenge, array $options = [] ] ); } - } diff --git a/Classes/Event/AuthenticationGetUserEvent.php b/Classes/Event/AuthenticationGetUserEvent.php index b7c32cf..1200978 100644 --- a/Classes/Event/AuthenticationGetUserEvent.php +++ b/Classes/Event/AuthenticationGetUserEvent.php @@ -1,4 +1,7 @@ $settings['oidcClientKey'], 'clientSecret' => $settings['oidcClientSecret'], 'redirectUri' => $settings['oidcRedirectUri'], diff --git a/Classes/Factory/OAuthProviderFactoryInterface.php b/Classes/Factory/OAuthProviderFactoryInterface.php index fb18e92..d45ab14 100644 --- a/Classes/Factory/OAuthProviderFactoryInterface.php +++ b/Classes/Factory/OAuthProviderFactoryInterface.php @@ -1,4 +1,7 @@ expr()->inSet('usergroup', (string)$id) ) ->execute() - ->fetchAll(); + ->fetchAllAssociative(); $tableConnection = GeneralUtility::makeInstance(ConnectionPool::class) ->getConnectionForTable('fe_users'); diff --git a/Classes/Service/OAuthService.php b/Classes/Service/OAuthService.php index 3c47e0a..f41d797 100644 --- a/Classes/Service/OAuthService.php +++ b/Classes/Service/OAuthService.php @@ -1,4 +1,7 @@ settings['oidcAuthorizeLanguageParameter'])) { $languageOption = $this->settings['oidcAuthorizeLanguageParameter']; @@ -65,9 +69,7 @@ public function getAuthorizationUrl(array $options = []) } } - $authorizationUrl = $this->getProvider()->getAuthorizationUrl($options); - - return $authorizationUrl; + return $this->getProvider()->getAuthorizationUrl($options); } /** @@ -76,7 +78,7 @@ public function getAuthorizationUrl(array $options = []) * @return string * @see getAuthorizationUrl() */ - public function getState() + public function getState(): string { return $this->getProvider()->getState(); } @@ -86,12 +88,12 @@ public function getState() * credentials grant. * * @param string $codeOrUsername Either a code or the username (if password is provided) - * @param null $password Optional parameter if authenticating with authorization code grant - * @param null $codeVerifier Code verifier for PKCE + * @param string|null $password Optional parameter if authenticating with authorization code grant + * @param string|null $codeVerifier Code verifier for PKCE * @return AccessToken - * @throws \League\OAuth2\Client\Provider\Exception\IdentityProviderException + * @throws IdentityProviderException */ - public function getAccessToken($codeOrUsername, $password = null, $codeVerifier = null) + public function getAccessToken(string $codeOrUsername, ?string $password = null, ?string $codeVerifier = null): AccessToken { if ($password === null) { $options = ['code' => $codeOrUsername]; @@ -121,7 +123,7 @@ public function getAccessToken($codeOrUsername, $password = null, $codeVerifier * @param string $password * @return AccessToken */ - public function getAccessTokenWithRequestPathAuthentication($username, $password) + public function getAccessTokenWithRequestPathAuthentication(string $username, string $password): ?AccessToken { $redirectUri = $this->settings['oidcRedirectUri']; if (empty($redirectUri)) { @@ -147,7 +149,7 @@ public function getAccessTokenWithRequestPathAuthentication($username, $password $content = curl_exec($ch); if ($content === false) { - throw new \RuntimeException('Curl ERROR: ' . curl_error($ch), 1510049345); + throw new RuntimeException('Curl ERROR: ' . curl_error($ch), 1510049345); } curl_close($ch); @@ -172,9 +174,9 @@ public function getAccessTokenWithRequestPathAuthentication($username, $password * Returns the resource owner. * * @param AccessToken $token - * @return \League\OAuth2\Client\Provider\ResourceOwnerInterface + * @return ResourceOwnerInterface */ - public function getResourceOwner(AccessToken $token) + public function getResourceOwner(AccessToken $token): ResourceOwnerInterface { return $this->getProvider()->getResourceOwner($token); } @@ -185,7 +187,7 @@ public function getResourceOwner(AccessToken $token) * @param AccessToken $token * @return bool */ - public function revokeToken(AccessToken $token) + public function revokeToken(AccessToken $token): bool { if (empty($this->settings['oidcEndpointRevoke'])) { return false; @@ -193,7 +195,7 @@ public function revokeToken(AccessToken $token) $provider = $this->getProvider(); $request = $provider->getRequest( - \League\OAuth2\Client\Provider\AbstractProvider::METHOD_POST, + AbstractProvider::METHOD_POST, $this->settings['oidcEndpointRevoke'], [ 'headers' => [ @@ -203,22 +205,19 @@ public function revokeToken(AccessToken $token) 'body' => 'token=' . $token->getToken(), ] ); + $response = $provider->getParsedResponse($request); + // TODO error handling? return true; } - /** - * Returns the OAuth client provider. - * - * @return \League\OAuth2\Client\Provider\GenericProvider - */ - protected function getProvider() + protected function getProvider(): AbstractProvider { if ($this->provider === null) { $factoryClass = $this->settings['oauthProviderFactory'] ?: GenericOAuthProviderFactory::class; if (!is_a($factoryClass, OAuthProviderFactoryInterface::class, true)) { - throw new \RuntimeException('OAuth provider factory class must implement the OAuthProviderFactoryInterface', 1652689564769); + throw new RuntimeException('OAuth provider factory class must implement the OAuthProviderFactoryInterface', 1652689564769); } $settings = $this->settings; @@ -238,7 +237,7 @@ protected function getProvider() return $this->provider; } - public function getFreshAccessToken() + public function getFreshAccessToken(): ?AccessToken { $serializedToken = $this->settings['access_token']; $options = json_decode($serializedToken, true); @@ -246,7 +245,7 @@ public function getFreshAccessToken() // Invalid token return null; } - $accessToken = new \League\OAuth2\Client\Token\AccessToken($options); + $accessToken = new AccessToken($options); if ($accessToken->hasExpired()) { try { @@ -255,7 +254,7 @@ public function getFreshAccessToken() ]); // TODO - } catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) { + } catch (IdentityProviderException $e) { // TODO: log problem return null; } @@ -263,5 +262,4 @@ public function getFreshAccessToken() return $accessToken; } - } diff --git a/Classes/ViewHelpers/OidcLinkViewHelper.php b/Classes/ViewHelpers/OidcLinkViewHelper.php index 3c2f936..8910124 100644 --- a/Classes/ViewHelpers/OidcLinkViewHelper.php +++ b/Classes/ViewHelpers/OidcLinkViewHelper.php @@ -1,4 +1,7 @@ debug('Post-processing markers for felogin form', ['request' => $requestId]); - $link = ''; $settings = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('oidc') ?? []; diff --git a/Configuration/TCA/Overrides/fe_groups.php b/Configuration/TCA/Overrides/fe_groups.php index 0938cd5..96f6d43 100644 --- a/Configuration/TCA/Overrides/fe_groups.php +++ b/Configuration/TCA/Overrides/fe_groups.php @@ -3,7 +3,7 @@ $tempColumns = [ 'tx_oidc_pattern' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:fe_groups.tx_oidc_pattern', 'config' => [ 'type' => 'input', diff --git a/Configuration/TCA/Overrides/fe_users.php b/Configuration/TCA/Overrides/fe_users.php index f5f64aa..c5f5ace 100644 --- a/Configuration/TCA/Overrides/fe_users.php +++ b/Configuration/TCA/Overrides/fe_users.php @@ -1,17 +1,16 @@ get('oidc') ?? []; $tempColumns = [ 'tx_oidc' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:fe_users.tx_oidc', 'config' => [ 'type' => 'input', 'size' => 30, - 'readOnly' => (bool)($settings['frontendUserMustExistLocally'] ?? '') ? 0 : 1, + 'readOnly' => !($settings['frontendUserMustExistLocally'] ?? ''), ] ], ]; diff --git a/Configuration/TypoScript/felogin/constants.typoscript b/Configuration/TypoScript/felogin/constants.typoscript index 4395a31..85b87a6 100644 --- a/Configuration/TypoScript/felogin/constants.typoscript +++ b/Configuration/TypoScript/felogin/constants.typoscript @@ -1,4 +1,2 @@ -plugin.tx_felogin_login.view.templateRootPath = EXT:oidc/Resources/Private/Templates/ -# Old EXT:felogin template (TYPO3 v9) -styles.content.loginform.templateFile = EXT:oidc/Resources/Private/Templates/FrontendLogin.html +plugin.tx_felogin_login.view.templateRootPath = EXT:oidc/Resources/Private/Templates/ diff --git a/Configuration/TypoScript/felogin/setup.typoscript b/Configuration/TypoScript/felogin/setup.typoscript index ea8fd73..e69de29 100644 --- a/Configuration/TypoScript/felogin/setup.typoscript +++ b/Configuration/TypoScript/felogin/setup.typoscript @@ -1,6 +0,0 @@ -# deprecated -plugin.tx_felogin_pi1 { - oidc { - wrap = OpenID Connect - } -} diff --git a/Resources/Private/Templates/FrontendLogin.html b/Resources/Private/Templates/FrontendLogin.html deleted file mode 100644 index 4059e19..0000000 --- a/Resources/Private/Templates/FrontendLogin.html +++ /dev/null @@ -1,174 +0,0 @@ - - - - Templates for felogin - - - -

Common markers

-

These are substituted in all felogin item displaying templates.

- -

Markers

-
    -
  • ###ACTION_URI### - URI of the request for the login/logout form
  • -
  • ###EXTRA_HIDDEN### - Hook required (additional hidden field used by kb_md5fepw extension by Kraft Bernhard)
  • -
  • ###ON_SUBMIT### - Hook required (used by kb_md5fepw extension by Kraft Bernhard)
  • -
  • ###PREFIXID### - Same as class name ('tx_felogin_pi1') useful to get a unique classname prefix
  • -
  • ###REDIRECT_URL### - URL of redirection upon login
  • -
  • ###NOREDIRECT### - if set, no redirect will be done
  • -
  • ###STORAGE_PID### - explicit enough I guess (if not : id of the page where user are stored)
  • -
  • ###STATUS_HEADER### - depends of the template
  • -
  • ###STATUS_MESSAGE### - depends of the template
  • -
  • ###OPENID_CONNECT### - Link to authenticate using OpenID Connect
  • -
- -

Wrap parts

-
    -
  • ###HEADER_VALID### - useful to define what to show/hide
  • -
  • ###MESSAGE_VALID### - useful to define what to show/hide
  • -
  • ###FORGOTP_VALID### - useful to define what to show/hide
  • -
  • ###PERMALOGIN_VALID### - useful to define what to show/hide
  • -
- -

felogin Language Markers (see pi/locallang.xlf)

-
    -
  • ###EMAIL_LABEL### - corresponding to 'your_email'
  • -
  • ###FORGOT_PASSWORD### - corresponding to 'forgot_password'
  • -
  • ###FORGOT_PASSWORD_BACKTOLOGIN### - corresponding to 'forgot_password_backToLogin'
  • -
  • ###FORGOT_PASSWORD_ENTEREMAIL### - corresponding to 'forgot_password_enterEmail'
  • -
  • ###LOGIN_LABEL### - corresponding to 'login'
  • -
  • ###PASSWORD_LABEL### - corresponding to 'password'
  • -
  • ###SEND_PASSWORD### - corresponding to 'send_password'
  • -
  • ###USERNAME_LABEL### - corresponding to 'username'
  • -
- - - -###STATUS_HEADER### -###STATUS_MESSAGE### - -
-
- ###LEGEND### -
- - -
-
- - -
-
- ###OPENID_CONNECT### -
- - -
- - - -
- - -
- -
- -
- - - - - ###EXTRA_HIDDEN### -
-
-
- - -

###FORGOT_PASSWORD###

- - - - - - - - -###STATUS_HEADER### -###STATUS_MESSAGE### - -
-
- ###LEGEND### -
- - ###USERNAME### -
-
- -
- -
- - - -
-
-
- - - - - -###STATUS_HEADER### -###STATUS_MESSAGE### - - - -
-
- -
-
- ###LEGEND### -
- - -
-
- -
-
-
- -

###BACKLINK_LOGIN### 

- - - - -###STATUS_HEADER### -###STATUS_MESSAGE### - - - -
-
- ###LEGEND### -
- - -
-
- - -
-
- -
-
- -
- -###BACKLINK_LOGIN### - diff --git a/Resources/Public/callback.php b/Resources/Public/callback.php index 7d1ac8a..5ed80cc 100644 --- a/Resources/Public/callback.php +++ b/Resources/Public/callback.php @@ -16,7 +16,7 @@ if (!(empty($_GET['state']) || empty($_GET['code']))) { $schema = $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''; if (empty($schema)) { - $schema = (@$_SERVER['HTTPS'] === 'on') ? 'https' : 'http'; + $schema = (($_SERVER['HTTPS'] ?? '') === 'on') ? 'https' : 'http'; } if ($_SERVER['SERVER_NAME'] !== '_') { $currentUrl = $schema . '://' . $_SERVER['SERVER_NAME']; @@ -26,8 +26,8 @@ $currentUrl = $schema . $_SERVER['HTTP_HOST']; } $currentUrl .= $_SERVER['REQUEST_URI']; - - if (($pos = strpos($currentUrl, 'typo3conf/ext/oidc/Resources/Public/callback.php')) !== false) { + $pos = strpos($currentUrl, 'typo3conf/ext/oidc/Resources/Public/callback.php'); + if ($pos !== false) { $connectUrl = substr($currentUrl, 0, $pos) . '?type=1489657462&state=' . $_GET['state'] . '&code=' . $_GET['code']; header('Location: ' . $connectUrl); exit(); diff --git a/composer.json b/composer.json index f9b5c08..850d486 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "license": "GPL-2.0-or-later", "require": { "php": ">= 7.4.0, <= 8.2.99", + "ext-curl": "*", "ext-json": "*", "typo3/cms-core": "^11", "league/oauth2-client": "^2.0" From c087305e73935f36c4e525cf56eab5fc5a5b00c4 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Thu, 27 Apr 2023 22:17:03 +0200 Subject: [PATCH 064/142] [TASK] Do not load all users into memory When the usergroup assignment has to be bulk changed due to a change in tx_oidc_pattern it is avoided to load all affected users into memory. Instead, process each user sequentially. --- Classes/Hooks/DataHandler.php | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Classes/Hooks/DataHandler.php b/Classes/Hooks/DataHandler.php index 2ad78fa..2b80f31 100644 --- a/Classes/Hooks/DataHandler.php +++ b/Classes/Hooks/DataHandler.php @@ -31,14 +31,13 @@ class DataHandler * * @param string $operation * @param string $table - * @param mixed $id + * @param int|string $id * @param array $fieldArray - * @param \TYPO3\CMS\Core\DataHandling\DataHandler $pObj * @return void */ - public function processDatamap_afterDatabaseOperations($operation, $table, $id, array $fieldArray, \TYPO3\CMS\Core\DataHandling\DataHandler $pObj) + public function processDatamap_afterDatabaseOperations(string $operation, string $table, $id, array $fieldArray) { - if (!($table === 'fe_groups' && $operation === 'update')) { + if ($table !== 'fe_groups' || $operation !== 'update') { return; } @@ -53,12 +52,11 @@ public function processDatamap_afterDatabaseOperations($operation, $table, $id, ->where( $queryBuilder->expr()->inSet('usergroup', (string)$id) ) - ->execute() - ->fetchAllAssociative(); + ->execute(); $tableConnection = GeneralUtility::makeInstance(ConnectionPool::class) ->getConnectionForTable('fe_users'); - foreach ($usersInThisUserGroup as $user) { + while ($user = $usersInThisUserGroup->fetchAssociative()) { $userGroups = GeneralUtility::intExplode(',', $user['usergroup'], true); // Remove this user group from the list $index = array_search($id, $userGroups); From 95e0573e2b7e3226990b3bf8545218576ce7f624 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Wed, 26 Apr 2023 20:39:05 +0200 Subject: [PATCH 065/142] [BUGFIX] Restore ext_emconf.php It is still needed for non-composer installations --- ext_emconf.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 ext_emconf.php diff --git a/ext_emconf.php b/ext_emconf.php new file mode 100644 index 0000000..bcf9508 --- /dev/null +++ b/ext_emconf.php @@ -0,0 +1,24 @@ + 'OpenID Connect Authentication', + 'description' => 'This extension uses OpenID Connect to authenticate users.', + 'category' => 'services', + 'author' => 'Xavier Perseguers', + 'author_company' => 'Causal Sàrl', + 'author_email' => 'xavier@causal.ch', + 'state' => 'stable', + 'uploadfolder' => 0, + 'createDirs' => '', + 'modify_tables' => '', + 'clearCacheOnLoad' => 0, + 'version' => '2.0.0-dev', + 'constraints' => [ + 'depends' => [ + 'php' => '7.4.0-8.2.99', + 'typo3' => '11.5.0-11.5.99', + ], + 'conflicts' => [], + 'suggests' => [], + ], +]; + From d06fba4df4fab1efac28c427fb40fe1393c7d0cc Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Tue, 9 May 2023 11:17:36 +0200 Subject: [PATCH 066/142] [BUGFIX] Set correct branch alias Use valid version number in ext_emconf --- composer.json | 2 +- ext_emconf.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 850d486..18e57cc 100644 --- a/composer.json +++ b/composer.json @@ -60,7 +60,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.3.x-dev" + "dev-master": "2.0.x-dev" }, "typo3/cms": { "extension-key": "oidc" diff --git a/ext_emconf.php b/ext_emconf.php index bcf9508..79f17f1 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -11,7 +11,7 @@ 'createDirs' => '', 'modify_tables' => '', 'clearCacheOnLoad' => 0, - 'version' => '2.0.0-dev', + 'version' => '2.0.0', 'constraints' => [ 'depends' => [ 'php' => '7.4.0-8.2.99', From a6d0913a14f4decd8f1e2822cb7b743da4ec0b17 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Tue, 9 May 2023 11:22:21 +0200 Subject: [PATCH 067/142] [FEATURE] Add event for post-processing a user record The new ModifyUserEvent allow to modify the user record before it is inserted/updated in the database. --- Classes/Event/ModifyUserEvent.php | 41 +++++++++++++++++++++++ Classes/Service/AuthenticationService.php | 13 +++++++ 2 files changed, 54 insertions(+) create mode 100644 Classes/Event/ModifyUserEvent.php diff --git a/Classes/Event/ModifyUserEvent.php b/Classes/Event/ModifyUserEvent.php new file mode 100644 index 0000000..1786e04 --- /dev/null +++ b/Classes/Event/ModifyUserEvent.php @@ -0,0 +1,41 @@ +user = $user; + } + + public function getUser(): array + { + return $this->user; + } + + public function setUser(array $user): void + { + $this->user = $user; + } +} diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 1b1e0be..881496e 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -18,6 +18,7 @@ namespace Causal\Oidc\Service; use Causal\Oidc\Event\AuthenticationGetUserEvent; +use Causal\Oidc\Event\ModifyUserEvent; use InvalidArgumentException; use League\OAuth2\Client\Provider\Exception\IdentityProviderException; use League\OAuth2\Client\Token\AccessToken; @@ -436,6 +437,12 @@ protected function convertResourceOwner(array $info) $this->logger->info('Detected a returning user'); $data['usergroup'] = implode(',', $newUserGroups); $user = array_merge($row, $data); + + $event = new ModifyUserEvent($user); + $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); + $eventDispatcher->dispatch($event); + $user = $event->getUser(); + if ($user != $row) { $this->logger->debug('Updating existing user', [ 'old' => $row, @@ -464,6 +471,12 @@ protected function convertResourceOwner(array $info) 'crdate' => $GLOBALS['EXEC_TIME'], 'tx_oidc' => $info['sub'], ]); + + $event = new ModifyUserEvent($data); + $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); + $eventDispatcher->dispatch($event); + $data = $event->getUser(); + $tableConnection->insert( $userTable, $data From 035ce2a139038828dc648f771cce65325616c3d9 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Tue, 9 May 2023 13:19:35 +0200 Subject: [PATCH 068/142] [FEATURE] Add method to retrieve access token for client This uses the grant 'client_credentials'. --- Classes/Service/OAuthService.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Classes/Service/OAuthService.php b/Classes/Service/OAuthService.php index f41d797..4796bae 100644 --- a/Classes/Service/OAuthService.php +++ b/Classes/Service/OAuthService.php @@ -21,8 +21,8 @@ use Causal\Oidc\Factory\OAuthProviderFactoryInterface; use League\OAuth2\Client\Provider\AbstractProvider; use League\OAuth2\Client\Provider\Exception\IdentityProviderException; -use League\OAuth2\Client\Provider\GenericProvider; use League\OAuth2\Client\Provider\ResourceOwnerInterface; +use League\OAuth2\Client\Token\AccessTokenInterface; use RuntimeException; use TYPO3\CMS\Core\Utility\GeneralUtility; use League\OAuth2\Client\Token\AccessToken; @@ -113,6 +113,13 @@ public function getAccessToken(string $codeOrUsername, ?string $password = null, return $accessToken; } + public function getAccessTokenForClient(): AccessTokenInterface + { + return $this->getProvider()->getAccessToken('client_credentials', [ + 'scope' => implode(',', $this->getProvider()->getDefaultScopes()), + ]); + } + /** * Returns an AccessToken using request path authentication. * From 77b727dedca933ed5fba8f6f738c376ea7572d21 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Tue, 9 May 2023 13:28:08 +0200 Subject: [PATCH 069/142] [FEATURE] Make auth service priority/quality configurable --- Resources/Private/Language/de.locallang_db.xlf | 10 +++++++++- Resources/Private/Language/locallang_db.xlf | 8 +++++++- ext_conf_template.txt | 6 ++++++ ext_localconf.php | 4 ++-- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/Resources/Private/Language/de.locallang_db.xlf b/Resources/Private/Language/de.locallang_db.xlf index a0cebd1..0d505f6 100644 --- a/Resources/Private/Language/de.locallang_db.xlf +++ b/Resources/Private/Language/de.locallang_db.xlf @@ -1,6 +1,6 @@ - +
@@ -11,6 +11,14 @@ OpenID Connect Identifier OpenID Connect Identifier + + Authentication service priority: This defines the order of the OIDC authentication service in relation to other authentication services. Higher number wins. + Authentifizierungs-Service Priorität: Definiert die Reihenfolge des OIDC Authentifizierungs-Service in Bezug auf andere Authentifizierungs-Services. Höhere Zahl gewinnt. + + + Authentication service Quality: This is used by TYPO3 if two authentication services have the same priority. Higher number wins. + Authentifizierungs-Service Qualität: TYPO3 nutzt diesen Wert, wenn zwei Authentifizierungs-Services die gleiche Priorität besitzen. Höhere Zahl gewinnt. + Enable PKCE: Enable PKCE flow. Code challenge and code verifier will be sent along. PKCE aktivieren: Aktiviert den PKCE-Fluss. Code-Challenge und Code-Verifier werden mitgeschickt. diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index ae2886b..9a972da 100755 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -1,6 +1,6 @@ - +
@@ -9,6 +9,12 @@ OpenID Connect Identifier + + Authentication service priority: This defines the order of the OIDC authentication service in relation to other authentication services. Higher number wins. + + + Authentication service Quality: This is used by TYPO3 if two authentication services have the same priority. Higher number wins. + Enable PKCE: Enable PKCE flow. Code challenge and code verifier will be sent along. diff --git a/ext_conf_template.txt b/ext_conf_template.txt index 2406c30..61f8f7c 100644 --- a/ext_conf_template.txt +++ b/ext_conf_template.txt @@ -60,3 +60,9 @@ oidcDisableCSRFProtection = 0 # cat=advanced//3; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oauthProviderFactory oauthProviderFactory = + +# cat=advanced//4; type=int+; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.authenticationServicePriority +authenticationServicePriority = 82 + +# cat=advanced//5; type=int+; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.authenticationServiceQuality +authenticationServiceQuality = 80 diff --git a/ext_localconf.php b/ext_localconf.php index d1e2d34..a0c6e04 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -34,8 +34,8 @@ 'description' => 'Authentication service for OpenID Connect.', 'subtype' => $subTypes, 'available' => true, - 'priority' => 82, /* will be called before default TYPO3 authentication service */ - 'quality' => 80, + 'priority' => (int)($settings['authenticationServicePriority'] ?? 82), + 'quality' => (int)($settings['authenticationServiceQuality'] ?? 80), 'os' => '', 'exec' => '', 'className' => $authenticationClassName, From d031643143b9612d48c88febb8e9baf993db9c2e Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Mon, 19 Jun 2023 13:04:10 +0200 Subject: [PATCH 070/142] [FEATURE] Add ModifyResourceOwnerEvent This allows pre-processing of the resource owner received by the server before it is converted to a user. Resolves: #76 --- Classes/Event/ModifyResourceOwnerEvent.php | 41 ++++++++++++++++++++++ Classes/Service/AuthenticationService.php | 8 ++++- 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 Classes/Event/ModifyResourceOwnerEvent.php diff --git a/Classes/Event/ModifyResourceOwnerEvent.php b/Classes/Event/ModifyResourceOwnerEvent.php new file mode 100644 index 0000000..a8caf31 --- /dev/null +++ b/Classes/Event/ModifyResourceOwnerEvent.php @@ -0,0 +1,41 @@ +resourceOwner = $resourceOwner; + } + + public function getResourceOwner(): array + { + return $this->resourceOwner; + } + + public function setResourceOwner(array $resourceOwner): void + { + $this->resourceOwner = $resourceOwner; + } +} diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 881496e..523a9fa 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -18,6 +18,7 @@ namespace Causal\Oidc\Service; use Causal\Oidc\Event\AuthenticationGetUserEvent; +use Causal\Oidc\Event\ModifyResourceOwnerEvent; use Causal\Oidc\Event\ModifyUserEvent; use InvalidArgumentException; use League\OAuth2\Client\Provider\Exception\IdentityProviderException; @@ -260,7 +261,12 @@ protected function getUserFromAccessToken(OAuthService $service, AccessToken $ac 1490086626 ); } - $user = $this->convertResourceOwner($resourceOwner); + + $event = new ModifyResourceOwnerEvent($resourceOwner); + $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); + $eventDispatcher->dispatch($event); + + $user = $this->convertResourceOwner($event->getResourceOwner()); if ($this->config['oidcRevokeAccessTokenAfterLogin']) { $service->revokeToken($accessToken); From 767657ea8f909341dd87a8410a9d2a14c700d532 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Mon, 19 Jun 2023 14:09:42 +0200 Subject: [PATCH 071/142] [TASK] Migrate callback.php to Middleware The callback is handled as a generic middleware. The URL magic is gone and the current request is simply enriched with the specific page-type for further handling in the controller. The default redirectUri now is the startpage of the site. Resolves: #104 --- Classes/Middleware/OauthCallback.php | 36 +++++++++++++++++++++++++++ Classes/Service/OAuthService.php | 21 ++++++---------- Resources/Public/callback.php | 37 ---------------------------- 3 files changed, 43 insertions(+), 51 deletions(-) create mode 100644 Classes/Middleware/OauthCallback.php delete mode 100644 Resources/Public/callback.php diff --git a/Classes/Middleware/OauthCallback.php b/Classes/Middleware/OauthCallback.php new file mode 100644 index 0000000..ce63df5 --- /dev/null +++ b/Classes/Middleware/OauthCallback.php @@ -0,0 +1,36 @@ +getQueryParams(); + $code = $queryParams['code'] ?? ''; + if ($code) { + $state = $queryParams['state'] ?? ''; + if (!$state) { + return new Response('Invalid state', 400); + } + + $queryParams['type'] = 1489657462; + $request = $request->withQueryParams($queryParams); + } + return $handler->handle($request); + } +} diff --git a/Classes/Service/OAuthService.php b/Classes/Service/OAuthService.php index 4796bae..e162095 100644 --- a/Classes/Service/OAuthService.php +++ b/Classes/Service/OAuthService.php @@ -132,17 +132,11 @@ public function getAccessTokenForClient(): AccessTokenInterface */ public function getAccessTokenWithRequestPathAuthentication(string $username, string $password): ?AccessToken { - $redirectUri = $this->settings['oidcRedirectUri']; - if (empty($redirectUri)) { - $redirectUri = GeneralUtility::getIndpEnv('TYPO3_SSL') ? 'https://' : 'http://'; - $redirectUri .= GeneralUtility::getIndpEnv('HTTP_HOST'); - $redirectUri .= '/typo3conf/ext/oidc/Resources/Public/callback.php'; - } $url = $this->settings['oidcEndpointAuthorize'] . '?' . http_build_query([ 'response_type' => 'code', 'client_id' => $this->settings['oidcClientKey'], 'scope' => $this->settings['oidcClientScopes'], - 'redirect_uri' => $redirectUri, + 'redirect_uri' => $this->getRedirectUrl(), ]); $ch = curl_init(); @@ -228,13 +222,7 @@ protected function getProvider(): AbstractProvider } $settings = $this->settings; - $settings['oidcRedirectUri'] = $this->settings['oidcRedirectUri']; - if (empty($settings['oidcRedirectUri'])) { - $redirectUri = GeneralUtility::getIndpEnv('TYPO3_SSL') ? 'https://' : 'http://'; - $redirectUri .= GeneralUtility::getIndpEnv('HTTP_HOST'); - $redirectUri .= '/typo3conf/ext/oidc/Resources/Public/callback.php'; - $settings['oidcRedirectUri'] = $redirectUri; - } + $settings['oidcRedirectUri'] = $this->getRedirectUrl(); /** @var OAuthProviderFactoryInterface $factory */ $factory = GeneralUtility::makeInstance($factoryClass); @@ -269,4 +257,9 @@ public function getFreshAccessToken(): ?AccessToken return $accessToken; } + + protected function getRedirectUrl(): string + { + return $this->settings['oidcRedirectUri'] ?? GeneralUtility::getIndpEnv('TYPO3_SITE_URL'); + } } diff --git a/Resources/Public/callback.php b/Resources/Public/callback.php deleted file mode 100644 index 5ed80cc..0000000 --- a/Resources/Public/callback.php +++ /dev/null @@ -1,37 +0,0 @@ - Date: Thu, 27 Apr 2023 23:20:47 +0200 Subject: [PATCH 072/142] [!!!][TASK] Add type declarations for ResourceOwnerHookInterface --- Classes/Service/ResourceOwnerHookInterface.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Classes/Service/ResourceOwnerHookInterface.php b/Classes/Service/ResourceOwnerHookInterface.php index 59cb88c..d152cc3 100644 --- a/Classes/Service/ResourceOwnerHookInterface.php +++ b/Classes/Service/ResourceOwnerHookInterface.php @@ -1,4 +1,7 @@ Date: Mon, 19 Jun 2023 14:31:03 +0200 Subject: [PATCH 073/142] [TASK] Use RequestFactory instead of direct CURL Resolves: #109 --- Classes/Service/OAuthService.php | 45 ++++++++++++++------------------ composer.json | 1 - 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/Classes/Service/OAuthService.php b/Classes/Service/OAuthService.php index 4796bae..cf77255 100644 --- a/Classes/Service/OAuthService.php +++ b/Classes/Service/OAuthService.php @@ -19,11 +19,13 @@ use Causal\Oidc\Factory\GenericOAuthProviderFactory; use Causal\Oidc\Factory\OAuthProviderFactoryInterface; +use GuzzleHttp\RequestOptions; use League\OAuth2\Client\Provider\AbstractProvider; use League\OAuth2\Client\Provider\Exception\IdentityProviderException; use League\OAuth2\Client\Provider\ResourceOwnerInterface; use League\OAuth2\Client\Token\AccessTokenInterface; use RuntimeException; +use TYPO3\CMS\Core\Http\RequestFactory; use TYPO3\CMS\Core\Utility\GeneralUtility; use League\OAuth2\Client\Token\AccessToken; @@ -145,32 +147,25 @@ public function getAccessTokenWithRequestPathAuthentication(string $username, st 'redirect_uri' => $redirectUri, ]); - $ch = curl_init(); - curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'Authorization: Basic ' . base64_encode($username . ':' . $password), - ]); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_VERBOSE, 1); - curl_setopt($ch, CURLOPT_HEADER, 1); - $content = curl_exec($ch); - - if ($content === false) { - throw new RuntimeException('Curl ERROR: ' . curl_error($ch), 1510049345); + $result = GeneralUtility::makeInstance(RequestFactory::class)->request( + 'GET', + $url, + [ + RequestOptions::AUTH => [$username, $password], + RequestOptions::ALLOW_REDIRECTS => false + ] + ); + + if ($result->getStatusCode() < 300 && $result->getStatusCode() >= 400) { + throw new RuntimeException('Request failed', 1510049345); } - curl_close($ch); - - $headers = explode(LF, $content); - foreach ($headers as $header) { - list($key, $value) = GeneralUtility::trimExplode(':', $header, false, 2); - if ($key === 'Location') { - $queryParams = explode('&', substr($value, strpos($value, '?') + 1)); - foreach ($queryParams as $param) { - list($key, $value) = explode('=', $param, 2); - if ($key === 'code') { - return $this->getAccessToken($value); - } - } + + if ($result->getHeader('Location')) { + $targetUrl = $result->getHeader('Location')[0]; + $query = parse_url($targetUrl, PHP_URL_QUERY); + parse_str($query, $queryParams); + if (isset($queryParams['code'])) { + return $this->getAccessToken($queryParams['code']); } } diff --git a/composer.json b/composer.json index 18e57cc..6c6a956 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,6 @@ "license": "GPL-2.0-or-later", "require": { "php": ">= 7.4.0, <= 8.2.99", - "ext-curl": "*", "ext-json": "*", "typo3/cms-core": "^11", "league/oauth2-client": "^2.0" From a902a2893241c89192cf970945596482bdb4fc16 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Mon, 19 Jun 2023 14:52:22 +0200 Subject: [PATCH 074/142] [TASK] Improve composer.json --- composer.json | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 18e57cc..e2de208 100644 --- a/composer.json +++ b/composer.json @@ -25,8 +25,10 @@ "php": ">= 7.4.0, <= 8.2.99", "ext-curl": "*", "ext-json": "*", - "typo3/cms-core": "^11", - "league/oauth2-client": "^2.0" + "typo3/cms-core": "^11.5", + "typo3/cms-frontend": "^11.5", + "typo3/cms-felogin": "^11.5", + "league/oauth2-client": "^2.7" }, "autoload": { "psr-4": { @@ -63,7 +65,16 @@ "dev-master": "2.0.x-dev" }, "typo3/cms": { + "app-dir": ".Build", + "web-dir": ".Build/web", "extension-key": "oidc" } + }, + "config": { + "vendor-dir": ".Build/vendor", + "allow-plugins": { + "typo3/cms-composer-installers": true, + "typo3/class-alias-loader": true + } } } From fff9ac5e152a8d891ce3113df56ff5812f8c13e4 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Mon, 19 Jun 2023 15:00:58 +0200 Subject: [PATCH 075/142] [TASK] Remove switchableControllerActions Resolves: #105 --- Configuration/TypoScript/setup.typoscript | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Configuration/TypoScript/setup.typoscript b/Configuration/TypoScript/setup.typoscript index f42bb97..c2d6869 100644 --- a/Configuration/TypoScript/setup.typoscript +++ b/Configuration/TypoScript/setup.typoscript @@ -14,11 +14,6 @@ OidcAuth { extensionName = Oidc pluginName = Pi1 controller = Authentication - switchableControllerActions { - Authentication { - 1 = connect - } - } } } From 5a422f9d1664293b33c345560b94acd77e5d2d63 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Thu, 27 Apr 2023 23:22:21 +0200 Subject: [PATCH 076/142] [TASK] Code cleanup of OAuthService Minor preparation for v12 included. --- Classes/Service/OAuthService.php | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/Classes/Service/OAuthService.php b/Classes/Service/OAuthService.php index b3b0722..d564338 100644 --- a/Classes/Service/OAuthService.php +++ b/Classes/Service/OAuthService.php @@ -26,8 +26,10 @@ use League\OAuth2\Client\Token\AccessTokenInterface; use RuntimeException; use TYPO3\CMS\Core\Http\RequestFactory; +use TYPO3\CMS\Core\Localization\Locale; use TYPO3\CMS\Core\Utility\GeneralUtility; use League\OAuth2\Client\Token\AccessToken; +use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; /** * Class OAuthService. @@ -47,7 +49,7 @@ class OAuthService * @param array $settings * @return $this */ - public function setSettings(array $settings) + public function setSettings(array $settings): self { $this->settings = $settings; @@ -65,9 +67,14 @@ public function getAuthorizationUrl(array $options = []): string if (!empty($this->settings['oidcAuthorizeLanguageParameter'])) { $languageOption = $this->settings['oidcAuthorizeLanguageParameter']; - if (isset($GLOBALS['TSFE']->lang)) { - $frontendLanguage = $GLOBALS['TSFE']->lang; - $options[$languageOption] = $frontendLanguage; + $language = $this->getTSFE()->getLanguage()->getLocale(); + if (is_string($language)) { + // v11 case + $options[$languageOption] = $language; + } else { + // v12 case + /** @var Locale $language */ + $options[$languageOption] = $language->getName(); } } @@ -115,6 +122,9 @@ public function getAccessToken(string $codeOrUsername, ?string $password = null, return $accessToken; } + /** + * @throws IdentityProviderException + */ public function getAccessTokenForClient(): AccessTokenInterface { return $this->getProvider()->getAccessToken('client_credentials', [ @@ -130,7 +140,8 @@ public function getAccessTokenForClient(): AccessTokenInterface * * @param string $username * @param string $password - * @return AccessToken + * @return AccessToken|null + * @throws IdentityProviderException */ public function getAccessTokenWithRequestPathAuthentication(string $username, string $password): ?AccessToken { @@ -150,7 +161,7 @@ public function getAccessTokenWithRequestPathAuthentication(string $username, st ] ); - if ($result->getStatusCode() < 300 && $result->getStatusCode() >= 400) { + if ($result->getStatusCode() < 300 || $result->getStatusCode() >= 400) { throw new RuntimeException('Request failed', 1510049345); } @@ -182,6 +193,7 @@ public function getResourceOwner(AccessToken $token): ResourceOwnerInterface * * @param AccessToken $token * @return bool + * @throws IdentityProviderException */ public function revokeToken(AccessToken $token): bool { @@ -257,4 +269,9 @@ protected function getRedirectUrl(): string { return $this->settings['oidcRedirectUri'] ?? GeneralUtility::getIndpEnv('TYPO3_SITE_URL'); } + + protected function getTSFE(): TypoScriptFrontendController + { + return $GLOBALS['TSFE']; + } } From d11359e0a5950931af9be5ebf838c9193e45fa77 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Mon, 26 Jun 2023 11:43:15 +0200 Subject: [PATCH 077/142] [DOCS] Update README.md for new felogin --- README.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 216ac62..e615e50 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,22 @@ secret. ## Default FE Loginbox -This extension integrates with the system extension 'felogin' and provides a new marker `###OPENID_CONNECT###` to be -used in the felogin template. A sample template is included. The marker will be replaced by a login link, pointing to -the authorization endpoint of the authorization server. +This extension integrates with the system extension 'felogin'. + +This FLUID markup can be used to include a link to the authorization endpoint of the authorization server. + +```html + + + OpenID Connect + + + Invalid OpenID Connect configuration + + +``` + +See also `Resources/Private/Templates/Login/Login.html` as reference. ## OIDC Login From f40dc599f0fa0c5c89980ba8140fd9f2456b2f0d Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Thu, 29 Jun 2023 17:02:42 +0200 Subject: [PATCH 078/142] [BUGFIX] Correctly fallback to default callback uri --- Classes/Service/OAuthService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Service/OAuthService.php b/Classes/Service/OAuthService.php index d564338..7a4da3c 100644 --- a/Classes/Service/OAuthService.php +++ b/Classes/Service/OAuthService.php @@ -267,7 +267,7 @@ public function getFreshAccessToken(): ?AccessToken protected function getRedirectUrl(): string { - return $this->settings['oidcRedirectUri'] ?? GeneralUtility::getIndpEnv('TYPO3_SITE_URL'); + return $this->settings['oidcRedirectUri'] ?: GeneralUtility::getIndpEnv('TYPO3_SITE_URL'); } protected function getTSFE(): TypoScriptFrontendController From fe57285a3007b21b0da7bd079a10df4d4999390b Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Thu, 29 Jun 2023 17:03:00 +0200 Subject: [PATCH 079/142] [BUGFIX] Register middleware --- Configuration/RequestMiddlewares.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 Configuration/RequestMiddlewares.php diff --git a/Configuration/RequestMiddlewares.php b/Configuration/RequestMiddlewares.php new file mode 100644 index 0000000..04398fc --- /dev/null +++ b/Configuration/RequestMiddlewares.php @@ -0,0 +1,14 @@ + [ + 'oidccallback' => [ + 'target' => \Causal\Oidc\Middleware\OauthCallback::class, + 'after' => [ + 'typo3/cms-core/normalized-params-attribute', + ], + 'before' => [ + 'typo3/cms-frontend/eid' + ] + ], + ], +]; From da50cd7874af977061e5bbc5ad6e381beece1369 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Mon, 17 Jul 2023 16:37:38 +0200 Subject: [PATCH 080/142] [BUGFIX] Determine FE|BE mode based on authInfo The global TYPO3_REQUEST is not yet set, so use the information provided by the Core through authInfo. --- Classes/Service/AuthenticationService.php | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 523a9fa..825163e 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -298,15 +298,9 @@ public function authUser(array $user): int */ protected function convertResourceOwner(array $info) { - if (ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend()) { - $mode = 'FE'; - $userTable = 'fe_users'; - $userGroupTable = 'fe_groups'; - } else { - $mode = 'BE'; - $userTable = 'be_users'; - $userGroupTable = 'be_groups'; - } + $mode = $this->authInfo['loginType']; + $userTable = $this->db_user['table']; + $userGroupTable = $mode === 'FE' ? 'fe_groups' : 'be_groups'; $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable($userTable); @@ -722,14 +716,11 @@ protected function getTypoScriptSetup(): array protected function generatePassword(): string { - $mode = ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend() - ? 'FE' - : 'BE'; $password = substr(str_shuffle('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$'), 0, 20); $passwordHashFactory = GeneralUtility::makeInstance(PasswordHashFactory::class); try { - $objInstanceSaltedPW = $passwordHashFactory->getDefaultHashInstance($mode); + $objInstanceSaltedPW = $passwordHashFactory->getDefaultHashInstance($this->authInfo['loginType']); } catch (InvalidPasswordHashException $e) { return ''; } @@ -738,8 +729,7 @@ protected function generatePassword(): string protected function getLocalTSFE(): TypoScriptFrontendController { - /** @var ServerRequestInterface $request */ - $request = $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals(); + $request = ServerRequestFactory::fromGlobals(); $siteMatcher = GeneralUtility::makeInstance(SiteMatcher::class); $routeResult = $siteMatcher->matchRequest($request); if ($routeResult instanceof SiteRouteResult) { From fc7e8a851d96c4f0a9c94e1c54adaa4be79dcf1b Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Mon, 17 Jul 2023 16:39:36 +0200 Subject: [PATCH 081/142] [TASK] Cleanup some code an capture exceptions Log full exceptions and cleanup some old code --- Classes/Service/AuthenticationService.php | 30 ++++++++++++++--------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 825163e..1d6436f 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -20,12 +20,13 @@ use Causal\Oidc\Event\AuthenticationGetUserEvent; use Causal\Oidc\Event\ModifyResourceOwnerEvent; use Causal\Oidc\Event\ModifyUserEvent; +use Doctrine\DBAL\DBALException; +use Doctrine\DBAL\Driver\Exception; use InvalidArgumentException; use League\OAuth2\Client\Provider\Exception\IdentityProviderException; use League\OAuth2\Client\Token\AccessToken; use LogicException; use Psr\EventDispatcher\EventDispatcherInterface; -use Psr\Http\Message\ServerRequestInterface; use RuntimeException; use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException; @@ -35,7 +36,6 @@ use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction; use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction; use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction; -use TYPO3\CMS\Core\Http\ApplicationType; use TYPO3\CMS\Core\Http\ServerRequestFactory; use TYPO3\CMS\Core\Routing\PageArguments; use TYPO3\CMS\Core\Routing\RouteNotFoundException; @@ -176,7 +176,7 @@ protected function authenticateWithAuthorizationCode(string $code, string $codeV // Probably a "server_error", meaning the code is not valid anymore $this->logger->error('Possibly replay: code has been refused by the authentication server', [ 'code' => $code, - 'message' => $e->getMessage(), + 'exception' => $e, ]); return false; } @@ -221,7 +221,7 @@ protected function authenticateWithResourceOwnerPasswordCredentials(string $user } catch (IdentityProviderException $e) { $this->logger->error('Authentication has been refused by the authentication server', [ 'username' => $username, - 'message' => $e->getMessage(), + 'exception' => $e, ]); } @@ -247,14 +247,17 @@ protected function getUserFromAccessToken(OAuthService $service, AccessToken $ac $resourceOwner = $service->getResourceOwner($accessToken)->toArray(); $this->logger->debug('Resource owner retrieved', $resourceOwner); } catch (IdentityProviderException $e) { - $this->logger->error('Could not retrieve resource owner', [ - 'message' => $e->getMessage(), - ]); + $this->logger->error('Could not retrieve resource owner', ['exception' => $e]); return false; } if (empty($resourceOwner['sub'])) { $this->logger->error('No "sub" found in resource owner, revoking access token'); - $service->revokeToken($accessToken); + try { + $service->revokeToken($accessToken); + } catch (IdentityProviderException $e) { + $this->logger->error('Could not revoke token', ['exception' => $e]); + return false; + } throw new RuntimeException( 'Resource owner does not have a sub part: ' . json_encode($resourceOwner) . '. Your access token has been revoked. Please try again.', @@ -269,7 +272,11 @@ protected function getUserFromAccessToken(OAuthService $service, AccessToken $ac $user = $this->convertResourceOwner($event->getResourceOwner()); if ($this->config['oidcRevokeAccessTokenAfterLogin']) { - $service->revokeToken($accessToken); + try { + $service->revokeToken($accessToken); + } catch (IdentityProviderException $e) { + $this->logger->error('Could not revoke token', ['exception' => $e]); + } } return $user; @@ -294,7 +301,6 @@ public function authUser(array $user): int * * @param array $info * @return array|bool - * @throws InvalidArgumentException */ protected function convertResourceOwner(array $info) { @@ -611,14 +617,14 @@ protected function applyMapping(string $table, array $oidc, array $typo3, array * @param string $value * @return array Modified $typo3 array * @throws UnexpectedValueException - * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::getFieldVal() + * @see ContentObjectRenderer::getFieldVal */ protected function mergeSimple(array $oidc, array $typo3, string $field, string $value): array { // Constant by default $mappedValue = $value; - if (preg_match("`<([^$]*)>`", $value, $attribute)) { // OIDC attribute + if (preg_match("`<([^$]*)>`", $value)) { // OIDC attribute $sections = !strstr($value, '//') ? [$value] : GeneralUtility::trimExplode('//', $value, true); From 86cf684c847cb6f257a2ed3684a337652e995405 Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Tue, 5 Sep 2023 08:32:53 +0200 Subject: [PATCH 082/142] [BUGFIX] Stick to 2-letter ISO code for the language Resolves: #126 --- Classes/Service/OAuthService.php | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/Classes/Service/OAuthService.php b/Classes/Service/OAuthService.php index 7a4da3c..4583d60 100644 --- a/Classes/Service/OAuthService.php +++ b/Classes/Service/OAuthService.php @@ -26,7 +26,6 @@ use League\OAuth2\Client\Token\AccessTokenInterface; use RuntimeException; use TYPO3\CMS\Core\Http\RequestFactory; -use TYPO3\CMS\Core\Localization\Locale; use TYPO3\CMS\Core\Utility\GeneralUtility; use League\OAuth2\Client\Token\AccessToken; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; @@ -66,15 +65,9 @@ public function getAuthorizationUrl(array $options = []): string { if (!empty($this->settings['oidcAuthorizeLanguageParameter'])) { $languageOption = $this->settings['oidcAuthorizeLanguageParameter']; - - $language = $this->getTSFE()->getLanguage()->getLocale(); - if (is_string($language)) { - // v11 case + if (!empty($languageOption)) { + $language = $this->getTSFE()->getLanguage()->getTwoLetterIsoCode(); $options[$languageOption] = $language; - } else { - // v12 case - /** @var Locale $language */ - $options[$languageOption] = $language->getName(); } } From e7ae3163abb6af91eaa2450fce5ada74fb1baf69 Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Wed, 11 Oct 2023 08:35:15 +0200 Subject: [PATCH 083/142] [TASK] Synchronize TER release script with composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 2a738f3..43072f7 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,7 @@ "extension-create-libs": [ "mkdir -p Libraries/temp", "[ -f $HOME/.composer/vendor/bin/phar-composer ] || composer global require clue/phar-composer", - "if [ ! -f Libraries/league-oauth2-client.phar ]; then cd Libraries/temp && composer require league/oauth2-client=^2.0 && composer config classmap-authoritative true && composer config prepend-autoloader false && composer dump-autoload; fi", + "if [ ! -f Libraries/league-oauth2-client.phar ]; then cd Libraries/temp && composer require league/oauth2-client=^2.7 && composer config classmap-authoritative true && composer config prepend-autoloader false && composer dump-autoload; fi", "[ -f Libraries/league-oauth2-client.phar ] || $HOME/.composer/vendor/bin/phar-composer build Libraries/temp/ Libraries/league-oauth2-client.phar", "chmod -x Libraries/*.phar", "rm -rf Libraries/temp" From f94d260a53df0e5a0b0f11a68928ce1e3db2bd0c Mon Sep 17 00:00:00 2001 From: Denis Lorch Date: Thu, 30 Nov 2023 10:47:18 +0100 Subject: [PATCH 084/142] Add a custom user groups event * to map the groups by a a different pattern if "Roles" does not fit * for example work with "claims" or "group_membership" data in a custom listener --- .../AuthenticationGetUserGroupsEvent.php | 70 +++++++++++++++++++ Classes/Service/AuthenticationService.php | 14 ++++ 2 files changed, 84 insertions(+) create mode 100644 Classes/Event/AuthenticationGetUserGroupsEvent.php diff --git a/Classes/Event/AuthenticationGetUserGroupsEvent.php b/Classes/Event/AuthenticationGetUserGroupsEvent.php new file mode 100644 index 0000000..48f5dc3 --- /dev/null +++ b/Classes/Event/AuthenticationGetUserGroupsEvent.php @@ -0,0 +1,70 @@ +groupTable = (string)$groupTable; + $this->groups = $groups; + $this->resource = $resourceOwner; + } + + /** + * @return string + */ + public function getGroupTable() + { + return $this->groupTable; + } + + /** + * @return array + */ + public function getUserGroups() + { + return $this->groups; + } + + /** + * @return array + */ + public function getResource() + { + return $this->resource; + } + + /** + * Set your customized user group ids + * @param array $groups + */ + public function setUserGroups($groups): void + { + $this->groups = $groups; + } +} \ No newline at end of file diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 1d6436f..ddc3eaa 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -18,6 +18,7 @@ namespace Causal\Oidc\Service; use Causal\Oidc\Event\AuthenticationGetUserEvent; +use Causal\Oidc\Event\AuthenticationGetUserGroupsEvent; use Causal\Oidc\Event\ModifyResourceOwnerEvent; use Causal\Oidc\Event\ModifyUserEvent; use Doctrine\DBAL\DBALException; @@ -436,6 +437,19 @@ protected function convertResourceOwner(array $info) // Add default user groups $newUserGroups = array_unique(array_merge($newUserGroups, $defaultUserGroups)); + // emit a generic groups mapping event + // to customize the groups if the resource structure pattern "Roles" does not fit + $event = new AuthenticationGetUserGroupsEvent($userGroupTable, $newUserGroups, $info); + $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); + $eventDispatcher->dispatch($event); + if ($newUserGroups !== $event->getUserGroups()) { + $this->logger->debug('Got customized user groups by AuthenticationGetUserGroupsEvent', [ + 'previous' => implode(',', $newUserGroups), + 'new' => implode(',', $event->getUserGroups()), + ]); + $newUserGroups = $event->getUserGroups(); + } + $tableConnection = GeneralUtility::makeInstance(ConnectionPool::class) ->getConnectionForTable($userTable); From d5a88ba1bcf27d87e3ad51c3333af2b7b8016c53 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Sun, 28 Jan 2024 22:04:47 +0100 Subject: [PATCH 085/142] [BUGFIX] Correct response for invalid state --- Classes/Middleware/OauthCallback.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Middleware/OauthCallback.php b/Classes/Middleware/OauthCallback.php index ce63df5..0935628 100644 --- a/Classes/Middleware/OauthCallback.php +++ b/Classes/Middleware/OauthCallback.php @@ -25,7 +25,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface if ($code) { $state = $queryParams['state'] ?? ''; if (!$state) { - return new Response('Invalid state', 400); + return (new Response())->withStatus(400, 'Invalid state'); } $queryParams['type'] = 1489657462; From 427b4181face4165ffb5310febc6cfc75b4a0d87 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Wed, 21 Feb 2024 10:54:58 +0100 Subject: [PATCH 086/142] [TASK] Allow PHP 8.3 --- composer.json | 2 +- ext_emconf.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 43072f7..98d3597 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ ], "license": "GPL-2.0-or-later", "require": { - "php": ">= 7.4.0, <= 8.2.99", + "php": ">= 7.4.0, <= 8.3.99", "ext-json": "*", "typo3/cms-core": "^11.5", "typo3/cms-frontend": "^11.5", diff --git a/ext_emconf.php b/ext_emconf.php index 79f17f1..e7e0221 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -14,7 +14,7 @@ 'version' => '2.0.0', 'constraints' => [ 'depends' => [ - 'php' => '7.4.0-8.2.99', + 'php' => '7.4.0-8.3.99', 'typo3' => '11.5.0-11.5.99', ], 'conflicts' => [], From c2b6df3a276394d72f3197fdc2f84620faade0ec Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Thu, 7 Mar 2024 10:38:12 +0100 Subject: [PATCH 087/142] [TASK] Remove unused use statement --- Classes/Service/AuthenticationService.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 1d6436f..3beaf8c 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -20,8 +20,6 @@ use Causal\Oidc\Event\AuthenticationGetUserEvent; use Causal\Oidc\Event\ModifyResourceOwnerEvent; use Causal\Oidc\Event\ModifyUserEvent; -use Doctrine\DBAL\DBALException; -use Doctrine\DBAL\Driver\Exception; use InvalidArgumentException; use League\OAuth2\Client\Provider\Exception\IdentityProviderException; use League\OAuth2\Client\Token\AccessToken; From 373370445cb50ea333f9353bbb1fedc9695d17cb Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Thu, 7 Mar 2024 10:38:32 +0100 Subject: [PATCH 088/142] [TASK] Use TYPO3_REQUEST for tx_oidc GET params --- Classes/Service/AuthenticationService.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 3beaf8c..3d68918 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -109,7 +109,8 @@ protected function getCodeVerifierFromSession() public function getUser() { $user = false; - $params = GeneralUtility::_GET('tx_oidc'); + $request = ServerRequestFactory::fromGlobals(); + $params = $request->getQueryParams()['tx_oidc'] ?? []; $code = $params['code'] ?? null; $username = $this->login['uname'] ?? null; From b73af4c47a69963cfb1954e74fbfaecce2ba586e Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Thu, 7 Mar 2024 10:58:48 +0100 Subject: [PATCH 089/142] [FEATURE] Add AuthenticationPreUserEvent This allows to manipulate the login data and to stop the login processing for this service. --- Classes/Event/AuthenticationPreUserEvent.php | 29 ++++++++++++++++ Classes/Service/AuthenticationService.php | 35 ++++++++++++-------- 2 files changed, 51 insertions(+), 13 deletions(-) create mode 100644 Classes/Event/AuthenticationPreUserEvent.php diff --git a/Classes/Event/AuthenticationPreUserEvent.php b/Classes/Event/AuthenticationPreUserEvent.php new file mode 100644 index 0000000..490f35a --- /dev/null +++ b/Classes/Event/AuthenticationPreUserEvent.php @@ -0,0 +1,29 @@ +loginData = $loginData; + } +} diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 3d68918..8d79361 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -18,6 +18,7 @@ namespace Causal\Oidc\Service; use Causal\Oidc\Event\AuthenticationGetUserEvent; +use Causal\Oidc\Event\AuthenticationPreUserEvent; use Causal\Oidc\Event\ModifyResourceOwnerEvent; use Causal\Oidc\Event\ModifyUserEvent; use InvalidArgumentException; @@ -108,28 +109,37 @@ protected function getCodeVerifierFromSession() */ public function getUser() { + $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); + $user = false; $request = ServerRequestFactory::fromGlobals(); $params = $request->getQueryParams()['tx_oidc'] ?? []; $code = $params['code'] ?? null; - $username = $this->login['uname'] ?? null; - - if (isset($this->login['uident_text'])) { - $password = $this->login['uident_text']; - } elseif (isset($this->login['uident'])) { - $password = $this->login['uident']; - } else { - $password = null; - } - if ($code !== null) { $codeVerifier = null; if ($this->config['enableCodeVerifier']) { $codeVerifier = $this->getCodeVerifierFromSession(); } $user = $this->authenticateWithAuthorizationCode($code, $codeVerifier); - } elseif (!(empty($username) || empty($password))) { - $user = $this->authenticateWithResourceOwnerPasswordCredentials($username, $password); + } else { + $event = new AuthenticationPreUserEvent($this->login); + $eventDispatcher->dispatch($event); + if (!$event->shouldProcess) { + return false; + } + $this->login = $event->loginData; + + $username = $this->login['uname'] ?? null; + if (isset($this->login['uident_text'])) { + $password = $this->login['uident_text']; + } elseif (isset($this->login['uident'])) { + $password = $this->login['uident']; + } else { + $password = null; + } + if (!empty($username) && !empty($password)) { + $user = $this->authenticateWithResourceOwnerPasswordCredentials($username, $password); + } } // dispatch a signal (containing the user with his access token if auth was successful) @@ -140,7 +150,6 @@ public function getUser() $dispatcher->dispatch(__CLASS__, 'getUser', ['user' => $user]); $event = new AuthenticationGetUserEvent($user); - $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); $eventDispatcher->dispatch($event); $user = $event->getUser(); From 901a8c8404c875710a84bf8a549e4dec035ae827 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Thu, 7 Mar 2024 12:36:41 +0100 Subject: [PATCH 090/142] [TASK] Use PSR request data instead of _GP() --- Classes/Controller/LoginController.php | 11 ++++++++--- Classes/EventListener/FrontendLoginEventListener.php | 6 +++++- Classes/ViewHelpers/OidcLinkViewHelper.php | 6 +++++- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php index 1a84810..a4f9bfe 100644 --- a/Classes/Controller/LoginController.php +++ b/Classes/Controller/LoginController.php @@ -21,6 +21,7 @@ use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; use TYPO3\CMS\Core\Http\PropagateResponseException; use TYPO3\CMS\Core\Http\RedirectResponse; +use TYPO3\CMS\Core\Http\ServerRequestFactory; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; @@ -72,7 +73,9 @@ public function login(string $_ = '', ?array $pluginConfiguration) $this->pluginConfiguration = $pluginConfiguration; } - if (GeneralUtility::_GP('logintype') == 'login') { + $request = ServerRequestFactory::fromGlobals(); + $loginType = $request->getParsedBody()['logintype'] ?? $request->getQueryParams()['logintype'] ?? ''; + if ($loginType === 'login') { // performRedirectAfterLogin stops flow by emitting a redirect $this->performRedirectAfterLogin(); } @@ -114,8 +117,10 @@ protected function performRedirectAfterLogin() protected function determineRedirectUrl() { - if (!empty(GeneralUtility::_GP('redirect_url'))) { - return GeneralUtility::_GP('redirect_url'); + $request = $GLOBALS['TYPO3_REQUEST']; + $redirectUrl = $request->getParsedBody()['redirect_url'] ?? $request->getQueryParams()['redirect_url'] ?? ''; + if (!empty($redirectUrl)) { + return $redirectUrl; } if (isset($this->pluginConfiguration['defaultRedirectPid'])) { diff --git a/Classes/EventListener/FrontendLoginEventListener.php b/Classes/EventListener/FrontendLoginEventListener.php index 0ae4dd5..fb719eb 100644 --- a/Classes/EventListener/FrontendLoginEventListener.php +++ b/Classes/EventListener/FrontendLoginEventListener.php @@ -50,8 +50,12 @@ public function modifyLoginFormView(ModifyLoginFormViewEvent $event): void if (empty($_SESSION['requestId']) || $_SESSION['requestId'] !== $requestId) { $this->prepareAuthorizationUrl($settings); + + $request = $GLOBALS['TYPO3_REQUEST']; + $redirectUrl = $request->getParsedBody()['redirect_url'] ?? $request->getQueryParams()['redirect_url'] ?? ''; + $_SESSION['requestId'] = $requestId; - $_SESSION['oidc_redirect_url'] = GeneralUtility::_GP('redirect_url'); + $_SESSION['oidc_redirect_url'] = $redirectUrl; $this->logger->debug('PHP session is available', [ 'id' => session_id(), diff --git a/Classes/ViewHelpers/OidcLinkViewHelper.php b/Classes/ViewHelpers/OidcLinkViewHelper.php index 8910124..f27e78d 100644 --- a/Classes/ViewHelpers/OidcLinkViewHelper.php +++ b/Classes/ViewHelpers/OidcLinkViewHelper.php @@ -58,8 +58,12 @@ public static function renderStatic(array $arguments, \Closure $renderChildrenCl if (empty($_SESSION['requestId']) || $_SESSION['requestId'] !== $requestId) { self::prepareAuthorizationUrl($settings); + + $request = $GLOBALS['TYPO3_REQUEST']; + $redirectUrl = $request->getParsedBody()['redirect_url'] ?? $request->getQueryParams()['redirect_url'] ?? ''; + $_SESSION['requestId'] = $requestId; - $_SESSION['oidc_redirect_url'] = GeneralUtility::_GP('redirect_url'); + $_SESSION['oidc_redirect_url'] = $redirectUrl; static::getLogger()->debug('PHP session is available', [ 'id' => session_id(), From 69b2593a65244765bac4b59040ee81681d023db2 Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Wed, 13 Mar 2024 17:39:31 +0100 Subject: [PATCH 091/142] [BUGFIX] Prevent PHP crash in TYPO3 v11 Resolves: #91 --- Classes/Service/AuthenticationService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 8d79361..2ad8cca 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -147,7 +147,7 @@ public function getUser() // provided by the authentication server /** @var Dispatcher $dispatcher */ $dispatcher = GeneralUtility::makeInstance(ObjectManager::class)->get(Dispatcher::class); - $dispatcher->dispatch(__CLASS__, 'getUser', ['user' => $user]); + $dispatcher->dispatch(__CLASS__, 'getUser', [$user]); $event = new AuthenticationGetUserEvent($user); $eventDispatcher->dispatch($event); From b202f27f27dd28f794bd09bfee1639ad82bb0540 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Wed, 13 Mar 2024 20:39:14 +0100 Subject: [PATCH 092/142] [BUGFIX] No optional parameter before required one --- Classes/Controller/LoginController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php index a4f9bfe..eb753db 100644 --- a/Classes/Controller/LoginController.php +++ b/Classes/Controller/LoginController.php @@ -67,7 +67,7 @@ public function setContentObjectRenderer(ContentObjectRenderer $cObj) * @param string $_ ignored * @param array|null $pluginConfiguration */ - public function login(string $_ = '', ?array $pluginConfiguration) + public function login(string $_, ?array $pluginConfiguration) { if (is_array($pluginConfiguration)) { $this->pluginConfiguration = $pluginConfiguration; From c072f45751b206da01807a542db4c839c284c2dd Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Thu, 14 Mar 2024 15:43:38 +0100 Subject: [PATCH 093/142] [TASK] Code cleanup --- Classes/Service/AuthenticationService.php | 43 ++++++++--------------- ext_localconf.php | 18 +++++----- 2 files changed, 24 insertions(+), 37 deletions(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 2ad8cca..7f01683 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -57,26 +57,15 @@ */ class AuthenticationService extends \TYPO3\CMS\Core\Authentication\AuthenticationService { - - /** - * true - this service was able to authenticate the user - */ - const STATUS_AUTHENTICATION_SUCCESS_CONTINUE = true; - /** * 200 - authenticated and no more checking needed */ - const STATUS_AUTHENTICATION_SUCCESS_BREAK = 200; - - /** - * false - this service was the right one to authenticate the user, but it failed - */ - const STATUS_AUTHENTICATION_FAILURE_BREAK = false; + private const STATUS_AUTHENTICATION_SUCCESS_BREAK = 200; /** * 100 - just go on. User is not authenticated but there's still no reason to stop */ - const STATUS_AUTHENTICATION_FAILURE_CONTINUE = 100; + private const STATUS_AUTHENTICATION_FAILURE_CONTINUE = 100; /** * Global extension configuration @@ -142,19 +131,21 @@ public function getUser() } } - // dispatch a signal (containing the user with his access token if auth was successful) - // so other extensions can use them to make further requests to an API - // provided by the authentication server - /** @var Dispatcher $dispatcher */ - $dispatcher = GeneralUtility::makeInstance(ObjectManager::class)->get(Dispatcher::class); - $dispatcher->dispatch(__CLASS__, 'getUser', [$user]); + if ($user) { + // dispatch a signal (containing the user with his access token if auth was successful) + // so other extensions can use them to make further requests to an API + // provided by the authentication server + /** @var Dispatcher $dispatcher */ + $dispatcher = GeneralUtility::makeInstance(ObjectManager::class)->get(Dispatcher::class); + $dispatcher->dispatch(__CLASS__, 'getUser', [$user]); - $event = new AuthenticationGetUserEvent($user); - $eventDispatcher->dispatch($event); - $user = $event->getUser(); + $event = new AuthenticationGetUserEvent($user); + $eventDispatcher->dispatch($event); + $user = $event->getUser(); - if (isset($user['accessToken'])) { - unset($user['accessToken']); + if (isset($user['accessToken'])) { + unset($user['accessToken']); + } } return $user; @@ -171,7 +162,6 @@ protected function authenticateWithAuthorizationCode(string $code, string $codeV { $this->logger->debug('Initializing OpenID Connect service'); - /** @var OAuthService $service */ $service = GeneralUtility::makeInstance(OAuthService::class); $service->setSettings($this->config); @@ -528,9 +518,6 @@ protected function convertResourceOwner(array $info) $this->logger->debug('User record reloaded', $user); } - // We need that for the upcoming call to authUser() - $user['tx_oidc'] = true; - return $user; } diff --git a/ext_localconf.php b/ext_localconf.php index a0c6e04..5fbac75 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -1,5 +1,7 @@ get('oidc') ?? []; // Service configuration -$subTypesArr = []; $subTypes = ''; if ($settings['enableFrontendAuthentication'] ?? '') { - $subTypesArr[] = 'getUserFE'; - $subTypesArr[] = 'authUserFE'; - $subTypesArr[] = 'getGroupsFE'; -} -if (is_array($subTypesArr)) { - $subTypesArr = array_unique($subTypesArr); + $subTypesArr = [ + 'getUserFE', + 'authUserFE', + 'getGroupsFE', + ]; $subTypes = implode(',', $subTypesArr); } @@ -54,8 +54,8 @@ ] ); - // Add typoscript for custom login plugin - \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPItoST43('oidc', null, '_login'); +// Add typoscript for custom login plugin +ExtensionManagementUtility::addPItoST43('oidc', null, '_login'); // Require 3rd-party libraries, in case TYPO3 does not run in composer mode $pharFileName = ExtensionManagementUtility::extPath('oidc') . 'Libraries/league-oauth2-client.phar'; From 1ad3741f764dd87ca5ce77bea255b2f19026f384 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Thu, 21 Mar 2024 09:51:41 +0100 Subject: [PATCH 094/142] [TASK] Mark OAuthService public for DI Resolves #142 --- Configuration/Services.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index 424ab17..bed2f39 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -4,9 +4,15 @@ services: autoconfigure: true public: false + Causal\Oidc\: + resource: '../Classes/*' + Causal\Oidc\EventListener\FrontendLoginEventListener: tags: - name: event.listener identifier: 'causal/oidc' method: 'modifyLoginFormView' event: TYPO3\CMS\FrontendLogin\Event\ModifyLoginFormViewEvent + + Causal\Oidc\Service\OAuthService: + public: true From 981507cc3844d243cd83123e27d1ad08d905d58c Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Tue, 2 Apr 2024 07:57:21 +0200 Subject: [PATCH 095/142] [SECURITY] Prevent authentication of user from other service Ensure the "authUser" step in the authentication process only verdicts positive on user data discovered by this very service and not by any other auth service. Relying on the 'tx_oidc' field of the user data is not sufficient, as any user previously logged in has a value in this field. We secure this step by preserving the Access Token, supplied by the actual OpenID connect authentication process during TYPO3's "getUser" step, in the user's data. If this token is detected the in the "authUser" step, we got an actual login by our service. --- Classes/Service/AuthenticationService.php | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 7f01683..60db5d0 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -142,10 +142,6 @@ public function getUser() $event = new AuthenticationGetUserEvent($user); $eventDispatcher->dispatch($event); $user = $event->getUser(); - - if (isset($user['accessToken'])) { - unset($user['accessToken']); - } } return $user; @@ -288,10 +284,18 @@ protected function getUserFromAccessToken(OAuthService $service, AccessToken $ac */ public function authUser(array $user): int { - if (!empty($user['tx_oidc'])) { - return static::STATUS_AUTHENTICATION_SUCCESS_BREAK; + // missing access token means the actual OIDC authentication step in the `getUser` method failed + // or has neven been executed, if the user was discovered by some other authentication service + if (!isset($user['accessToken'])) { + return static::STATUS_AUTHENTICATION_FAILURE_CONTINUE; } - return static::STATUS_AUTHENTICATION_FAILURE_CONTINUE; + + // this is not a valid user authenticated via OIDC + if (empty($user['tx_oidc'])) { + return static::STATUS_AUTHENTICATION_FAILURE_CONTINUE; + } + + return static::STATUS_AUTHENTICATION_SUCCESS_BREAK; } /** From c69d95df6cbc605ba77203b4ebe6b7b659279043 Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Tue, 2 Apr 2024 08:02:54 +0200 Subject: [PATCH 096/142] [TASK] Reformat README.md --- README.md | 60 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index e615e50..1ff843c 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,20 @@ # OpenID Connect -This extension lets you authenticate Frontend users against an OpenID Connect server. It is preconfigured to work with -the [WSO2 Identity Server](https://wso2.com/identity-and-access-management/) from the Swiss Alpine Club but may be used -with your own identity server as well. +This extension lets you authenticate Frontend users against an OpenID Connect +server. It is preconfigured to work with the +[WSO2 Identity Server](https://wso2.com/identity-and-access-management/) from +the Swiss Alpine Club but may be used with your own identity server as well. -If you are a Swiss Alpine Club section, be sure to get in touch with Bern in order to get your dedicated private key and -secret. +If you are a Swiss Alpine Club section, be sure to get in touch with Bern in +order to get your dedicated private key and secret. ## Default FE Loginbox This extension integrates with the system extension 'felogin'. -This FLUID markup can be used to include a link to the authorization endpoint of the authorization server. +This Fluid markup can be used to include a link to the authorization endpoint of +the authorization server. ```html @@ -29,29 +31,31 @@ See also `Resources/Private/Templates/Login/Login.html` as reference. ## OIDC Login -If openid_connect is your only means of frontend login, you can use the included "OIDC Login" plugin. Add it to your -login page, where you would normally add the felogin box. After adding the OIDC Login plugin, requests to the login -page will immediately be redirected to the authorization server. +If openid_connect is your only means of frontend login, you can use the included +"OIDC Login" plugin. Add it to your login page, where you would normally add the +felogin box. After adding the OIDC Login plugin, requests to the login page will +immediately be redirected to the authorization server. After the login process, the user will be redirected: - The OIDC Login supports the same `redirect_url` parameter as the felogin box -- If no parameter is set, OIDC Login will redirect the user to the page configured at - `plugin.tx_oidc_login.defaultRedirectPid`. +- If no parameter is set, OIDC Login will redirect the user to the page + configured at `plugin.tx_oidc_login.defaultRedirectPid`. - If that configuration is not set either, the user will be redirected to '/'. ## PKCE (Proof of Key for Code Exchange) -If your OIDC Login supports _Proof of Key for Code Exchange_ you can enable it by -checking `enableCodeVerifier` in the extension configuration. A shared secret will -be sent along preventing _Authorization Code Interception Attacks_. See +If your OIDC Login supports _Proof of Key for Code Exchange_ you can enable it +by checking `enableCodeVerifier` in the extension configuration. A shared secret +will be sent along preventing _Authorization Code Interception Attacks_. See https://tools.ietf.org/html/rfc7636 for details. ## Configuring ### Mapping Frontend User Fields -- Configuration is done through TypoScript within `plugin.tx_oidc.mapping.fe_users` +- Configuration is done through TypoScript within + `plugin.tx_oidc.mapping.fe_users` - OIDC attributes will be recognized by the specific characters `<>`: ``` @@ -73,7 +77,8 @@ https://tools.ietf.org/html/rfc7636 for details. ``` - Support for [TypoScript "split"](https://docs.typo3.org/m/typo3/reference-typoscript/master/en-us/Functions/Stdwrap.html#data) - (`//`). This will check multiple field names and return the first one yielding some non-empty value. E.g., + (`//`). This will check multiple field names and return the first one yielding + some non-empty value. E.g., ``` username = // // // @@ -88,26 +93,27 @@ https://tools.ietf.org/html/rfc7636 for details. ### OIDC Login -- `plugin.tx_oidc_login.defaultRedirectPid` UID of the page that users will be redirected to, if no `redirect_url` -parameter is set. +- `plugin.tx_oidc_login.defaultRedirectPid` UID of the page that users will be + redirected to, if no `redirect_url` parameter is set. ## Logging -This extension makes use of the Logging system introduced in TYPO3 CMS 6.0. It is far more flexible than the old one -writing to the "sys_log" table. Technical details may be found in the -[TYPO3 Core API](https://docs.typo3.org/m/typo3/reference-coreapi/master/en-us/ApiOverview/Logging/Index.html#logging). +This extension makes use of the Logging system introduced in TYPO3 CMS 6.0. It +is far more flexible than the old one writing to the "sys_log" table. Technical +details may be found in the [TYPO3 Core API](https://docs.typo3.org/m/typo3/reference-coreapi/master/en-us/ApiOverview/Logging/Index.html#logging). -As an administrator, what you should know is that the TYPO3 Logger forwards log records to "Writers", which persist the -log record. +As an administrator, what you should know is that the TYPO3 Logger forwards log +records to "Writers", which persist the log record. -By default, with a vanilla TYPO3 installation, messages are written to the default log file -(`var/log/typo3_*.log`). +By default, with a vanilla TYPO3 installation, messages are written to the +default log file (`var/log/typo3_*.log`). ### Dedicated Log File for OpenID Connect -If you want to redirect every logging information from this extension to `var/log/oidc.log` and send log -entries with level "WARNING" or above to the system log, you may add following configuration to +If you want to redirect every logging information from this extension to +`var/log/oidc.log` and send log entries with level "WARNING" or above to the +system log, you may add following configuration to `typo3conf/AdditionalConfiguration.php`: ``` From dd0125f120d799447aa1b99e8bb3ee2b9b948b29 Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Tue, 2 Apr 2024 08:03:07 +0200 Subject: [PATCH 097/142] [TASK] Raise version to 2.1.0 --- composer.json | 2 +- ext_emconf.php | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 98d3597..9078eab 100644 --- a/composer.json +++ b/composer.json @@ -61,7 +61,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "2.1.x-dev" }, "typo3/cms": { "app-dir": ".Build", diff --git a/ext_emconf.php b/ext_emconf.php index e7e0221..8913378 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -7,11 +7,7 @@ 'author_company' => 'Causal Sàrl', 'author_email' => 'xavier@causal.ch', 'state' => 'stable', - 'uploadfolder' => 0, - 'createDirs' => '', - 'modify_tables' => '', - 'clearCacheOnLoad' => 0, - 'version' => '2.0.0', + 'version' => '2.1.0', 'constraints' => [ 'depends' => [ 'php' => '7.4.0-8.3.99', @@ -21,4 +17,3 @@ 'suggests' => [], ], ]; - From 53d5519fa9788ddbeec2ee1967703be132afeba3 Mon Sep 17 00:00:00 2001 From: Denis Lorch Date: Mon, 8 Apr 2024 11:12:33 +0200 Subject: [PATCH 098/142] Improve type declarations in AuthenticationGetuserGroupsEvent --- .../AuthenticationGetUserGroupsEvent.php | 37 +++++-------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/Classes/Event/AuthenticationGetUserGroupsEvent.php b/Classes/Event/AuthenticationGetUserGroupsEvent.php index 48f5dc3..fe4d7f2 100644 --- a/Classes/Event/AuthenticationGetUserGroupsEvent.php +++ b/Classes/Event/AuthenticationGetUserGroupsEvent.php @@ -10,60 +10,41 @@ */ final class AuthenticationGetUserGroupsEvent { - /** - * @var array - */ - protected $groupTable; - /** - * @var array - */ - protected $groups; - /** - * @var array - */ - protected $resource; + protected string $groupTable; + protected array $groups; + protected array $resource; /** * @param string $groupTable - fe_groups or be_groups * @param array $groups - known user group ids * @param array $resourceOwner - resource owner data */ - public function __construct($groupTable, $groups, $resourceOwner) + public function __construct(string $groupTable, array $groups, array $resourceOwner) { - $this->groupTable = (string)$groupTable; + $this->groupTable = $groupTable; $this->groups = $groups; $this->resource = $resourceOwner; } - /** - * @return string - */ - public function getGroupTable() + public function getGroupTable(): string { return $this->groupTable; } - /** - * @return array - */ - public function getUserGroups() + public function getUserGroups(): array { return $this->groups; } - /** - * @return array - */ - public function getResource() + public function getResource(): array { return $this->resource; } /** * Set your customized user group ids - * @param array $groups */ - public function setUserGroups($groups): void + public function setUserGroups(array $groups): void { $this->groups = $groups; } From 65e90242a76fd5a5b2e20b90bfa7619c91cc261a Mon Sep 17 00:00:00 2001 From: Hannes Lau Date: Tue, 9 Apr 2024 16:41:51 +0200 Subject: [PATCH 099/142] [BUGFIX] #146 Respect authorize URL options without PKCE --- Classes/Controller/LoginController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php index eb753db..8ae5d6f 100644 --- a/Classes/Controller/LoginController.php +++ b/Classes/Controller/LoginController.php @@ -95,10 +95,10 @@ protected function performRedirectToLogin(array $authorizationUrlOptions = []) if ($this->settings['enableCodeVerifier']) { $codeVerifier = $this->generateCodeVerifier(); $codeChallenge = $this->convertVerifierToChallenge($codeVerifier); - $options = $this->addCodeChallengeToOptions($codeChallenge, $authorizationUrlOptions); + $authorizationUrlOptions = $this->addCodeChallengeToOptions($codeChallenge, $authorizationUrlOptions); $_SESSION['oidc_code_verifier'] = $codeVerifier; } - $authorizationUrl = $service->getAuthorizationUrl($options); + $authorizationUrl = $service->getAuthorizationUrl($authorizationUrlOptions); $state = $service->getState(); $_SESSION['oidc_state'] = $state; From f45b85adc329b1411c034faa9b78b21648f27443 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Fri, 12 Apr 2024 11:47:00 +0200 Subject: [PATCH 100/142] [BUGFIX] Only request provider default scopes if available Resolves: #148 --- Classes/Service/OAuthService.php | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/Classes/Service/OAuthService.php b/Classes/Service/OAuthService.php index 4583d60..8401da9 100644 --- a/Classes/Service/OAuthService.php +++ b/Classes/Service/OAuthService.php @@ -20,6 +20,8 @@ use Causal\Oidc\Factory\GenericOAuthProviderFactory; use Causal\Oidc\Factory\OAuthProviderFactoryInterface; use GuzzleHttp\RequestOptions; +use League\OAuth2\Client\Grant\AuthorizationCode; +use League\OAuth2\Client\Grant\Password; use League\OAuth2\Client\Provider\AbstractProvider; use League\OAuth2\Client\Provider\Exception\IdentityProviderException; use League\OAuth2\Client\Provider\ResourceOwnerInterface; @@ -98,21 +100,25 @@ public function getState(): string public function getAccessToken(string $codeOrUsername, ?string $password = null, ?string $codeVerifier = null): AccessToken { if ($password === null) { - $options = ['code' => $codeOrUsername]; + $options = [ + 'code' => $codeOrUsername, + ]; if ($codeVerifier !== null) { $options['code_verifier'] = $codeVerifier; } - $accessToken = $this->getProvider()->getAccessToken('authorization_code', $options); + $grant = new AuthorizationCode(); } else { - $accessToken = $this->getProvider()->getAccessToken('password', [ + $options = [ 'username' => $codeOrUsername, 'password' => $password, + ]; + if (is_callable([$this->getProvider(), 'getDefaultScopes'])) { // Oddly, the client does not send scope along automatically but WSO2 expects it anyway... - 'scope' => implode(',', $this->getProvider()->getDefaultScopes()), - ]); + $options['scope'] = implode(',', $this->getProvider()->getDefaultScopes()); + } + $grant = new Password(); } - - return $accessToken; + return $this->getProvider()->getAccessToken($grant, $options); } /** From 2a3ca8e378c33754000e938439449abb35b091b2 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Fri, 3 May 2024 13:44:04 +0200 Subject: [PATCH 101/142] [TASK] Use default scopes from ext config for token via password grant Resolves: #148 --- Classes/Service/OAuthService.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Classes/Service/OAuthService.php b/Classes/Service/OAuthService.php index 8401da9..f77a310 100644 --- a/Classes/Service/OAuthService.php +++ b/Classes/Service/OAuthService.php @@ -111,9 +111,11 @@ public function getAccessToken(string $codeOrUsername, ?string $password = null, $options = [ 'username' => $codeOrUsername, 'password' => $password, + 'scope' => $this->settings['oidcClientScopes'], ]; + // The GenericProvider has this as a public function (contrary to the interface), + // so we use its scopes instead as there might be some modified provider. if (is_callable([$this->getProvider(), 'getDefaultScopes'])) { - // Oddly, the client does not send scope along automatically but WSO2 expects it anyway... $options['scope'] = implode(',', $this->getProvider()->getDefaultScopes()); } $grant = new Password(); From 235b013a5e92da25127e0c44ba67d8da0e0b30b3 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Fri, 3 May 2024 14:13:57 +0200 Subject: [PATCH 102/142] [BUGFIX] Cope with missing TSFE If for some reason TSFE is not available, use "en" as fallback language. Resolves: #138 --- Classes/Service/OAuthService.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Classes/Service/OAuthService.php b/Classes/Service/OAuthService.php index f77a310..ad163f4 100644 --- a/Classes/Service/OAuthService.php +++ b/Classes/Service/OAuthService.php @@ -68,7 +68,7 @@ public function getAuthorizationUrl(array $options = []): string if (!empty($this->settings['oidcAuthorizeLanguageParameter'])) { $languageOption = $this->settings['oidcAuthorizeLanguageParameter']; if (!empty($languageOption)) { - $language = $this->getTSFE()->getLanguage()->getTwoLetterIsoCode(); + $language = $this->getTSFE() ? $this->getTSFE()->getLanguage()->getTwoLetterIsoCode() : 'en'; $options[$languageOption] = $language; } } @@ -271,8 +271,8 @@ protected function getRedirectUrl(): string return $this->settings['oidcRedirectUri'] ?: GeneralUtility::getIndpEnv('TYPO3_SITE_URL'); } - protected function getTSFE(): TypoScriptFrontendController + protected function getTSFE(): ? TypoScriptFrontendController { - return $GLOBALS['TSFE']; + return $GLOBALS['TSFE'] ?? null; } } From 3ae3a07b725baaad44c49b817b081e775e248a8d Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Fri, 3 May 2024 14:26:15 +0200 Subject: [PATCH 103/142] [TASK] Enhance event data Include a reference to the calling authentication service in the event's data. Resolves: #136 --- CHANGELOG.md | 5 +++++ Classes/Event/AuthenticationGetUserEvent.php | 12 +++++++++++- Classes/Event/AuthenticationPreUserEvent.php | 12 +++++++++++- Classes/Event/ModifyResourceOwnerEvent.php | 12 +++++++++++- Classes/Event/ModifyUserEvent.php | 12 +++++++++++- Classes/Service/AuthenticationService.php | 10 +++++----- 6 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0dcc3f2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# TYPO3 OpenID Connect integration changelog + +## Version 2.x + +* Enhanced events to include a reference to the AuthenticationService [#136](https://github.com/xperseguers/t3ext-oidc/issues/136) diff --git a/Classes/Event/AuthenticationGetUserEvent.php b/Classes/Event/AuthenticationGetUserEvent.php index 1200978..1aaf659 100644 --- a/Classes/Event/AuthenticationGetUserEvent.php +++ b/Classes/Event/AuthenticationGetUserEvent.php @@ -17,6 +17,8 @@ namespace Causal\Oidc\Event; +use TYPO3\CMS\Core\Authentication\AbstractAuthenticationService; + final class AuthenticationGetUserEvent { /** @@ -24,12 +26,15 @@ final class AuthenticationGetUserEvent */ protected $user; + protected AbstractAuthenticationService $authenticationService; + /** * @param array|bool $user */ - public function __construct($user) + public function __construct($user, AbstractAuthenticationService $authenticationService) { $this->user = $user; + $this->authenticationService = $authenticationService; } /** @@ -48,4 +53,9 @@ public function setUser($user): void { $this->user = $user; } + + public function getAuthenticationService(): AbstractAuthenticationService + { + return $this->authenticationService; + } } diff --git a/Classes/Event/AuthenticationPreUserEvent.php b/Classes/Event/AuthenticationPreUserEvent.php index 490f35a..e7633c3 100644 --- a/Classes/Event/AuthenticationPreUserEvent.php +++ b/Classes/Event/AuthenticationPreUserEvent.php @@ -17,13 +17,23 @@ namespace Causal\Oidc\Event; +use TYPO3\CMS\Core\Authentication\AbstractAuthenticationService; + final class AuthenticationPreUserEvent { public array $loginData = []; + + protected AbstractAuthenticationService $authenticationService; + public bool $shouldProcess = true; - public function __construct(array $loginData) + public function __construct(array $loginData, AbstractAuthenticationService $authenticationService) { $this->loginData = $loginData; } + + public function getAuthenticationService(): AbstractAuthenticationService + { + return $this->authenticationService; + } } diff --git a/Classes/Event/ModifyResourceOwnerEvent.php b/Classes/Event/ModifyResourceOwnerEvent.php index a8caf31..b407011 100644 --- a/Classes/Event/ModifyResourceOwnerEvent.php +++ b/Classes/Event/ModifyResourceOwnerEvent.php @@ -17,6 +17,8 @@ namespace Causal\Oidc\Event; +use TYPO3\CMS\Core\Authentication\AbstractAuthenticationService; + final class ModifyResourceOwnerEvent { /** @@ -24,9 +26,12 @@ final class ModifyResourceOwnerEvent */ protected array $resourceOwner; - public function __construct(array $resourceOwner) + protected AbstractAuthenticationService $authenticationService; + + public function __construct(array $resourceOwner, AbstractAuthenticationService $authenticationService) { $this->resourceOwner = $resourceOwner; + $this->authenticationService = $authenticationService; } public function getResourceOwner(): array @@ -38,4 +43,9 @@ public function setResourceOwner(array $resourceOwner): void { $this->resourceOwner = $resourceOwner; } + + public function getAuthenticationService(): AbstractAuthenticationService + { + return $this->authenticationService; + } } diff --git a/Classes/Event/ModifyUserEvent.php b/Classes/Event/ModifyUserEvent.php index 1786e04..67c1b8e 100644 --- a/Classes/Event/ModifyUserEvent.php +++ b/Classes/Event/ModifyUserEvent.php @@ -17,6 +17,8 @@ namespace Causal\Oidc\Event; +use TYPO3\CMS\Core\Authentication\AbstractAuthenticationService; + final class ModifyUserEvent { /** @@ -24,9 +26,12 @@ final class ModifyUserEvent */ protected array $user; - public function __construct(array $user) + protected AbstractAuthenticationService $authenticationService; + + public function __construct(array $user, AbstractAuthenticationService $authenticationService) { $this->user = $user; + $this->authenticationService = $authenticationService; } public function getUser(): array @@ -38,4 +43,9 @@ public function setUser(array $user): void { $this->user = $user; } + + public function getAuthenticationService(): AbstractAuthenticationService + { + return $this->authenticationService; + } } diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 60db5d0..32229c8 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -111,7 +111,7 @@ public function getUser() } $user = $this->authenticateWithAuthorizationCode($code, $codeVerifier); } else { - $event = new AuthenticationPreUserEvent($this->login); + $event = new AuthenticationPreUserEvent($this->login, $this); $eventDispatcher->dispatch($event); if (!$event->shouldProcess) { return false; @@ -139,7 +139,7 @@ public function getUser() $dispatcher = GeneralUtility::makeInstance(ObjectManager::class)->get(Dispatcher::class); $dispatcher->dispatch(__CLASS__, 'getUser', [$user]); - $event = new AuthenticationGetUserEvent($user); + $event = new AuthenticationGetUserEvent($user, $this); $eventDispatcher->dispatch($event); $user = $event->getUser(); } @@ -259,7 +259,7 @@ protected function getUserFromAccessToken(OAuthService $service, AccessToken $ac ); } - $event = new ModifyResourceOwnerEvent($resourceOwner); + $event = new ModifyResourceOwnerEvent($resourceOwner, $this); $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); $eventDispatcher->dispatch($event); @@ -446,7 +446,7 @@ protected function convertResourceOwner(array $info) $data['usergroup'] = implode(',', $newUserGroups); $user = array_merge($row, $data); - $event = new ModifyUserEvent($user); + $event = new ModifyUserEvent($user, $this); $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); $eventDispatcher->dispatch($event); $user = $event->getUser(); @@ -480,7 +480,7 @@ protected function convertResourceOwner(array $info) 'tx_oidc' => $info['sub'], ]); - $event = new ModifyUserEvent($data); + $event = new ModifyUserEvent($data, $this); $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); $eventDispatcher->dispatch($event); $data = $event->getUser(); From ef44e8667295946b44c1e856412dd3d7f6f5bbec Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Tue, 14 May 2024 17:09:00 +0200 Subject: [PATCH 104/142] [TASK] Handle code_verifier without error suppression --- Classes/Service/AuthenticationService.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 60db5d0..71ff37e 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -82,12 +82,12 @@ public function __construct() $this->config = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('oidc') ?? []; } - protected function getCodeVerifierFromSession() + protected function getCodeVerifierFromSession(): ?string { if (session_id() === '') { session_start(); } - return @$_SESSION['oidc_code_verifier']; + return $_SESSION['oidc_code_verifier'] ?? null; } /** @@ -154,7 +154,7 @@ public function getUser() * @param string|null $codeVerifier * @return array|bool */ - protected function authenticateWithAuthorizationCode(string $code, string $codeVerifier = null) + protected function authenticateWithAuthorizationCode(string $code, ?string $codeVerifier) { $this->logger->debug('Initializing OpenID Connect service'); From 3ef75792673b2ff6776bdbe453d344e20fd7ea7d Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Tue, 14 May 2024 17:12:18 +0200 Subject: [PATCH 105/142] [DOC] Document PHP session usage --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 1ff843c..bb5d706 100644 --- a/README.md +++ b/README.md @@ -135,3 +135,18 @@ $GLOBALS['TYPO3_CONF_VARS']['LOG']['Causal']['Oidc']['writerConfiguration'] = [ **Hint:** Be sure to read [Configuration of the Logging system](https://docs.typo3.org/m/typo3/reference-coreapi/master/en-us/ApiOverview/Logging/Configuration/Index.html#logging-configuration) to fine-tune your configuration on any production website. + +## Session handling + +This extension is using PHP's native sessions (`$_SESSION`) to store state about +ongoing authentication attempts. + +The following data is stored: + +* `oidc_state` +* `oidc_login_url` +* `oidc_authorization_url` +* `oidc_redirect_url` +* `oidc_code_verifier` +* `requestId` + From 84eca124adce5a96820210c5bf8c81f965d4b213 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Wed, 15 May 2024 12:54:14 +0200 Subject: [PATCH 106/142] [TASK] Replace deprecated redirectToUri method --- Classes/Controller/AuthenticationController.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Classes/Controller/AuthenticationController.php b/Classes/Controller/AuthenticationController.php index c12b41b..b76b94d 100644 --- a/Classes/Controller/AuthenticationController.php +++ b/Classes/Controller/AuthenticationController.php @@ -17,10 +17,14 @@ namespace Causal\Oidc\Controller; +use Psr\Http\Message\ResponseInterface; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use RuntimeException; +use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationExtensionNotConfiguredException; +use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationPathDoesNotExistException; use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; +use TYPO3\CMS\Core\Http\RedirectResponse; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; @@ -39,6 +43,8 @@ class AuthenticationController extends ActionController implements LoggerAwareIn * Initializes the controller before invoking an action method. * * @return void + * @throws ExtensionConfigurationExtensionNotConfiguredException + * @throws ExtensionConfigurationPathDoesNotExistException */ public function initializeAction() { @@ -47,10 +53,8 @@ public function initializeAction() /** * Initiates the silent authentication action. - * - * @return void */ - public function connectAction() + public function connectAction(): ResponseInterface { $this->logger->debug('Initiating the silent authentication'); if ((empty($_GET['state']) || empty($_GET['code']))) { @@ -86,6 +90,6 @@ public function connectAction() } $this->logger->info('Redirecting to login URL', ['url' => $loginUrl]); - $this->redirectToUri($loginUrl); + return new RedirectResponse($this->addBaseUriIfNecessary($loginUrl), 303); } } From 82e433edbf4567af464445206e3d3f9381db9675 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Wed, 15 May 2024 13:20:22 +0200 Subject: [PATCH 107/142] [TASK] Store request object in LoginController Avoids accessing the global --- Classes/Controller/LoginController.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php index 8ae5d6f..7ab5d23 100644 --- a/Classes/Controller/LoginController.php +++ b/Classes/Controller/LoginController.php @@ -21,6 +21,7 @@ use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; use TYPO3\CMS\Core\Http\PropagateResponseException; use TYPO3\CMS\Core\Http\RedirectResponse; +use TYPO3\CMS\Core\Http\ServerRequest; use TYPO3\CMS\Core\Http\ServerRequestFactory; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; @@ -46,6 +47,8 @@ class LoginController */ public ?ContentObjectRenderer $cObj = null; + protected ServerRequest $request; + public function __construct() { $this->settings = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('oidc') ?? []; @@ -73,8 +76,8 @@ public function login(string $_, ?array $pluginConfiguration) $this->pluginConfiguration = $pluginConfiguration; } - $request = ServerRequestFactory::fromGlobals(); - $loginType = $request->getParsedBody()['logintype'] ?? $request->getQueryParams()['logintype'] ?? ''; + $this->request = ServerRequestFactory::fromGlobals(); + $loginType = $this->request->getParsedBody()['logintype'] ?? $this->request->getQueryParams()['logintype'] ?? ''; if ($loginType === 'login') { // performRedirectAfterLogin stops flow by emitting a redirect $this->performRedirectAfterLogin(); @@ -91,7 +94,6 @@ protected function performRedirectToLogin(array $authorizationUrlOptions = []) if (session_id() === '') { session_start(); } - $options = []; if ($this->settings['enableCodeVerifier']) { $codeVerifier = $this->generateCodeVerifier(); $codeChallenge = $this->convertVerifierToChallenge($codeVerifier); @@ -117,8 +119,7 @@ protected function performRedirectAfterLogin() protected function determineRedirectUrl() { - $request = $GLOBALS['TYPO3_REQUEST']; - $redirectUrl = $request->getParsedBody()['redirect_url'] ?? $request->getQueryParams()['redirect_url'] ?? ''; + $redirectUrl = $this->request->getParsedBody()['redirect_url'] ?? $this->request->getQueryParams()['redirect_url'] ?? ''; if (!empty($redirectUrl)) { return $redirectUrl; } @@ -148,7 +149,7 @@ protected function convertVerifierToChallenge($codeVerifier): string return rtrim(strtr(base64_encode(hash('sha256', $codeVerifier, true)), '+/', '-_'), '='); } - protected function addCodeChallengeToOptions($codeChallenge, array $options = []): array + protected function addCodeChallengeToOptions($codeChallenge, array $options): array { return array_merge( $options, From 770d8127b59eb97508f33bf2fdb4f714026f5814 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Wed, 15 May 2024 14:42:01 +0200 Subject: [PATCH 108/142] [TASK] Introduce OpenIdConnectService Centralize duplicate code from viewhelper and event listener. --- .../FrontendLoginEventListener.php | 108 ++------------- Classes/Service/OpenIdConnectService.php | 117 ++++++++++++++++ Classes/ViewHelpers/OidcLinkViewHelper.php | 129 ++---------------- Configuration/Services.yaml | 3 + 4 files changed, 138 insertions(+), 219 deletions(-) create mode 100644 Classes/Service/OpenIdConnectService.php diff --git a/Classes/EventListener/FrontendLoginEventListener.php b/Classes/EventListener/FrontendLoginEventListener.php index fb719eb..c19f752 100644 --- a/Classes/EventListener/FrontendLoginEventListener.php +++ b/Classes/EventListener/FrontendLoginEventListener.php @@ -1,5 +1,7 @@ get('oidc') ?? []; - - if (empty($settings['oidcClientKey']) - || empty($settings['oidcClientSecret']) - || empty($settings['oidcEndpointAuthorize']) - || empty($settings['oidcEndpointToken']) - ) { - return; - } - - $requestId = $this->getUniqueId(); - $this->logger->debug('Post-processing felogin form', ['request' => $requestId]); - - if (session_id() === '') { // If no session exists, start a new one - $this->logger->debug('No PHP session found'); - session_start(); + $authService = GeneralUtility::makeInstance(OpenIdConnectService::class); + try { + $uri = $authService->generateOpenidConnectUri(); + } catch (\InvalidArgumentException $e) { + $uri = '#InvalidOIDCConfiguration'; } - - if (empty($_SESSION['requestId']) || $_SESSION['requestId'] !== $requestId) { - $this->prepareAuthorizationUrl($settings); - - $request = $GLOBALS['TYPO3_REQUEST']; - $redirectUrl = $request->getParsedBody()['redirect_url'] ?? $request->getQueryParams()['redirect_url'] ?? ''; - - $_SESSION['requestId'] = $requestId; - $_SESSION['oidc_redirect_url'] = $redirectUrl; - - $this->logger->debug('PHP session is available', [ - 'id' => session_id(), - 'data' => $_SESSION, - ]); - } else { - $this->logger->debug('Reusing same authorization URL and state'); - } - - $event->getView()->assign('openidConnectUri', $_SESSION['oidc_authorization_url']); - } - - /** - * Prepares the authorization URL and corresponding expected state (to mitigate CSRF attack) - * and stores information into the session. - * - * @param array $settings - */ - protected function prepareAuthorizationUrl(array $settings): void - { - /** @var OAuthService $service */ - $service = GeneralUtility::makeInstance(OAuthService::class); - $service->setSettings($settings); - $authorizationUrl = $service->getAuthorizationUrl(); - - // Store the state - $state = $service->getState(); - - $this->logger->debug('Generating authorization URL', [ - 'url' => $authorizationUrl, - 'state' => $state, - ]); - - $loginUrl = GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL'); - // Sanitize the URL - $parts = parse_url($loginUrl); - $queryParts = array_filter(explode('&', $parts['query'] ?? ''), function ($v) { - list ($k,) = explode('=', $v, 2); - - return !in_array($k, ['logintype', 'tx_oidc[code]']); - }); - $parts['query'] = implode('&', $queryParts); - $loginUrl = $parts['scheme'] . '://' . $parts['host']; - if (!empty($parts['port']) && !in_array((int)$parts['port'], [80, 443], true)) { - $loginUrl .= ':' . $parts['port']; - } - $loginUrl .= $parts['path']; - if (!empty($parts['query'])) { - $loginUrl .= '?' . $parts['query']; - } - - $_SESSION['oidc_state'] = $state; - $_SESSION['oidc_login_url'] = $loginUrl; - $_SESSION['oidc_authorization_url'] = $authorizationUrl; - } - - /** - * Returns a unique ID for the current processed request. - * - * This is supposed to be independent of the actual web server (Nginx or Apache) and - * the way PHP was built and unique enough for our use case, as opposed to using: - * - * - zend_thread_id() which requires PHP to be built with Zend Thread Safety - ZTS - support and debug mode - * - apache_getenv('UNIQUE_ID') which requires Apache as web server and mod_unique_id - * - * @return string - */ - protected function getUniqueId(): string - { - return sprintf('%08x', abs(crc32($_SERVER['REMOTE_ADDR'] . $_SERVER['REQUEST_TIME'] . $_SERVER['REMOTE_PORT']))); + $event->getView()->assign('openidConnectUri', $uri); } } diff --git a/Classes/Service/OpenIdConnectService.php b/Classes/Service/OpenIdConnectService.php new file mode 100644 index 0000000..60bc89a --- /dev/null +++ b/Classes/Service/OpenIdConnectService.php @@ -0,0 +1,117 @@ +OAuthService = $OAuthService; + $this->config = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('oidc') ?? []; + } + + public function generateOpenidConnectUri(): string + { + if (empty($this->config['oidcClientKey']) + || empty($this->config['oidcClientSecret']) + || empty($this->config['oidcEndpointAuthorize']) + || empty($this->config['oidcEndpointToken']) + ) { + throw new InvalidArgumentException('Missing extension configuration', 1715775147); + } + + $requestId = $this->getUniqueId(); + + $this->logger->debug('Generating OpenID Connect URI', ['request' => $requestId]); + + if (session_id() === '') { // If no session exists, start a new one + $this->logger->debug('No PHP session found'); + session_start(); + } + + if (empty($_SESSION['requestId']) || $_SESSION['requestId'] !== $requestId) { + $request = $GLOBALS['TYPO3_REQUEST']; + $redirectUrl = $request->getParsedBody()['redirect_url'] ?? $request->getQueryParams()['redirect_url'] ?? ''; + + $data = $this->prepareAuthorizationUrl(); + $_SESSION['oidc_state'] = $data['state']; + $_SESSION['oidc_login_url'] = $data['login_url']; + $_SESSION['oidc_authorization_url'] = $data['authorization_url']; + $_SESSION['requestId'] = $requestId; + $_SESSION['oidc_redirect_url'] = $redirectUrl; + + $this->logger->debug('PHP session is available', [ + 'id' => session_id(), + 'data' => $_SESSION, + ]); + } else { + $this->logger->debug('Reusing same authorization URL and state'); + } + return $_SESSION['oidc_authorization_url'] ?? ''; + } + + /** + * Prepares the authorization URL and corresponding expected state (to mitigate CSRF attack) + * and stores information into the session. + */ + protected function prepareAuthorizationUrl(): array + { + $this->OAuthService->setSettings($this->config); + + $authorizationUrl = $this->OAuthService->getAuthorizationUrl(); + $state = $this->OAuthService->getState(); + + $this->logger->debug('Generating authorization URL', [ + 'url' => $authorizationUrl, + 'state' => $state, + ]); + + $loginUrl = new Uri(GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL')); + + // filter query string + $queryParts = array_filter(explode('&', $loginUrl->getQuery()), function ($k) { + return $k !== 'logintype' && $k !== 'tx_oidc[code]'; + }, ARRAY_FILTER_USE_KEY); + $loginUrl = $loginUrl->withQuery(implode('&', $queryParts)); + + return [ + 'state' => $state, + 'login_url' => (string)$loginUrl, + 'authorization_url' => $authorizationUrl + ]; + } + + /** + * Returns a unique ID for the current processed request. + * + * This is supposed to be independent of the actual web server (Nginx or Apache) and + * the way PHP was built and unique enough for our use case, as opposed to using: + * + * - zend_thread_id() which requires PHP to be built with Zend Thread Safety - ZTS - support and debug mode + * - apache_getenv('UNIQUE_ID') which requires Apache as web server and mod_unique_id + * + * @return string + */ + protected function getUniqueId(): string + { + return sprintf('%08x', abs(crc32($_SERVER['REMOTE_ADDR'] . $_SERVER['REQUEST_TIME'] . $_SERVER['REMOTE_PORT']))); + } +} diff --git a/Classes/ViewHelpers/OidcLinkViewHelper.php b/Classes/ViewHelpers/OidcLinkViewHelper.php index f27e78d..80ace13 100644 --- a/Classes/ViewHelpers/OidcLinkViewHelper.php +++ b/Classes/ViewHelpers/OidcLinkViewHelper.php @@ -17,9 +17,8 @@ namespace Causal\Oidc\ViewHelpers; -use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; -use TYPO3\CMS\Core\Log\Logger; -use TYPO3\CMS\Core\Log\LogManager; +use Causal\Oidc\Service\OpenIdConnectService; +use InvalidArgumentException; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper; @@ -34,127 +33,17 @@ class OidcLinkViewHelper extends AbstractViewHelper * @param array $arguments * @param \Closure $renderChildrenClosure * @param RenderingContextInterface $renderingContext - * @return string link + * @return string URI */ public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext) { - - $requestId = self::getUniqueId(); - static::getLogger()->debug('Post-processing markers for felogin form', ['request' => $requestId]); - - $settings = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('oidc') ?? []; - - if (empty($settings['oidcClientKey']) - || empty($settings['oidcClientSecret']) - || empty($settings['oidcEndpointAuthorize']) - || empty($settings['oidcEndpointToken']) - ) { - $link = 'Invalid OpenID Connect configuration'; - } else { - if (session_id() === '') { // If no session exists, start a new one - static::getLogger()->debug('No PHP session found'); - session_start(); - } - - if (empty($_SESSION['requestId']) || $_SESSION['requestId'] !== $requestId) { - self::prepareAuthorizationUrl($settings); - - $request = $GLOBALS['TYPO3_REQUEST']; - $redirectUrl = $request->getParsedBody()['redirect_url'] ?? $request->getQueryParams()['redirect_url'] ?? ''; - - $_SESSION['requestId'] = $requestId; - $_SESSION['oidc_redirect_url'] = $redirectUrl; - - static::getLogger()->debug('PHP session is available', [ - 'id' => session_id(), - 'data' => $_SESSION, - ]); - } else { - static::getLogger()->debug('Reusing same authorization URL and state'); - } - - $link = $_SESSION['oidc_authorization_url']; - } - - return $link; - } - - /** - * Prepares the authorization URL and corresponding expected state (to mitigate CSRF attack) - * and stores information into the session. - * - * @param array $settings - * @return void - */ - protected static function prepareAuthorizationUrl(array $settings) - { - /** @var \Causal\Oidc\Service\OAuthService $service */ - $service = GeneralUtility::makeInstance(\Causal\Oidc\Service\OAuthService::class); - $service->setSettings($settings); - $authorizationUrl = $service->getAuthorizationUrl(); - - // Store the state - $state = $service->getState(); - - static::getLogger()->debug('Generating authorization URL', [ - 'url' => $authorizationUrl, - 'state' => $state, - ]); - - $loginUrl = GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL'); - // Sanitize the URL - $parts = parse_url($loginUrl); - $queryParts = array_filter(explode('&', $parts['query']), function ($v) { - [$k,] = explode('=', $v, 2); - - return !in_array($k, ['logintype', 'tx_oidc[code]']); - }); - $parts['query'] = implode('&', $queryParts); - $loginUrl = $parts['scheme'] . '://' . $parts['host']; - if (!empty($parts['port']) && !in_array((int)$parts['port'], [80, 443], true)) { - $loginUrl .= ':' . $parts['port']; - } - $loginUrl .= $parts['path']; - if (!empty($parts['query'])) { - $loginUrl .= '?' . $parts['query']; - } - - $_SESSION['oidc_state'] = $state; - $_SESSION['oidc_login_url'] = $loginUrl; - $_SESSION['oidc_authorization_url'] = $authorizationUrl; - } - - /** - * Returns a unique ID for the current processed request. - * - * This is supposed to be independent of the actual web server (Nginx or Apache) and - * the way PHP was built and unique enough for our use case, as opposed to using: - * - * - zend_thread_id() which requires PHP to be built with Zend Thread Safety - ZTS - support and debug mode - * - apache_getenv('UNIQUE_ID') which requires Apache as web server and mod_unique_id - * - * @return string - */ - protected static function getUniqueId() - { - $uniqueId = sprintf('%08x', abs(crc32($_SERVER['REMOTE_ADDR'] . $_SERVER['REQUEST_TIME'] . $_SERVER['REMOTE_PORT']))); - - return $uniqueId; - } - - /** - * Returns a logger. - * - * @return Logger - */ - protected static function getLogger() - { - /** @var Logger $logger */ - static $logger = null; - if ($logger === null) { - $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__); + $authService = GeneralUtility::makeInstance(OpenIdConnectService::class); + try { + $uri = $authService->generateOpenidConnectUri(); + } catch (InvalidArgumentException $e) { + $uri = '#InvalidOIDCConfiguration'; } - return $logger; + return $uri; } } diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index bed2f39..1f8c492 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -16,3 +16,6 @@ services: Causal\Oidc\Service\OAuthService: public: true + + Causal\Oidc\Service\OpenIdConnectService: + public: true From e7ff42adbde43e9b442e847cf8296e1b184a30c5 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Wed, 15 May 2024 15:09:50 +0200 Subject: [PATCH 109/142] [TASK] Cleanup LoginController Use functionality from OpenIdConnectService --- Classes/Controller/LoginController.php | 88 ++++++------------------ Classes/Service/OpenIdConnectService.php | 34 +++++++-- 2 files changed, 50 insertions(+), 72 deletions(-) diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php index 7ab5d23..bc91433 100644 --- a/Classes/Controller/LoginController.php +++ b/Classes/Controller/LoginController.php @@ -17,33 +17,22 @@ namespace Causal\Oidc\Controller; -use Causal\Oidc\Service\OAuthService; -use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; +use Causal\Oidc\Service\OpenIdConnectService; use TYPO3\CMS\Core\Http\PropagateResponseException; use TYPO3\CMS\Core\Http\RedirectResponse; use TYPO3\CMS\Core\Http\ServerRequest; -use TYPO3\CMS\Core\Http\ServerRequestFactory; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; class LoginController { - /** - * Global oidc settings - * - * @var array - */ - protected array $settings; - /** * TypoScript configuration of this plugin - * - * @var array */ protected array $pluginConfiguration = []; /** - * @var ContentObjectRenderer|null will automatically be injected, if this controller is called as a plugin + * will automatically be injected, if this controller is called as a plugin */ public ?ContentObjectRenderer $cObj = null; @@ -51,7 +40,7 @@ class LoginController public function __construct() { - $this->settings = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('oidc') ?? []; + $this->request = $GLOBALS['TYPO3_REQUEST']; } public function setContentObjectRenderer(ContentObjectRenderer $cObj) @@ -69,6 +58,7 @@ public function setContentObjectRenderer(ContentObjectRenderer $cObj) * * @param string $_ ignored * @param array|null $pluginConfiguration + * @throws PropagateResponseException */ public function login(string $_, ?array $pluginConfiguration) { @@ -76,45 +66,25 @@ public function login(string $_, ?array $pluginConfiguration) $this->pluginConfiguration = $pluginConfiguration; } - $this->request = ServerRequestFactory::fromGlobals(); $loginType = $this->request->getParsedBody()['logintype'] ?? $this->request->getQueryParams()['logintype'] ?? ''; if ($loginType === 'login') { - // performRedirectAfterLogin stops flow by emitting a redirect - $this->performRedirectAfterLogin(); + $redirectUrl = $this->determineRedirectUrl(); + $this->redirect($redirectUrl); } - $this->performRedirectToLogin($pluginConfiguration['authorizationUrlOptions.']); - } - - protected function performRedirectToLogin(array $authorizationUrlOptions = []) - { - /** @var OAuthService $service */ - $service = GeneralUtility::makeInstance(OAuthService::class); - $service->setSettings($this->settings); - - if (session_id() === '') { - session_start(); - } - if ($this->settings['enableCodeVerifier']) { - $codeVerifier = $this->generateCodeVerifier(); - $codeChallenge = $this->convertVerifierToChallenge($codeVerifier); - $authorizationUrlOptions = $this->addCodeChallengeToOptions($codeChallenge, $authorizationUrlOptions); - $_SESSION['oidc_code_verifier'] = $codeVerifier; - } - $authorizationUrl = $service->getAuthorizationUrl($authorizationUrlOptions); - - $state = $service->getState(); - $_SESSION['oidc_state'] = $state; - $_SESSION['oidc_login_url'] = GeneralUtility::getIndpEnv('REQUEST_URI'); - $_SESSION['oidc_authorization_url'] = $authorizationUrl; - unset($_SESSION['oidc_redirect_url']); // The redirect will be handled by this plugin + $authorizationUrl = $this->determineAuthorizationUrl($pluginConfiguration['authorizationUrlOptions.']); $this->redirect($authorizationUrl); } - protected function performRedirectAfterLogin() + protected function determineAuthorizationUrl(array $authorizationUrlOptions): string { - $redirectUrl = $this->determineRedirectUrl(); - $this->redirect($redirectUrl); + $oidcService = GeneralUtility::makeInstance(OpenIdConnectService::class); + $authorizationUrl = $oidcService->generateOpenidConnectUri($authorizationUrlOptions); + + // The redirect will be handled by this plugin + unset($_SESSION['oidc_redirect_url']); + + return $authorizationUrl; } protected function determineRedirectUrl() @@ -125,8 +95,8 @@ protected function determineRedirectUrl() } if (isset($this->pluginConfiguration['defaultRedirectPid'])) { - $defaultRedirectPid = $this->pluginConfiguration['defaultRedirectPid']; - if ((int)$defaultRedirectPid > 0) { + $defaultRedirectPid = (int)$this->pluginConfiguration['defaultRedirectPid']; + if ($defaultRedirectPid > 0) { return $this->cObj->typoLink_URL(['parameter' => $defaultRedirectPid]); } } @@ -134,29 +104,11 @@ protected function determineRedirectUrl() return '/'; } + /** + * @throws PropagateResponseException + */ protected function redirect(string $redirectUrl): void { throw new PropagateResponseException(new RedirectResponse($redirectUrl)); } - - protected function generateCodeVerifier(): string - { - return bin2hex(random_bytes(64)); - } - - protected function convertVerifierToChallenge($codeVerifier): string - { - return rtrim(strtr(base64_encode(hash('sha256', $codeVerifier, true)), '+/', '-_'), '='); - } - - protected function addCodeChallengeToOptions($codeChallenge, array $options): array - { - return array_merge( - $options, - [ - 'code_challenge' => $codeChallenge, - 'code_challenge_method' => 'S256', - ] - ); - } } diff --git a/Classes/Service/OpenIdConnectService.php b/Classes/Service/OpenIdConnectService.php index 60bc89a..f1f70bb 100644 --- a/Classes/Service/OpenIdConnectService.php +++ b/Classes/Service/OpenIdConnectService.php @@ -28,7 +28,7 @@ public function __construct(OAuthService $OAuthService) $this->config = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('oidc') ?? []; } - public function generateOpenidConnectUri(): string + public function generateOpenidConnectUri(array $authorizationUrlOptions = []): string { if (empty($this->config['oidcClientKey']) || empty($this->config['oidcClientSecret']) @@ -48,15 +48,23 @@ public function generateOpenidConnectUri(): string } if (empty($_SESSION['requestId']) || $_SESSION['requestId'] !== $requestId) { + $codeVerifier = null; + if ($this->config['enableCodeVerifier']) { + $codeVerifier = $this->generateCodeVerifier(); + $codeChallenge = $this->convertVerifierToChallenge($codeVerifier); + $authorizationUrlOptions = array_merge($authorizationUrlOptions, $this->getCodeChallengeOptions($codeChallenge)); + } + $request = $GLOBALS['TYPO3_REQUEST']; $redirectUrl = $request->getParsedBody()['redirect_url'] ?? $request->getQueryParams()['redirect_url'] ?? ''; + $data = $this->prepareAuthorizationUrl($authorizationUrlOptions); - $data = $this->prepareAuthorizationUrl(); $_SESSION['oidc_state'] = $data['state']; $_SESSION['oidc_login_url'] = $data['login_url']; $_SESSION['oidc_authorization_url'] = $data['authorization_url']; $_SESSION['requestId'] = $requestId; $_SESSION['oidc_redirect_url'] = $redirectUrl; + $_SESSION['oidc_code_verifier'] = $codeVerifier; $this->logger->debug('PHP session is available', [ 'id' => session_id(), @@ -72,11 +80,11 @@ public function generateOpenidConnectUri(): string * Prepares the authorization URL and corresponding expected state (to mitigate CSRF attack) * and stores information into the session. */ - protected function prepareAuthorizationUrl(): array + protected function prepareAuthorizationUrl(array $authorizationUrlOptions): array { $this->OAuthService->setSettings($this->config); - $authorizationUrl = $this->OAuthService->getAuthorizationUrl(); + $authorizationUrl = $this->OAuthService->getAuthorizationUrl($authorizationUrlOptions); $state = $this->OAuthService->getState(); $this->logger->debug('Generating authorization URL', [ @@ -114,4 +122,22 @@ protected function getUniqueId(): string { return sprintf('%08x', abs(crc32($_SERVER['REMOTE_ADDR'] . $_SERVER['REQUEST_TIME'] . $_SERVER['REMOTE_PORT']))); } + + protected function generateCodeVerifier(): string + { + return bin2hex(random_bytes(64)); + } + + protected function convertVerifierToChallenge($codeVerifier): string + { + return rtrim(strtr(base64_encode(hash('sha256', $codeVerifier, true)), '+/', '-_'), '='); + } + + protected function getCodeChallengeOptions($codeChallenge): array + { + return [ + 'code_challenge' => $codeChallenge, + 'code_challenge_method' => 'S256', + ]; + } } From 36fed91b7cba86d2cbc3843d7796767b154a496c Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Wed, 15 May 2024 13:57:49 +0200 Subject: [PATCH 110/142] [TASK] Move AuthenticationController logic to middleware The code in the AuthenticationController does not need anything from Extbase. In fact, it was only triggered by a custom PAGE TypoScript object, which itself was only triggered by the callback middleware. To simplify this code, the whole code is moved directly to the middleware, removing all the Extbase overhead. --- .../Controller/AuthenticationController.php | 95 ------------------- Classes/Middleware/OauthCallback.php | 65 +++++++++++-- Configuration/TypoScript/setup.typoscript | 18 ---- ext_localconf.php | 14 --- 4 files changed, 58 insertions(+), 134 deletions(-) delete mode 100644 Classes/Controller/AuthenticationController.php diff --git a/Classes/Controller/AuthenticationController.php b/Classes/Controller/AuthenticationController.php deleted file mode 100644 index b76b94d..0000000 --- a/Classes/Controller/AuthenticationController.php +++ /dev/null @@ -1,95 +0,0 @@ -globalSettings = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('oidc') ?? []; - } - - /** - * Initiates the silent authentication action. - */ - public function connectAction(): ResponseInterface - { - $this->logger->debug('Initiating the silent authentication'); - if ((empty($_GET['state']) || empty($_GET['code']))) { - $this->logger->error('No state or code detected', ['GET' => $_GET]); - throw new RuntimeException('No state or code detected', 1487001047); - } - - if (session_id() === '') { - $this->logger->debug('No PHP session found'); - session_start(); - } - $this->logger->debug('PHP session is available', [ - 'id' => session_id(), - 'data' => $_SESSION, - ]); - - if ($_GET['state'] !== ($_SESSION['oidc_state'] ?? null)) { - $this->logger->error('Invalid returning state detected', [ - 'expected' => $_SESSION['oidc_state'] ?? null, - 'actual' => $_GET['state'], - ]); - if (!$this->globalSettings['oidcDisableCSRFProtection']) { - throw new RuntimeException('Invalid state', 1489658206); - } - $this->logger->warning('Bypassing CSRF attack mitigation protection according to the extension configuration'); - } - - $loginUrl = $_SESSION['oidc_login_url']; - $loginUrl .= strpos($loginUrl, '?') !== false ? '&' : '?'; - $loginUrl .= 'logintype=login&tx_oidc[code]=' . $_GET['code']; - if (!empty($_SESSION['oidc_redirect_url']) && strpos($loginUrl, 'redirect_url=') === false) { - $loginUrl .= '&redirect_url=' . urlencode($_SESSION['oidc_redirect_url']); - } - - $this->logger->info('Redirecting to login URL', ['url' => $loginUrl]); - return new RedirectResponse($this->addBaseUriIfNecessary($loginUrl), 303); - } -} diff --git a/Classes/Middleware/OauthCallback.php b/Classes/Middleware/OauthCallback.php index 0935628..2d884cc 100644 --- a/Classes/Middleware/OauthCallback.php +++ b/Classes/Middleware/OauthCallback.php @@ -8,10 +8,17 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; +use TYPO3\CMS\Core\Http\RedirectResponse; use TYPO3\CMS\Core\Http\Response; +use TYPO3\CMS\Core\Utility\GeneralUtility; -class OauthCallback implements MiddlewareInterface +class OauthCallback implements MiddlewareInterface, LoggerAwareInterface { + use LoggerAwareTrait; + /** * @param ServerRequestInterface $request * @param RequestHandlerInterface $handler @@ -22,15 +29,59 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface { $queryParams = $request->getQueryParams(); $code = $queryParams['code'] ?? ''; - if ($code) { - $state = $queryParams['state'] ?? ''; - if (!$state) { + if (!$code) { + return $handler->handle($request); + } + + // A code was supplied, we start the OIDC handling + + $state = $queryParams['state'] ?? ''; + if (!$state) { + return (new Response())->withStatus(400, 'Invalid state'); + } + $this->logger->debug('Initiating the silent authentication'); + + if (session_id() === '') { + $this->logger->debug('No PHP session found'); + session_start(); + } + $this->logger->debug('PHP session is available', [ + 'id' => session_id(), + 'data' => $_SESSION, + ]); + + $stateFromSession = $_SESSION['oidc_state'] ?? null; + $loginUrl = $_SESSION['oidc_login_url'] ?? ''; + $redirectUrlFromSession = $_SESSION['oidc_redirect_url'] ?? ''; + + if ($state !== $stateFromSession) { + $globalSettings = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('oidc') ?? []; + if (!$globalSettings['oidcDisableCSRFProtection']) { + $this->logger->error('Invalid returning state detected', [ + 'expected' => $stateFromSession, + 'actual' => $state, + ]); return (new Response())->withStatus(400, 'Invalid state'); } + $this->logger->info('State mismatch. Bypassing CSRF attack mitigation protection according to the extension configuration', [ + 'expected' => $stateFromSession, + 'actual' => $state, + ]); + } - $queryParams['type'] = 1489657462; - $request = $request->withQueryParams($queryParams); + $loginUrlParams = [ + 'logintype' => 'login', + 'tx_oidc' => ['code' => $code], + ]; + if ($redirectUrlFromSession && strpos($loginUrl, 'redirect_url=') === false) { + $loginUrlParams['redirect_url'] = $redirectUrlFromSession; } - return $handler->handle($request); + + $loginUrl .= strpos($loginUrl, '?') !== false ? '&' : '?'; + $loginUrl .= ltrim(GeneralUtility::implodeArrayForUrl('', $loginUrlParams), '&'); + + $this->logger->info('Redirecting to login URL', ['url' => $loginUrl]); + + return new RedirectResponse(GeneralUtility::locationHeaderUrl($loginUrl), 303); } } diff --git a/Configuration/TypoScript/setup.typoscript b/Configuration/TypoScript/setup.typoscript index c2d6869..e4ba97c 100644 --- a/Configuration/TypoScript/setup.typoscript +++ b/Configuration/TypoScript/setup.typoscript @@ -1,21 +1,3 @@ -OidcAuth = PAGE -OidcAuth { - typeNum = 1489657462 - config { - xhtml_cleaning = 0 - disableAllHeaderCode = 1 - admPanel = 0 - } - headerData > - 10 = USER_INT - 10 { - userFunc = TYPO3\CMS\Extbase\Core\Bootstrap->run - vendorName = Causal - extensionName = Oidc - pluginName = Pi1 - controller = Authentication - } -} # format is "TYPO3 column" = plugin.tx_oidc.mapping { diff --git a/ext_localconf.php b/ext_localconf.php index 5fbac75..b447bd5 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -2,12 +2,10 @@ declare(strict_types=1); -use Causal\Oidc\Controller\AuthenticationController; use Causal\Oidc\Service\AuthenticationService; use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Extbase\Utility\ExtensionUtility; defined('TYPO3') or die(); @@ -42,18 +40,6 @@ ] ); -ExtensionUtility::configurePlugin( - 'oidc', - 'Pi1', - [ - AuthenticationController::class => 'connect', - ], - // non-cacheable actions - [ - AuthenticationController::class => 'connect' - ] -); - // Add typoscript for custom login plugin ExtensionManagementUtility::addPItoST43('oidc', null, '_login'); From b69b4695fbf0f7e24e45aaaed46331c141bddd5d Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Wed, 15 May 2024 15:13:50 +0200 Subject: [PATCH 111/142] [TASK] Rename DataHandler hook class to be unique Also move the hook registration into ext_localconf.php --- Classes/Hooks/{DataHandler.php => DataHandlerOidc.php} | 2 +- ext_localconf.php | 3 +++ ext_tables.php | 4 ---- 3 files changed, 4 insertions(+), 5 deletions(-) rename Classes/Hooks/{DataHandler.php => DataHandlerOidc.php} (99%) delete mode 100644 ext_tables.php diff --git a/Classes/Hooks/DataHandler.php b/Classes/Hooks/DataHandlerOidc.php similarity index 99% rename from Classes/Hooks/DataHandler.php rename to Classes/Hooks/DataHandlerOidc.php index 2b80f31..25d4298 100644 --- a/Classes/Hooks/DataHandler.php +++ b/Classes/Hooks/DataHandlerOidc.php @@ -23,7 +23,7 @@ /** * Hooks for \TYPO3\CMS\Core\DataHandling\DataHandler. */ -class DataHandler +class DataHandlerOidc { /** diff --git a/ext_localconf.php b/ext_localconf.php index b447bd5..cc4b826 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Causal\Oidc\Hooks\DataHandlerOidc; use Causal\Oidc\Service\AuthenticationService; use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; @@ -9,6 +10,8 @@ defined('TYPO3') or die(); +$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][] = DataHandlerOidc::class; + $settings = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('oidc') ?? []; // Service configuration diff --git a/ext_tables.php b/ext_tables.php deleted file mode 100644 index 5e11f75..0000000 --- a/ext_tables.php +++ /dev/null @@ -1,4 +0,0 @@ - Date: Wed, 15 May 2024 16:20:54 +0200 Subject: [PATCH 112/142] [TASK] Remove outdated composer.json "replace" info --- composer.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/composer.json b/composer.json index 9078eab..67c5d18 100644 --- a/composer.json +++ b/composer.json @@ -34,9 +34,6 @@ "Causal\\Oidc\\": "Classes/" } }, - "replace": { - "typo3-ter/oidc": "self.version" - }, "scripts": { "extension-create-libs": [ "mkdir -p Libraries/temp", From 33f4684d13e93fcf39db36ef9aaf227d24456aa8 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Wed, 15 May 2024 16:27:13 +0200 Subject: [PATCH 113/142] [BUGFIX] felogin usage docs expect empty uri on failure --- Classes/EventListener/FrontendLoginEventListener.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/EventListener/FrontendLoginEventListener.php b/Classes/EventListener/FrontendLoginEventListener.php index c19f752..91a54e9 100644 --- a/Classes/EventListener/FrontendLoginEventListener.php +++ b/Classes/EventListener/FrontendLoginEventListener.php @@ -33,7 +33,7 @@ public function modifyLoginFormView(ModifyLoginFormViewEvent $event): void try { $uri = $authService->generateOpenidConnectUri(); } catch (\InvalidArgumentException $e) { - $uri = '#InvalidOIDCConfiguration'; + $uri = ''; } $event->getView()->assign('openidConnectUri', $uri); } From 69bc9f5ae572154e4324d45a28029466edbadebc Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Wed, 15 May 2024 16:25:05 +0200 Subject: [PATCH 114/142] [FEATURE] Replace PHP session with JWT In order to support multi-head environments without sticky sessions, the data formerly stored in the PHP session is now stored inside a JWT. Resolves: #154 --- Classes/AuthenticationContext.php | 47 ++++++ Classes/Controller/LoginController.php | 6 +- .../FrontendLoginEventListener.php | 6 +- Classes/Middleware/OauthCallback.php | 135 ++++++++++++++---- Classes/Security/JwtTrait.php | 66 +++++++++ Classes/Service/AuthenticationService.php | 15 +- Classes/Service/OpenIdConnectService.php | 79 +++++----- Classes/ViewHelpers/OidcLinkViewHelper.php | 3 +- README.md | 15 -- .../Unit/Service/OpenIdConnectServiceTest.php | 50 +++++++ Tests/UnitTests-v10.xml | 40 ++++++ composer.json | 17 ++- 12 files changed, 385 insertions(+), 94 deletions(-) create mode 100644 Classes/AuthenticationContext.php create mode 100644 Classes/Security/JwtTrait.php create mode 100644 Tests/Unit/Service/OpenIdConnectServiceTest.php create mode 100644 Tests/UnitTests-v10.xml diff --git a/Classes/AuthenticationContext.php b/Classes/AuthenticationContext.php new file mode 100644 index 0000000..65b346b --- /dev/null +++ b/Classes/AuthenticationContext.php @@ -0,0 +1,47 @@ +state = $state; + $this->loginUrl = $loginUrl; + $this->authorizationUrl = $authorizationUrl; + $this->requestId = $requestId; + $this->redirectUrl = $redirectUrl; + $this->codeVerifier = $codeVerifier; + } + + public static function fromJwt(string $cookieValue): self + { + $payload = self::decodeJwt($cookieValue, self::createSigningKeyFromEncryptionKey(static::class), true); + return new self(...$payload); + } + + public function toHashSignedJwt(): string + { + $payload = get_object_vars($this); + return self::encodeHashSignedJwt($payload, self::createSigningKeyFromEncryptionKey(static::class)); + } +} diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php index bc91433..bad46a6 100644 --- a/Classes/Controller/LoginController.php +++ b/Classes/Controller/LoginController.php @@ -79,12 +79,12 @@ public function login(string $_, ?array $pluginConfiguration) protected function determineAuthorizationUrl(array $authorizationUrlOptions): string { $oidcService = GeneralUtility::makeInstance(OpenIdConnectService::class); - $authorizationUrl = $oidcService->generateOpenidConnectUri($authorizationUrlOptions); + $authContext = $oidcService->generateAuthenticationContext($this->request, $authorizationUrlOptions); // The redirect will be handled by this plugin - unset($_SESSION['oidc_redirect_url']); + $authContext->redirectUrl = ''; - return $authorizationUrl; + return $authContext->authorizationUrl; } protected function determineRedirectUrl() diff --git a/Classes/EventListener/FrontendLoginEventListener.php b/Classes/EventListener/FrontendLoginEventListener.php index c19f752..3f4dd78 100644 --- a/Classes/EventListener/FrontendLoginEventListener.php +++ b/Classes/EventListener/FrontendLoginEventListener.php @@ -18,6 +18,7 @@ namespace Causal\Oidc\EventListener; use Causal\Oidc\Service\OpenIdConnectService; +use InvalidArgumentException; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -31,8 +32,9 @@ public function modifyLoginFormView(ModifyLoginFormViewEvent $event): void { $authService = GeneralUtility::makeInstance(OpenIdConnectService::class); try { - $uri = $authService->generateOpenidConnectUri(); - } catch (\InvalidArgumentException $e) { + $authContext = $authService->generateAuthenticationContext($GLOBALS['TYPO3_REQUEST']); + $uri = $authContext->authorizationUrl; + } catch (InvalidArgumentException $e) { $uri = '#InvalidOIDCConfiguration'; } $event->getView()->assign('openidConnectUri', $uri); diff --git a/Classes/Middleware/OauthCallback.php b/Classes/Middleware/OauthCallback.php index 2d884cc..dcd3858 100644 --- a/Classes/Middleware/OauthCallback.php +++ b/Classes/Middleware/OauthCallback.php @@ -4,13 +4,19 @@ namespace Causal\Oidc\Middleware; +use Causal\Oidc\AuthenticationContext; +use Causal\Oidc\Service\OpenIdConnectService; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; +use Symfony\Component\HttpFoundation\Cookie; +use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationExtensionNotConfiguredException; +use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationPathDoesNotExistException; use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; +use TYPO3\CMS\Core\Http\NormalizedParams; use TYPO3\CMS\Core\Http\RedirectResponse; use TYPO3\CMS\Core\Http\Response; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -19,69 +25,140 @@ class OauthCallback implements MiddlewareInterface, LoggerAwareInterface { use LoggerAwareTrait; + protected const COOKIE_NAME = 'oidc_context'; + protected const COOKIE_PREFIX = ''; + protected const SECURE_PREFIX = '__Secure-'; + + protected OpenIdConnectService $openIdConnectService; + + public function __construct(OpenIdConnectService $openIdConnectService) + { + $this->openIdConnectService = $openIdConnectService; + } + /** * @param ServerRequestInterface $request * @param RequestHandlerInterface $handler * @return ResponseInterface * see https://github.com/thephpleague/oauth2-client + * @throws ExtensionConfigurationExtensionNotConfiguredException + * @throws ExtensionConfigurationPathDoesNotExistException */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { + $authContext = $this->resolveAuthenticationContext($request); + if ($authContext) { + $this->openIdConnectService->setAuthenticationContext($authContext); + $this->logger->debug('Authentication context is available', [ + 'data' => $authContext, + ]); + } + $queryParams = $request->getQueryParams(); $code = $queryParams['code'] ?? ''; if (!$code) { - return $handler->handle($request); + $response = $handler->handle($request); + return $this->enrichResponseWithCookie($request, $response); + } + if (!$authContext) { + return (new Response())->withStatus(400, 'Missing OIDC authentication context'); } // A code was supplied, we start the OIDC handling + $this->logger->debug('Initiating the silent authentication'); + $state = $queryParams['state'] ?? ''; if (!$state) { return (new Response())->withStatus(400, 'Invalid state'); } - $this->logger->debug('Initiating the silent authentication'); - - if (session_id() === '') { - $this->logger->debug('No PHP session found'); - session_start(); - } - $this->logger->debug('PHP session is available', [ - 'id' => session_id(), - 'data' => $_SESSION, - ]); - - $stateFromSession = $_SESSION['oidc_state'] ?? null; - $loginUrl = $_SESSION['oidc_login_url'] ?? ''; - $redirectUrlFromSession = $_SESSION['oidc_redirect_url'] ?? ''; - - if ($state !== $stateFromSession) { + if ($state !== $authContext->state) { $globalSettings = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('oidc') ?? []; if (!$globalSettings['oidcDisableCSRFProtection']) { $this->logger->error('Invalid returning state detected', [ - 'expected' => $stateFromSession, + 'expected' => $authContext->state, 'actual' => $state, ]); return (new Response())->withStatus(400, 'Invalid state'); } $this->logger->info('State mismatch. Bypassing CSRF attack mitigation protection according to the extension configuration', [ - 'expected' => $stateFromSession, + 'expected' => $authContext->state, 'actual' => $state, ]); } - $loginUrlParams = [ - 'logintype' => 'login', - 'tx_oidc' => ['code' => $code], - ]; - if ($redirectUrlFromSession && strpos($loginUrl, 'redirect_url=') === false) { - $loginUrlParams['redirect_url'] = $redirectUrlFromSession; + $loginUrl = $this->openIdConnectService->getFinalLoginUrl($code); + + $this->logger->info('Redirecting to login URL', ['url' => (string)$loginUrl]); + + return new RedirectResponse(GeneralUtility::locationHeaderUrl((string)$loginUrl), 303); + } + + /** + * @see \TYPO3\CMS\Core\Middleware\RequestTokenMiddleware::resolveNoncePool (v12+) + */ + protected function resolveAuthenticationContext(ServerRequestInterface $request): ?AuthenticationContext + { + $secure = $this->isHttps($request); + // resolves cookie name dependent on whether TLS is used in request and uses `__Secure-` prefix, + // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#cookie_prefixes + $securePrefix = $secure ? self::SECURE_PREFIX : ''; + $cookiePrefix = $securePrefix . self::COOKIE_PREFIX; + $cookiePrefixLength = strlen($cookiePrefix); + $cookies = array_filter( + $request->getCookieParams(), + static fn($name) => is_string($name) && str_starts_with($name, $cookiePrefix), + ARRAY_FILTER_USE_KEY + ); + foreach ($cookies as $name => $value) { + $name = substr($name, $cookiePrefixLength); + if ($name === self::COOKIE_NAME) { + return AuthenticationContext::fromJwt($value); + } } + return null; + } - $loginUrl .= strpos($loginUrl, '?') !== false ? '&' : '?'; - $loginUrl .= ltrim(GeneralUtility::implodeArrayForUrl('', $loginUrlParams), '&'); + /** + * @see \TYPO3\CMS\Core\Middleware\RequestTokenMiddleware::enrichResponseWithCookie (v12+) + */ + protected function enrichResponseWithCookie(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $authContext = $this->openIdConnectService->getAuthenticationContext(); + if (!$authContext) { + return $response; + } + + $secure = $this->isHttps($request); + $normalizedParams = $request->getAttribute('normalizedParams'); + $path = $normalizedParams->getSitePath(); + $securePrefix = $secure ? self::SECURE_PREFIX : ''; + $cookiePrefix = $securePrefix . self::COOKIE_PREFIX; + + $createCookie = static fn(string $name, string $value, int $expire): Cookie => new Cookie( + $name, + $value, + $expire, + $path, + null, + $secure, + true, + false, + Cookie::SAMESITE_LAX + ); - $this->logger->info('Redirecting to login URL', ['url' => $loginUrl]); + $cookies = []; + $cookies[] = $createCookie($cookiePrefix . self::COOKIE_NAME, $authContext->toHashSignedJwt(), 0); - return new RedirectResponse(GeneralUtility::locationHeaderUrl($loginUrl), 303); + foreach ($cookies as $cookie) { + $response = $response->withAddedHeader('Set-Cookie', (string)$cookie); + } + return $response; + } + + protected function isHttps(ServerRequestInterface $request): bool + { + $normalizedParams = $request->getAttribute('normalizedParams'); + return $normalizedParams instanceof NormalizedParams && $normalizedParams->isHttps(); } } diff --git a/Classes/Security/JwtTrait.php b/Classes/Security/JwtTrait.php new file mode 100644 index 0000000..bdbe663 --- /dev/null +++ b/Classes/Security/JwtTrait.php @@ -0,0 +1,66 @@ +getKeyMaterial(), self::getDefaultSigningAlgorithm()); + } + + /** + * @param string $jwt + * @param Key $key + * @param bool $associative + * @return \stdClass|array + */ + private static function decodeJwt(string $jwt, Key $key, bool $associative = false) + { + $payload = JWT::decode($jwt, $key); + return $associative ? json_decode(json_encode($payload), true) : $payload; + } +} diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 71ff37e..672866d 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -17,10 +17,12 @@ namespace Causal\Oidc\Service; +use Causal\Oidc\AuthenticationContext; use Causal\Oidc\Event\AuthenticationGetUserEvent; use Causal\Oidc\Event\AuthenticationPreUserEvent; use Causal\Oidc\Event\ModifyResourceOwnerEvent; use Causal\Oidc\Event\ModifyUserEvent; +use Causal\Oidc\Middleware\OauthCallback; use InvalidArgumentException; use League\OAuth2\Client\Provider\Exception\IdentityProviderException; use League\OAuth2\Client\Token\AccessToken; @@ -82,14 +84,6 @@ public function __construct() $this->config = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('oidc') ?? []; } - protected function getCodeVerifierFromSession(): ?string - { - if (session_id() === '') { - session_start(); - } - return $_SESSION['oidc_code_verifier'] ?? null; - } - /** * Finds a user. * @@ -107,7 +101,10 @@ public function getUser() if ($code !== null) { $codeVerifier = null; if ($this->config['enableCodeVerifier']) { - $codeVerifier = $this->getCodeVerifierFromSession(); + $authContext = GeneralUtility::makeInstance(OpenIdConnectService::class)->getAuthenticationContext(); + if ($authContext) { + $codeVerifier = $authContext->codeVerifier; + } } $user = $this->authenticateWithAuthorizationCode($code, $codeVerifier); } else { diff --git a/Classes/Service/OpenIdConnectService.php b/Classes/Service/OpenIdConnectService.php index f1f70bb..ec927fb 100644 --- a/Classes/Service/OpenIdConnectService.php +++ b/Classes/Service/OpenIdConnectService.php @@ -4,10 +4,12 @@ namespace Causal\Oidc\Service; +use Causal\Oidc\AuthenticationContext; use InvalidArgumentException; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; +use TYPO3\CMS\Core\Http\ServerRequest; use TYPO3\CMS\Core\Http\Uri; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -17,18 +19,20 @@ class OpenIdConnectService implements LoggerAwareInterface protected OAuthService $OAuthService; + protected ?AuthenticationContext $authContext = null; + /** * Global extension configuration */ protected array $config; - public function __construct(OAuthService $OAuthService) + public function __construct(OAuthService $OAuthService, array $config = []) { $this->OAuthService = $OAuthService; - $this->config = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('oidc') ?? []; + $this->config = $config ?: GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('oidc') ?? []; } - public function generateOpenidConnectUri(array $authorizationUrlOptions = []): string + public function generateAuthenticationContext(ServerRequest $request, array $authorizationUrlOptions = []): AuthenticationContext { if (empty($this->config['oidcClientKey']) || empty($this->config['oidcClientSecret']) @@ -42,12 +46,7 @@ public function generateOpenidConnectUri(array $authorizationUrlOptions = []): s $this->logger->debug('Generating OpenID Connect URI', ['request' => $requestId]); - if (session_id() === '') { // If no session exists, start a new one - $this->logger->debug('No PHP session found'); - session_start(); - } - - if (empty($_SESSION['requestId']) || $_SESSION['requestId'] !== $requestId) { + if (!$this->authContext || $this->authContext->requestId !== $requestId) { $codeVerifier = null; if ($this->config['enableCodeVerifier']) { $codeVerifier = $this->generateCodeVerifier(); @@ -55,32 +54,48 @@ public function generateOpenidConnectUri(array $authorizationUrlOptions = []): s $authorizationUrlOptions = array_merge($authorizationUrlOptions, $this->getCodeChallengeOptions($codeChallenge)); } - $request = $GLOBALS['TYPO3_REQUEST']; $redirectUrl = $request->getParsedBody()['redirect_url'] ?? $request->getQueryParams()['redirect_url'] ?? ''; - $data = $this->prepareAuthorizationUrl($authorizationUrlOptions); - - $_SESSION['oidc_state'] = $data['state']; - $_SESSION['oidc_login_url'] = $data['login_url']; - $_SESSION['oidc_authorization_url'] = $data['authorization_url']; - $_SESSION['requestId'] = $requestId; - $_SESSION['oidc_redirect_url'] = $redirectUrl; - $_SESSION['oidc_code_verifier'] = $codeVerifier; - - $this->logger->debug('PHP session is available', [ - 'id' => session_id(), - 'data' => $_SESSION, - ]); + + $this->authContext = $this->prepareAuthorizationContext($authorizationUrlOptions); + $this->authContext->requestId = $requestId; + $this->authContext->redirectUrl = $redirectUrl; + $this->authContext->codeVerifier = $codeVerifier; } else { $this->logger->debug('Reusing same authorization URL and state'); } - return $_SESSION['oidc_authorization_url'] ?? ''; + return $this->authContext; + } + + public function setAuthenticationContext(AuthenticationContext $authContext) + { + $this->authContext = $authContext; + } + + public function getAuthenticationContext(): ?AuthenticationContext + { + return $this->authContext; + } + + public function getFinalLoginUrl(string $code): Uri + { + $loginUrlParams = [ + 'logintype' => 'login', + 'tx_oidc' => ['code' => $code], + ]; + if ($this->authContext->redirectUrl && strpos($this->authContext->loginUrl, 'redirect_url=') === false) { + $loginUrlParams['redirect_url'] = $this->authContext->redirectUrl; + } + $loginUrl = new Uri($this->authContext->loginUrl); + + $query = $loginUrl->getQuery() . GeneralUtility::implodeArrayForUrl('', $loginUrlParams); + + return $loginUrl->withQuery(ltrim($query, '&')); } /** * Prepares the authorization URL and corresponding expected state (to mitigate CSRF attack) - * and stores information into the session. */ - protected function prepareAuthorizationUrl(array $authorizationUrlOptions): array + protected function prepareAuthorizationContext(array $authorizationUrlOptions): AuthenticationContext { $this->OAuthService->setSettings($this->config); @@ -92,19 +107,19 @@ protected function prepareAuthorizationUrl(array $authorizationUrlOptions): arra 'state' => $state, ]); + return new AuthenticationContext($state, (string)$this->getLoginUrlForContext(), $authorizationUrl); + } + + protected function getLoginUrlForContext(): Uri + { $loginUrl = new Uri(GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL')); // filter query string $queryParts = array_filter(explode('&', $loginUrl->getQuery()), function ($k) { return $k !== 'logintype' && $k !== 'tx_oidc[code]'; }, ARRAY_FILTER_USE_KEY); - $loginUrl = $loginUrl->withQuery(implode('&', $queryParts)); - return [ - 'state' => $state, - 'login_url' => (string)$loginUrl, - 'authorization_url' => $authorizationUrl - ]; + return $loginUrl->withQuery(implode('&', $queryParts)); } /** diff --git a/Classes/ViewHelpers/OidcLinkViewHelper.php b/Classes/ViewHelpers/OidcLinkViewHelper.php index 80ace13..88a704e 100644 --- a/Classes/ViewHelpers/OidcLinkViewHelper.php +++ b/Classes/ViewHelpers/OidcLinkViewHelper.php @@ -39,7 +39,8 @@ public static function renderStatic(array $arguments, \Closure $renderChildrenCl { $authService = GeneralUtility::makeInstance(OpenIdConnectService::class); try { - $uri = $authService->generateOpenidConnectUri(); + $authContext = $authService->generateAuthenticationContext($GLOBALS['TYPO3_REQUEST']); + $uri = $authContext->authorizationUrl; } catch (InvalidArgumentException $e) { $uri = '#InvalidOIDCConfiguration'; } diff --git a/README.md b/README.md index bb5d706..1ff843c 100644 --- a/README.md +++ b/README.md @@ -135,18 +135,3 @@ $GLOBALS['TYPO3_CONF_VARS']['LOG']['Causal']['Oidc']['writerConfiguration'] = [ **Hint:** Be sure to read [Configuration of the Logging system](https://docs.typo3.org/m/typo3/reference-coreapi/master/en-us/ApiOverview/Logging/Configuration/Index.html#logging-configuration) to fine-tune your configuration on any production website. - -## Session handling - -This extension is using PHP's native sessions (`$_SESSION`) to store state about -ongoing authentication attempts. - -The following data is stored: - -* `oidc_state` -* `oidc_login_url` -* `oidc_authorization_url` -* `oidc_redirect_url` -* `oidc_code_verifier` -* `requestId` - diff --git a/Tests/Unit/Service/OpenIdConnectServiceTest.php b/Tests/Unit/Service/OpenIdConnectServiceTest.php new file mode 100644 index 0000000..ae2ae9d --- /dev/null +++ b/Tests/Unit/Service/OpenIdConnectServiceTest.php @@ -0,0 +1,50 @@ +service = new OpenIdConnectService(new OAuthService(), ['dummy']); + $this->service->setAuthenticationContext(new AuthenticationContext('', '', '', '', 'https://example.com/redirect')); + } + + /** + * @test + * @dataProvider getFinalLoginUrlReturnsExpectedUrlDataProvider + */ + public function getFinalLoginUrlReturnsExpectedUrl(string $loginUrl, string $expected): void + { + $this->service->getAuthenticationContext()->loginUrl = $loginUrl; + self::assertSame($expected, (string)$this->service->getFinalLoginUrl('somecode')); + } + + public static function getFinalLoginUrlReturnsExpectedUrlDataProvider(): array + { + return [ + 'default' => [ + 'loginUrl' => 'https://example.com/login', + 'expected' => 'https://example.com/login?logintype=login&tx_oidc%5Bcode%5D=somecode&redirect_url=https%3A%2F%2Fexample.com%2Fredirect', + ], + 'preserves params' => [ + 'loginUrl' => 'https://example.com/login?otherparam=foo', + 'expected' => 'https://example.com/login?otherparam=foo&logintype=login&tx_oidc%5Bcode%5D=somecode&redirect_url=https%3A%2F%2Fexample.com%2Fredirect', + ], + 'preserves redirect_url' => [ + 'loginUrl' => 'https://example.com/login?redirect_url=http%3A%2F%2Fexample.com%2Fother', + 'expected' => 'https://example.com/login?redirect_url=http%3A%2F%2Fexample.com%2Fother&logintype=login&tx_oidc%5Bcode%5D=somecode', + ], + ]; + } +} diff --git a/Tests/UnitTests-v10.xml b/Tests/UnitTests-v10.xml new file mode 100644 index 0000000..ca35b7d --- /dev/null +++ b/Tests/UnitTests-v10.xml @@ -0,0 +1,40 @@ + + + + + + Unit/ + + + + + + + diff --git a/composer.json b/composer.json index 67c5d18..e75abbe 100644 --- a/composer.json +++ b/composer.json @@ -27,18 +27,29 @@ "typo3/cms-core": "^11.5", "typo3/cms-frontend": "^11.5", "typo3/cms-felogin": "^11.5", - "league/oauth2-client": "^2.7" + "league/oauth2-client": "^2.7", + "firebase/php-jwt": "^6.10" + }, + "require-dev": { + "phpunit/phpunit": "^10", + "typo3/testing-framework": "7.x-dev", + "typo3/minimal": "^11" }, "autoload": { "psr-4": { "Causal\\Oidc\\": "Classes/" } }, + "autoload-dev": { + "psr-4": { + "Causal\\Oidc\\Tests\\": "Tests/" + } + }, "scripts": { "extension-create-libs": [ "mkdir -p Libraries/temp", "[ -f $HOME/.composer/vendor/bin/phar-composer ] || composer global require clue/phar-composer", - "if [ ! -f Libraries/league-oauth2-client.phar ]; then cd Libraries/temp && composer require league/oauth2-client=^2.7 && composer config classmap-authoritative true && composer config prepend-autoloader false && composer dump-autoload; fi", + "if [ ! -f Libraries/league-oauth2-client.phar ]; then cd Libraries/temp && composer require league/oauth2-client=^2.7 && composer require firebase/php-jwt=^6.10 composer config classmap-authoritative true && composer config prepend-autoloader false && composer dump-autoload; fi", "[ -f Libraries/league-oauth2-client.phar ] || $HOME/.composer/vendor/bin/phar-composer build Libraries/temp/ Libraries/league-oauth2-client.phar", "chmod -x Libraries/*.phar", "rm -rf Libraries/temp" @@ -58,7 +69,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.1.x-dev" + "dev-master": "2.2.x-dev" }, "typo3/cms": { "app-dir": ".Build", From deaf950a8171adea7268b77cf619c2a3cee9d654 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Thu, 16 May 2024 15:56:17 +0200 Subject: [PATCH 115/142] [TASK] Try several ways to retrieve the request in AuthenticationService Starting with TYPO3 v12 the request is available in the `authInfo` array. If this is not the case (e.g. in v11), try the global `TYPO3_REQUEST`, otherwise construct a new one by the `ServerRequestFactory` --- Classes/Service/AuthenticationService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 71ff37e..6a19326 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -101,7 +101,7 @@ public function getUser() $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); $user = false; - $request = ServerRequestFactory::fromGlobals(); + $request = $this->authInfo['request'] ?? $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals(); $params = $request->getQueryParams()['tx_oidc'] ?? []; $code = $params['code'] ?? null; if ($code !== null) { @@ -734,7 +734,7 @@ protected function generatePassword(): string protected function getLocalTSFE(): TypoScriptFrontendController { - $request = ServerRequestFactory::fromGlobals(); + $request = $this->authInfo['request'] ?? $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals(); $siteMatcher = GeneralUtility::makeInstance(SiteMatcher::class); $routeResult = $siteMatcher->matchRequest($request); if ($routeResult instanceof SiteRouteResult) { From 60a1e225605f55197dfcfdc7740074dbda5c2894 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Mon, 27 May 2024 14:19:49 +0200 Subject: [PATCH 116/142] [TASK] Limit AuthenticationContext property access Also remove useless condition and reduce debug logging for new authentication urls. --- Classes/AuthenticationContext.php | 34 ++++++++++++---- Classes/Controller/LoginController.php | 2 +- .../FrontendLoginEventListener.php | 2 +- Classes/Middleware/OauthCallback.php | 10 ++--- Classes/Service/AuthenticationService.php | 2 - Classes/Service/OpenIdConnectService.php | 40 +++++++------------ Classes/ViewHelpers/OidcLinkViewHelper.php | 2 +- .../Unit/Service/OpenIdConnectServiceTest.php | 3 +- 8 files changed, 50 insertions(+), 45 deletions(-) diff --git a/Classes/AuthenticationContext.php b/Classes/AuthenticationContext.php index 65b346b..3a70532 100644 --- a/Classes/AuthenticationContext.php +++ b/Classes/AuthenticationContext.php @@ -10,18 +10,18 @@ class AuthenticationContext { use JwtTrait; - public string $requestId = ''; - public string $state = ''; - public string $loginUrl = ''; - public string $authorizationUrl = ''; - public string $redirectUrl = ''; - public ?string $codeVerifier = null; + protected string $requestId; + protected string $state; + protected string $loginUrl; + protected string $authorizationUrl; + public string $redirectUrl; + public ?string $codeVerifier; public function __construct( string $state, string $loginUrl, string $authorizationUrl, - string $requestId = '', + string $requestId, string $redirectUrl = '', ?string $codeVerifier = null ) { @@ -33,6 +33,26 @@ public function __construct( $this->codeVerifier = $codeVerifier; } + public function getAuthorizationUrl(): string + { + return $this->authorizationUrl; + } + + public function getLoginUrl(): string + { + return $this->loginUrl; + } + + public function getState(): string + { + return $this->state; + } + + public function getRequestId(): string + { + return $this->requestId; + } + public static function fromJwt(string $cookieValue): self { $payload = self::decodeJwt($cookieValue, self::createSigningKeyFromEncryptionKey(static::class), true); diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php index bad46a6..9b00a75 100644 --- a/Classes/Controller/LoginController.php +++ b/Classes/Controller/LoginController.php @@ -84,7 +84,7 @@ protected function determineAuthorizationUrl(array $authorizationUrlOptions): st // The redirect will be handled by this plugin $authContext->redirectUrl = ''; - return $authContext->authorizationUrl; + return $authContext->getAuthorizationUrl(); } protected function determineRedirectUrl() diff --git a/Classes/EventListener/FrontendLoginEventListener.php b/Classes/EventListener/FrontendLoginEventListener.php index 3f4dd78..cd3bfa4 100644 --- a/Classes/EventListener/FrontendLoginEventListener.php +++ b/Classes/EventListener/FrontendLoginEventListener.php @@ -33,7 +33,7 @@ public function modifyLoginFormView(ModifyLoginFormViewEvent $event): void $authService = GeneralUtility::makeInstance(OpenIdConnectService::class); try { $authContext = $authService->generateAuthenticationContext($GLOBALS['TYPO3_REQUEST']); - $uri = $authContext->authorizationUrl; + $uri = $authContext->getAuthorizationUrl(); } catch (InvalidArgumentException $e) { $uri = '#InvalidOIDCConfiguration'; } diff --git a/Classes/Middleware/OauthCallback.php b/Classes/Middleware/OauthCallback.php index dcd3858..be04bb2 100644 --- a/Classes/Middleware/OauthCallback.php +++ b/Classes/Middleware/OauthCallback.php @@ -49,9 +49,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $authContext = $this->resolveAuthenticationContext($request); if ($authContext) { $this->openIdConnectService->setAuthenticationContext($authContext); - $this->logger->debug('Authentication context is available', [ - 'data' => $authContext, - ]); + $this->logger->debug('Authentication context is available', ['data' => $authContext]); } $queryParams = $request->getQueryParams(); @@ -72,17 +70,17 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface if (!$state) { return (new Response())->withStatus(400, 'Invalid state'); } - if ($state !== $authContext->state) { + if ($state !== $authContext->getState()) { $globalSettings = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('oidc') ?? []; if (!$globalSettings['oidcDisableCSRFProtection']) { $this->logger->error('Invalid returning state detected', [ - 'expected' => $authContext->state, + 'expected' => $authContext->getState(), 'actual' => $state, ]); return (new Response())->withStatus(400, 'Invalid state'); } $this->logger->info('State mismatch. Bypassing CSRF attack mitigation protection according to the extension configuration', [ - 'expected' => $authContext->state, + 'expected' => $authContext->getState(), 'actual' => $state, ]); } diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 672866d..5f084e2 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -17,12 +17,10 @@ namespace Causal\Oidc\Service; -use Causal\Oidc\AuthenticationContext; use Causal\Oidc\Event\AuthenticationGetUserEvent; use Causal\Oidc\Event\AuthenticationPreUserEvent; use Causal\Oidc\Event\ModifyResourceOwnerEvent; use Causal\Oidc\Event\ModifyUserEvent; -use Causal\Oidc\Middleware\OauthCallback; use InvalidArgumentException; use League\OAuth2\Client\Provider\Exception\IdentityProviderException; use League\OAuth2\Client\Token\AccessToken; diff --git a/Classes/Service/OpenIdConnectService.php b/Classes/Service/OpenIdConnectService.php index ec927fb..66e1539 100644 --- a/Classes/Service/OpenIdConnectService.php +++ b/Classes/Service/OpenIdConnectService.php @@ -43,26 +43,21 @@ public function generateAuthenticationContext(ServerRequest $request, array $aut } $requestId = $this->getUniqueId(); + $codeVerifier = null; + if ($this->config['enableCodeVerifier']) { + $codeVerifier = $this->generateCodeVerifier(); + $codeChallenge = $this->convertVerifierToChallenge($codeVerifier); + $authorizationUrlOptions = array_merge($authorizationUrlOptions, $this->getCodeChallengeOptions($codeChallenge)); + } - $this->logger->debug('Generating OpenID Connect URI', ['request' => $requestId]); + $redirectUrl = $request->getParsedBody()['redirect_url'] ?? $request->getQueryParams()['redirect_url'] ?? ''; - if (!$this->authContext || $this->authContext->requestId !== $requestId) { - $codeVerifier = null; - if ($this->config['enableCodeVerifier']) { - $codeVerifier = $this->generateCodeVerifier(); - $codeChallenge = $this->convertVerifierToChallenge($codeVerifier); - $authorizationUrlOptions = array_merge($authorizationUrlOptions, $this->getCodeChallengeOptions($codeChallenge)); - } + $this->authContext = $this->prepareAuthorizationContext($authorizationUrlOptions, $requestId); + $this->authContext->redirectUrl = $redirectUrl; + $this->authContext->codeVerifier = $codeVerifier; - $redirectUrl = $request->getParsedBody()['redirect_url'] ?? $request->getQueryParams()['redirect_url'] ?? ''; + $this->logger->debug('Generated new Authentication Context', ['authContext' => $this->authContext]); - $this->authContext = $this->prepareAuthorizationContext($authorizationUrlOptions); - $this->authContext->requestId = $requestId; - $this->authContext->redirectUrl = $redirectUrl; - $this->authContext->codeVerifier = $codeVerifier; - } else { - $this->logger->debug('Reusing same authorization URL and state'); - } return $this->authContext; } @@ -82,10 +77,10 @@ public function getFinalLoginUrl(string $code): Uri 'logintype' => 'login', 'tx_oidc' => ['code' => $code], ]; - if ($this->authContext->redirectUrl && strpos($this->authContext->loginUrl, 'redirect_url=') === false) { + if ($this->authContext->redirectUrl && strpos($this->authContext->getLoginUrl(), 'redirect_url=') === false) { $loginUrlParams['redirect_url'] = $this->authContext->redirectUrl; } - $loginUrl = new Uri($this->authContext->loginUrl); + $loginUrl = new Uri($this->authContext->getLoginUrl()); $query = $loginUrl->getQuery() . GeneralUtility::implodeArrayForUrl('', $loginUrlParams); @@ -95,19 +90,14 @@ public function getFinalLoginUrl(string $code): Uri /** * Prepares the authorization URL and corresponding expected state (to mitigate CSRF attack) */ - protected function prepareAuthorizationContext(array $authorizationUrlOptions): AuthenticationContext + protected function prepareAuthorizationContext(array $authorizationUrlOptions, string $requestId): AuthenticationContext { $this->OAuthService->setSettings($this->config); $authorizationUrl = $this->OAuthService->getAuthorizationUrl($authorizationUrlOptions); $state = $this->OAuthService->getState(); - $this->logger->debug('Generating authorization URL', [ - 'url' => $authorizationUrl, - 'state' => $state, - ]); - - return new AuthenticationContext($state, (string)$this->getLoginUrlForContext(), $authorizationUrl); + return new AuthenticationContext($state, (string)$this->getLoginUrlForContext(), $authorizationUrl, $requestId); } protected function getLoginUrlForContext(): Uri diff --git a/Classes/ViewHelpers/OidcLinkViewHelper.php b/Classes/ViewHelpers/OidcLinkViewHelper.php index 88a704e..92885e2 100644 --- a/Classes/ViewHelpers/OidcLinkViewHelper.php +++ b/Classes/ViewHelpers/OidcLinkViewHelper.php @@ -40,7 +40,7 @@ public static function renderStatic(array $arguments, \Closure $renderChildrenCl $authService = GeneralUtility::makeInstance(OpenIdConnectService::class); try { $authContext = $authService->generateAuthenticationContext($GLOBALS['TYPO3_REQUEST']); - $uri = $authContext->authorizationUrl; + $uri = $authContext->getAuthorizationUrl(); } catch (InvalidArgumentException $e) { $uri = '#InvalidOIDCConfiguration'; } diff --git a/Tests/Unit/Service/OpenIdConnectServiceTest.php b/Tests/Unit/Service/OpenIdConnectServiceTest.php index ae2ae9d..876602d 100644 --- a/Tests/Unit/Service/OpenIdConnectServiceTest.php +++ b/Tests/Unit/Service/OpenIdConnectServiceTest.php @@ -17,7 +17,6 @@ protected function setUp(): void { parent::setUp(); $this->service = new OpenIdConnectService(new OAuthService(), ['dummy']); - $this->service->setAuthenticationContext(new AuthenticationContext('', '', '', '', 'https://example.com/redirect')); } /** @@ -26,7 +25,7 @@ protected function setUp(): void */ public function getFinalLoginUrlReturnsExpectedUrl(string $loginUrl, string $expected): void { - $this->service->getAuthenticationContext()->loginUrl = $loginUrl; + $this->service->setAuthenticationContext(new AuthenticationContext('', $loginUrl, '', '', 'https://example.com/redirect')); self::assertSame($expected, (string)$this->service->getFinalLoginUrl('somecode')); } From eedb66c091b42d729da4671fc4302987528226c0 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Mon, 27 May 2024 14:42:11 +0200 Subject: [PATCH 117/142] [BUGFIX] Only handle authentication stuff for GET requests --- Classes/Middleware/OauthCallback.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Classes/Middleware/OauthCallback.php b/Classes/Middleware/OauthCallback.php index be04bb2..cacb7d8 100644 --- a/Classes/Middleware/OauthCallback.php +++ b/Classes/Middleware/OauthCallback.php @@ -46,6 +46,10 @@ public function __construct(OpenIdConnectService $openIdConnectService) */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { + if ($request->getMethod() !== 'GET') { + return $handler->handle($request); + } + $authContext = $this->resolveAuthenticationContext($request); if ($authContext) { $this->openIdConnectService->setAuthenticationContext($authContext); From b8e8b1543dc3c5251ef9261dff5388512c5cd888 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Thu, 6 Jun 2024 15:01:38 +0200 Subject: [PATCH 118/142] [TASK] Catch any error that may occur during auth url generation The underlying provider may throw arbitrary errors. Make sure those do not propagate to Frontend. --- Classes/EventListener/FrontendLoginEventListener.php | 4 ++++ Classes/ViewHelpers/OidcLinkViewHelper.php | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Classes/EventListener/FrontendLoginEventListener.php b/Classes/EventListener/FrontendLoginEventListener.php index cd3bfa4..f791fbf 100644 --- a/Classes/EventListener/FrontendLoginEventListener.php +++ b/Classes/EventListener/FrontendLoginEventListener.php @@ -21,6 +21,7 @@ use InvalidArgumentException; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; +use Throwable; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\FrontendLogin\Event\ModifyLoginFormViewEvent; @@ -36,6 +37,9 @@ public function modifyLoginFormView(ModifyLoginFormViewEvent $event): void $uri = $authContext->getAuthorizationUrl(); } catch (InvalidArgumentException $e) { $uri = '#InvalidOIDCConfiguration'; + } catch (Throwable $e) { + // whatever the provider did wrong (can be connection errors) + $uri = '#oidcError'; } $event->getView()->assign('openidConnectUri', $uri); } diff --git a/Classes/ViewHelpers/OidcLinkViewHelper.php b/Classes/ViewHelpers/OidcLinkViewHelper.php index 92885e2..cc68f30 100644 --- a/Classes/ViewHelpers/OidcLinkViewHelper.php +++ b/Classes/ViewHelpers/OidcLinkViewHelper.php @@ -19,6 +19,7 @@ use Causal\Oidc\Service\OpenIdConnectService; use InvalidArgumentException; +use Throwable; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper; @@ -43,8 +44,10 @@ public static function renderStatic(array $arguments, \Closure $renderChildrenCl $uri = $authContext->getAuthorizationUrl(); } catch (InvalidArgumentException $e) { $uri = '#InvalidOIDCConfiguration'; + } catch (Throwable $e) { + // whatever the provider did wrong (can be connection errors) + $uri = '#oidcError'; } - return $uri; } } From 3bbc30dae3e535f21570f609a0e42b6fc431d85e Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Fri, 21 Jun 2024 11:50:11 +0200 Subject: [PATCH 119/142] [TASK] Use dedicated route for authorization url Instead of always generating an authentication url for every rendering of the oidc button, create a dedicated route, which will create the authentication URL on demand and redirect the user there. Related #158 --- .../FrontendLoginEventListener.php | 15 +--- .../Middleware/AuthenticationUrlRequest.php | 51 +++++++++++++ Classes/Service/OpenIdConnectService.php | 72 +++++++++++++------ Classes/ViewHelpers/OidcLinkViewHelper.php | 15 +--- Configuration/RequestMiddlewares.php | 13 +++- README.md | 2 +- .../Private/Language/de.locallang_db.xlf | 8 ++- Resources/Private/Language/locallang_db.xlf | 7 +- Resources/Private/Templates/Login/Login.html | 9 +-- ext_conf_template.txt | 3 + 10 files changed, 134 insertions(+), 61 deletions(-) create mode 100644 Classes/Middleware/AuthenticationUrlRequest.php diff --git a/Classes/EventListener/FrontendLoginEventListener.php b/Classes/EventListener/FrontendLoginEventListener.php index f791fbf..a454c98 100644 --- a/Classes/EventListener/FrontendLoginEventListener.php +++ b/Classes/EventListener/FrontendLoginEventListener.php @@ -18,10 +18,8 @@ namespace Causal\Oidc\EventListener; use Causal\Oidc\Service\OpenIdConnectService; -use InvalidArgumentException; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; -use Throwable; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\FrontendLogin\Event\ModifyLoginFormViewEvent; @@ -31,16 +29,9 @@ class FrontendLoginEventListener implements LoggerAwareInterface public function modifyLoginFormView(ModifyLoginFormViewEvent $event): void { - $authService = GeneralUtility::makeInstance(OpenIdConnectService::class); - try { - $authContext = $authService->generateAuthenticationContext($GLOBALS['TYPO3_REQUEST']); - $uri = $authContext->getAuthorizationUrl(); - } catch (InvalidArgumentException $e) { - $uri = '#InvalidOIDCConfiguration'; - } catch (Throwable $e) { - // whatever the provider did wrong (can be connection errors) - $uri = '#oidcError'; + $uri = GeneralUtility::makeInstance(OpenIdConnectService::class)->getAuthenticationRequestUrl(); + if ($uri) { + $event->getView()->assign('openidConnectUri', (string)$uri); } - $event->getView()->assign('openidConnectUri', $uri); } } diff --git a/Classes/Middleware/AuthenticationUrlRequest.php b/Classes/Middleware/AuthenticationUrlRequest.php new file mode 100644 index 0000000..709c364 --- /dev/null +++ b/Classes/Middleware/AuthenticationUrlRequest.php @@ -0,0 +1,51 @@ +openIdConnectService = $openIdConnectService; + } + + /** + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return ResponseInterface + * see https://github.com/thephpleague/oauth2-client + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + if ($request->getMethod() === 'GET' && $this->openIdConnectService->isAuthenticationRequest($request)) { + try { + $authContext = $this->openIdConnectService->generateAuthenticationContext($request); + $uri = $authContext->getAuthorizationUrl(); + return new RedirectResponse($uri); + } catch (InvalidArgumentException|Throwable $e) { + // config error or + // whatever the provider did wrong (can be connection errors) + return (new Response())->withStatus(500, 'Authentication provider error'); + } + } + return $handler->handle($request); + } +} diff --git a/Classes/Service/OpenIdConnectService.php b/Classes/Service/OpenIdConnectService.php index 66e1539..9f2cf3d 100644 --- a/Classes/Service/OpenIdConnectService.php +++ b/Classes/Service/OpenIdConnectService.php @@ -6,12 +6,14 @@ use Causal\Oidc\AuthenticationContext; use InvalidArgumentException; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\UriInterface; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; -use TYPO3\CMS\Core\Http\ServerRequest; use TYPO3\CMS\Core\Http\Uri; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; class OpenIdConnectService implements LoggerAwareInterface { @@ -32,7 +34,33 @@ public function __construct(OAuthService $OAuthService, array $config = []) $this->config = $config ?: GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('oidc') ?? []; } - public function generateAuthenticationContext(ServerRequest $request, array $authorizationUrlOptions = []): AuthenticationContext + public function isAuthenticationRequest(ServerRequestInterface $request): bool + { + $language = $request->getAttribute('language'); + return $request->getUri()->getPath() === $language->getBase()->getPath() . $this->config['authenticationUrlRoute']; + } + + public function getAuthenticationRequestUrl(): ?UriInterface + { + /** @var TypoScriptFrontendController $tsfe */ + $tsfe = $GLOBALS['TSFE'] ?? null; + $request = $GLOBALS['TYPO3_REQUEST'] ?? null; + if ($tsfe && $request) { + $loginUrl = GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL'); + $redirectUrl = $request->getParsedBody()['redirect_url'] ?? $request->getQueryParams()['redirect_url'] ?? ''; + $query = GeneralUtility::implodeArrayForUrl('', [ + 'login_url' => $loginUrl, + 'redirect_url' => $redirectUrl, + 'validation_hash' => GeneralUtility::hmac($loginUrl . $redirectUrl, 'oidc'), + ]); + return $tsfe->getLanguage()->getBase() + ->withPath($tsfe->getLanguage()->getBase()->getPath() . $this->config['authenticationUrlRoute']) + ->withQuery($query); + } + return null; + } + + public function generateAuthenticationContext(ServerRequestInterface $request, array $authorizationUrlOptions = []): AuthenticationContext { if (empty($this->config['oidcClientKey']) || empty($this->config['oidcClientSecret']) @@ -42,6 +70,13 @@ public function generateAuthenticationContext(ServerRequest $request, array $aut throw new InvalidArgumentException('Missing extension configuration', 1715775147); } + $loginUrl = $request->getQueryParams()['login_url'] ?? ''; + $redirectUrl = $request->getQueryParams()['redirect_url'] ?? ''; + $hash = $request->getQueryParams()['validation_hash'] ?? ''; + if (GeneralUtility::hmac($loginUrl . $redirectUrl, 'oidc') !== $hash) { + throw new InvalidArgumentException('Invalid query string', 1719003567); + } + $requestId = $this->getUniqueId(); $codeVerifier = null; if ($this->config['enableCodeVerifier']) { @@ -50,11 +85,19 @@ public function generateAuthenticationContext(ServerRequest $request, array $aut $authorizationUrlOptions = array_merge($authorizationUrlOptions, $this->getCodeChallengeOptions($codeChallenge)); } - $redirectUrl = $request->getParsedBody()['redirect_url'] ?? $request->getQueryParams()['redirect_url'] ?? ''; + $this->OAuthService->setSettings($this->config); - $this->authContext = $this->prepareAuthorizationContext($authorizationUrlOptions, $requestId); - $this->authContext->redirectUrl = $redirectUrl; - $this->authContext->codeVerifier = $codeVerifier; + $authorizationUrl = $this->OAuthService->getAuthorizationUrl($authorizationUrlOptions); + $state = $this->OAuthService->getState(); + + $this->authContext = new AuthenticationContext( + $state, + (string)$this->getLoginUrlForContext($loginUrl), + $authorizationUrl, + $requestId, + $redirectUrl, + $codeVerifier + ); $this->logger->debug('Generated new Authentication Context', ['authContext' => $this->authContext]); @@ -87,22 +130,9 @@ public function getFinalLoginUrl(string $code): Uri return $loginUrl->withQuery(ltrim($query, '&')); } - /** - * Prepares the authorization URL and corresponding expected state (to mitigate CSRF attack) - */ - protected function prepareAuthorizationContext(array $authorizationUrlOptions, string $requestId): AuthenticationContext - { - $this->OAuthService->setSettings($this->config); - - $authorizationUrl = $this->OAuthService->getAuthorizationUrl($authorizationUrlOptions); - $state = $this->OAuthService->getState(); - - return new AuthenticationContext($state, (string)$this->getLoginUrlForContext(), $authorizationUrl, $requestId); - } - - protected function getLoginUrlForContext(): Uri + protected function getLoginUrlForContext(string $loginUrl): Uri { - $loginUrl = new Uri(GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL')); + $loginUrl = new Uri($loginUrl); // filter query string $queryParts = array_filter(explode('&', $loginUrl->getQuery()), function ($k) { diff --git a/Classes/ViewHelpers/OidcLinkViewHelper.php b/Classes/ViewHelpers/OidcLinkViewHelper.php index cc68f30..f49d77c 100644 --- a/Classes/ViewHelpers/OidcLinkViewHelper.php +++ b/Classes/ViewHelpers/OidcLinkViewHelper.php @@ -18,8 +18,6 @@ namespace Causal\Oidc\ViewHelpers; use Causal\Oidc\Service\OpenIdConnectService; -use InvalidArgumentException; -use Throwable; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper; @@ -38,16 +36,7 @@ class OidcLinkViewHelper extends AbstractViewHelper */ public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext) { - $authService = GeneralUtility::makeInstance(OpenIdConnectService::class); - try { - $authContext = $authService->generateAuthenticationContext($GLOBALS['TYPO3_REQUEST']); - $uri = $authContext->getAuthorizationUrl(); - } catch (InvalidArgumentException $e) { - $uri = '#InvalidOIDCConfiguration'; - } catch (Throwable $e) { - // whatever the provider did wrong (can be connection errors) - $uri = '#oidcError'; - } - return $uri; + $uri = GeneralUtility::makeInstance(OpenIdConnectService::class)->getAuthenticationRequestUrl(); + return (string)$uri; } } diff --git a/Configuration/RequestMiddlewares.php b/Configuration/RequestMiddlewares.php index 04398fc..2825885 100644 --- a/Configuration/RequestMiddlewares.php +++ b/Configuration/RequestMiddlewares.php @@ -7,8 +7,17 @@ 'typo3/cms-core/normalized-params-attribute', ], 'before' => [ - 'typo3/cms-frontend/eid' - ] + 'typo3/cms-frontend/eid', + ], + ], + 'oidcauthurl' => [ + 'target' => \Causal\Oidc\Middleware\AuthenticationUrlRequest::class, + 'after' => [ + 'typo3/cms-frontend/site', + ], + 'before' => [ + 'typo3/cms-frontend/authentication', + ], ], ], ]; diff --git a/README.md b/README.md index 1ff843c..a9e1e45 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ the authorization server. ```html - OpenID Connect + OpenID Connect Invalid OpenID Connect configuration diff --git a/Resources/Private/Language/de.locallang_db.xlf b/Resources/Private/Language/de.locallang_db.xlf index 0d505f6..9e50df0 100644 --- a/Resources/Private/Language/de.locallang_db.xlf +++ b/Resources/Private/Language/de.locallang_db.xlf @@ -1,6 +1,6 @@ - - + +
@@ -19,6 +19,10 @@ Authentication service Quality: This is used by TYPO3 if two authentication services have the same priority. Higher number wins. Authentifizierungs-Service Qualität: TYPO3 nutzt diesen Wert, wenn zwei Authentifizierungs-Services die gleiche Priorität besitzen. Höhere Zahl gewinnt. + + Route for retrieving the authentication URL of the Identity Provider + Route um die Authentifzierungs-URL des Identitäts-Anbieters zu erhalten + Enable PKCE: Enable PKCE flow. Code challenge and code verifier will be sent along. PKCE aktivieren: Aktiviert den PKCE-Fluss. Code-Challenge und Code-Verifier werden mitgeschickt. diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index 9a972da..ff6cf8b 100755 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -1,6 +1,6 @@ - - + +
@@ -15,6 +15,9 @@ Authentication service Quality: This is used by TYPO3 if two authentication services have the same priority. Higher number wins. + + Route for retrieving the authentication URL of the Identity Provider + Enable PKCE: Enable PKCE flow. Code challenge and code verifier will be sent along. diff --git a/Resources/Private/Templates/Login/Login.html b/Resources/Private/Templates/Login/Login.html index 187f397..8dde321 100644 --- a/Resources/Private/Templates/Login/Login.html +++ b/Resources/Private/Templates/Login/Login.html @@ -50,14 +50,7 @@

- - - OpenID Connect - - - Invalid OpenID Connect configuration - - + Login with OpenID Connect
diff --git a/ext_conf_template.txt b/ext_conf_template.txt index 61f8f7c..8846dcf 100644 --- a/ext_conf_template.txt +++ b/ext_conf_template.txt @@ -66,3 +66,6 @@ authenticationServicePriority = 82 # cat=advanced//5; type=int+; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.authenticationServiceQuality authenticationServiceQuality = 80 + +# cat=advanced//6; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.authenticationUrlRoute +authenticationUrlRoute = oidc/authentication From a90f2eea4ac1e4d102dd40e7867680d3e57b3033 Mon Sep 17 00:00:00 2001 From: Denis Lorch Date: Mon, 1 Jul 2024 11:48:41 +0200 Subject: [PATCH 120/142] Update AuthenticationGetUserGroupsEvent to latest event changes * Add auth service instance * Align event class * Add CHANGELOG entry --- CHANGELOG.md | 1 + .../AuthenticationGetUserGroupsEvent.php | 28 +++++++++++++++++-- Classes/Service/AuthenticationService.php | 2 +- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dcc3f2..cf027c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,3 +3,4 @@ ## Version 2.x * Enhanced events to include a reference to the AuthenticationService [#136](https://github.com/xperseguers/t3ext-oidc/issues/136) +* Added a user groups event to map groups by a different pattern than "Roles", e.g. "claims" [#129](https://github.com/xperseguers/t3ext-oidc/pull/129) diff --git a/Classes/Event/AuthenticationGetUserGroupsEvent.php b/Classes/Event/AuthenticationGetUserGroupsEvent.php index fe4d7f2..6c39c28 100644 --- a/Classes/Event/AuthenticationGetUserGroupsEvent.php +++ b/Classes/Event/AuthenticationGetUserGroupsEvent.php @@ -2,28 +2,45 @@ declare(strict_types=1); +/* + * 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! + */ + namespace Causal\Oidc\Event; +use TYPO3\CMS\Core\Authentication\AbstractAuthenticationService; + /** * Customize user group mapping - * if your resource owner structure differs form the default "Roles" workflow + * if your resource owner structure differs from the default "Roles" workflow */ final class AuthenticationGetUserGroupsEvent { protected string $groupTable; protected array $groups; protected array $resource; + protected AbstractAuthenticationService $authenticationService; /** * @param string $groupTable - fe_groups or be_groups * @param array $groups - known user group ids * @param array $resourceOwner - resource owner data */ - public function __construct(string $groupTable, array $groups, array $resourceOwner) + public function __construct(string $groupTable, array $groups, array $resourceOwner, AbstractAuthenticationService $authenticationService) { $this->groupTable = $groupTable; $this->groups = $groups; $this->resource = $resourceOwner; + $this->authenticationService = $authenticationService; } public function getGroupTable(): string @@ -41,6 +58,11 @@ public function getResource(): array return $this->resource; } + public function getAuthenticationService(): AbstractAuthenticationService + { + return $this->authenticationService; + } + /** * Set your customized user group ids */ @@ -48,4 +70,4 @@ public function setUserGroups(array $groups): void { $this->groups = $groups; } -} \ No newline at end of file +} diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 3e39880..eb5e1a1 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -436,7 +436,7 @@ protected function convertResourceOwner(array $info) // emit a generic groups mapping event // to customize the groups if the resource structure pattern "Roles" does not fit - $event = new AuthenticationGetUserGroupsEvent($userGroupTable, $newUserGroups, $info); + $event = new AuthenticationGetUserGroupsEvent($userGroupTable, $newUserGroups, $info, $this); $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); $eventDispatcher->dispatch($event); if ($newUserGroups !== $event->getUserGroups()) { From 5b7f46e3c9503f8726360ebc272cde7361d6a7cd Mon Sep 17 00:00:00 2001 From: Eike Starkmann Date: Tue, 24 Oct 2023 10:57:07 +0200 Subject: [PATCH 121/142] [TASK] make compatible for TYPO3 12.4 --- Classes/Hooks/DataHandlerOidc.php | 6 +++--- Classes/Service/AuthenticationService.php | 24 ++++++++++++----------- composer.json | 6 +++--- ext_emconf.php | 2 +- ext_localconf.php | 2 +- 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/Classes/Hooks/DataHandlerOidc.php b/Classes/Hooks/DataHandlerOidc.php index 25d4298..d908646 100644 --- a/Classes/Hooks/DataHandlerOidc.php +++ b/Classes/Hooks/DataHandlerOidc.php @@ -17,6 +17,7 @@ namespace Causal\Oidc\Hooks; +use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -52,7 +53,7 @@ public function processDatamap_afterDatabaseOperations(string $operation, string ->where( $queryBuilder->expr()->inSet('usergroup', (string)$id) ) - ->execute(); + ->executeQuery(); $tableConnection = GeneralUtility::makeInstance(ConnectionPool::class) ->getConnectionForTable('fe_users'); @@ -66,7 +67,7 @@ public function processDatamap_afterDatabaseOperations(string $operation, string 'fe_users', [ 'usergroup' => implode(',', $userGroups), - 'tstamp' => $GLOBALS['EXEC_TIME'], + 'tstamp' => GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('date', 'timestamp'), ], [ 'uid' => $user['uid'], @@ -75,5 +76,4 @@ public function processDatamap_afterDatabaseOperations(string $operation, string } } } - } diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index eb5e1a1..ec84891 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -250,7 +250,7 @@ protected function getUserFromAccessToken(OAuthService $service, AccessToken $ac } throw new RuntimeException( 'Resource owner does not have a sub part: ' . json_encode($resourceOwner) - . '. Your access token has been revoked. Please try again.', + . '. Your access token has been revoked. Please try again.', 1490086626 ); } @@ -317,12 +317,12 @@ protected function convertResourceOwner(array $info) GeneralUtility::intExplode(',', $this->config['usersStoragePid']), Connection::PARAM_INT_ARRAY )), - $queryBuilder->expr()->orX( + $queryBuilder->expr()->or( $queryBuilder->expr()->eq('tx_oidc', $queryBuilder->createNamedParameter($info['sub'])), $queryBuilder->expr()->eq('username', $queryBuilder->createNamedParameter($info['email'])) ) ) - ->execute() + ->executeQuery() ->fetchAssociative(); $reEnableUser = (bool)$this->config['reEnableFrontendUsers']; @@ -387,7 +387,7 @@ protected function convertResourceOwner(array $info) $queryBuilder->expr()->in('uid', $currentUserGroups), $queryBuilder->expr()->neq('tx_oidc_pattern', $queryBuilder->quote('')) ) - ->execute() + ->executeQuery() ->fetchAllAssociative(); $oidcUserGroups = []; @@ -410,7 +410,7 @@ protected function convertResourceOwner(array $info) ->where( $queryBuilder->expr()->neq('tx_oidc_pattern', $queryBuilder->quote('')) ) - ->execute() + ->executeQuery() ->fetchAllAssociative(); $roles = is_array($info['Roles']) ? $info['Roles'] : GeneralUtility::trimExplode(',', $info['Roles'], true); @@ -465,7 +465,7 @@ protected function convertResourceOwner(array $info) 'old' => $row, 'new' => $user, ]); - $user['tstamp'] = $GLOBALS['EXEC_TIME']; + $user['tstamp'] = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('date', 'timestamp'); $tableConnection->update( $userTable, $user, @@ -485,7 +485,7 @@ protected function convertResourceOwner(array $info) $data = array_merge($data, [ 'pid' => GeneralUtility::intExplode(',', $this->config['usersStoragePid'], true)[0], 'usergroup' => implode(',', $newUserGroups), - 'crdate' => $GLOBALS['EXEC_TIME'], + 'crdate' => GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('date', 'timestamp'), 'tx_oidc' => $info['sub'], ]); @@ -541,11 +541,13 @@ protected function getUserByUidAndTable(int $uid, string $table): array $queryResult = $queryBuilder ->select('*') ->from($table) - ->where($queryBuilder->expr()->eq( - 'uid', - $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)) + ->where( + $queryBuilder->expr()->eq( + 'uid', + $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT) + ) ) - ->execute(); + ->executeQuery(); $user = $queryResult->fetchAssociative(); if (!is_array($user) || $user === []) { diff --git a/composer.json b/composer.json index e75abbe..66ad466 100644 --- a/composer.json +++ b/composer.json @@ -24,9 +24,9 @@ "require": { "php": ">= 7.4.0, <= 8.3.99", "ext-json": "*", - "typo3/cms-core": "^11.5", - "typo3/cms-frontend": "^11.5", - "typo3/cms-felogin": "^11.5", + "typo3/cms-core": "^11.5 | ^12.4", + "typo3/cms-frontend": "^11.5 | ^12.4", + "typo3/cms-felogin": "^11.5 | ^12.4", "league/oauth2-client": "^2.7", "firebase/php-jwt": "^6.10" }, diff --git a/ext_emconf.php b/ext_emconf.php index 8913378..661b2b3 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -11,7 +11,7 @@ 'constraints' => [ 'depends' => [ 'php' => '7.4.0-8.3.99', - 'typo3' => '11.5.0-11.5.99', + 'typo3' => '11.5.0-12.9.99', ], 'conflicts' => [], 'suggests' => [], diff --git a/ext_localconf.php b/ext_localconf.php index cc4b826..d593e37 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -44,7 +44,7 @@ ); // Add typoscript for custom login plugin -ExtensionManagementUtility::addPItoST43('oidc', null, '_login'); +\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPItoST43('oidc', '', '_login'); // Require 3rd-party libraries, in case TYPO3 does not run in composer mode $pharFileName = ExtensionManagementUtility::extPath('oidc') . 'Libraries/league-oauth2-client.phar'; From f07041d5f01099f4b2dad279ceefb2d4c1a641aa Mon Sep 17 00:00:00 2001 From: Eike Starkmann Date: Tue, 24 Oct 2023 17:02:19 +0200 Subject: [PATCH 122/142] [TASK] authorizationUrlOptions might be not set --- Classes/Controller/LoginController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php index 9b00a75..5fa7e89 100644 --- a/Classes/Controller/LoginController.php +++ b/Classes/Controller/LoginController.php @@ -72,7 +72,7 @@ public function login(string $_, ?array $pluginConfiguration) $this->redirect($redirectUrl); } - $authorizationUrl = $this->determineAuthorizationUrl($pluginConfiguration['authorizationUrlOptions.']); + $authorizationUrl = $this->determineAuthorizationUrl($pluginConfiguration['authorizationUrlOptions.'] ?? []); $this->redirect($authorizationUrl); } From aee232b04d493dfb3cc9351bff7e19317a16bff6 Mon Sep 17 00:00:00 2001 From: Eike Starkmann Date: Mon, 30 Oct 2023 12:30:05 +0100 Subject: [PATCH 123/142] [TASK] remove signal slot --- Classes/Service/AuthenticationService.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index ec84891..62b8615 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -37,6 +37,7 @@ use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction; use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction; use TYPO3\CMS\Core\Http\ServerRequestFactory; +use TYPO3\CMS\Core\Information\Typo3Version; use TYPO3\CMS\Core\Routing\PageArguments; use TYPO3\CMS\Core\Routing\RouteNotFoundException; use TYPO3\CMS\Core\Routing\SiteMatcher; @@ -131,9 +132,13 @@ public function getUser() // dispatch a signal (containing the user with his access token if auth was successful) // so other extensions can use them to make further requests to an API // provided by the authentication server - /** @var Dispatcher $dispatcher */ - $dispatcher = GeneralUtility::makeInstance(ObjectManager::class)->get(Dispatcher::class); - $dispatcher->dispatch(__CLASS__, 'getUser', [$user]); + $versionInformation = GeneralUtility::makeInstance(Typo3Version::class); + if ($versionInformation->getMajorVersion() < 12) { + //can not use import statement, Dispatcher is no longer available in TYPO3 12 + /** @var Dispatcher $dispatcher */ + $dispatcher = GeneralUtility::makeInstance(ObjectManager::class)->get(Dispatcher::class); + $dispatcher->dispatch(__CLASS__, 'getUser', [$user]); + } $event = new AuthenticationGetUserEvent($user, $this); $eventDispatcher->dispatch($event); From eb6a054b0e8c01fd897b699401d35219a93630b7 Mon Sep 17 00:00:00 2001 From: Felix Althaus Date: Tue, 7 Nov 2023 18:22:40 +0100 Subject: [PATCH 124/142] [BUGFIX] Fix null pointer exceptions --- Classes/Service/AuthenticationService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 62b8615..48fb94c 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -324,7 +324,7 @@ protected function convertResourceOwner(array $info) )), $queryBuilder->expr()->or( $queryBuilder->expr()->eq('tx_oidc', $queryBuilder->createNamedParameter($info['sub'])), - $queryBuilder->expr()->eq('username', $queryBuilder->createNamedParameter($info['email'])) + $queryBuilder->expr()->eq('username', $queryBuilder->createNamedParameter($info['email'] ?? '')) ) ) ->executeQuery() From 24c6187a1c6edb2956249bfa3c50503d9dbaa08e Mon Sep 17 00:00:00 2001 From: Felix Althaus Date: Wed, 8 Nov 2023 16:03:09 +0100 Subject: [PATCH 125/142] [BUGFIX][WIP] Mitigate CSRF protection for fe_login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The frontend login process is protected against CSRF beginning with TYPO3 12. For now we fake the request token when the GET variable tx_oidc is set. I doubt connect action‘s redirect is still necessary. This should still be checked. --- .../ProcessRequestTokenListener.php | 23 +++++++++++++++++++ Configuration/Services.yaml | 6 +++++ 2 files changed, 29 insertions(+) create mode 100644 Classes/EventListener/ProcessRequestTokenListener.php diff --git a/Classes/EventListener/ProcessRequestTokenListener.php b/Classes/EventListener/ProcessRequestTokenListener.php new file mode 100644 index 0000000..898c8ad --- /dev/null +++ b/Classes/EventListener/ProcessRequestTokenListener.php @@ -0,0 +1,23 @@ +getUser(); + $requestToken = $event->getRequestToken(); + if ($requestToken instanceof RequestToken) { + // fine, there is a valid request-token + return; + } + // @TODO how to improve security? Maybe get rid of redirect in \Causal\Oidc\Controller\AuthenticationController::connectAction? + if (!isset($_GET['tx_oidc'])) { + return; + } + $event->setRequestToken(RequestToken::create('core/user-auth/' . strtolower($user->loginType))); + } +} diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index 1f8c492..1e47020 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -19,3 +19,9 @@ services: Causal\Oidc\Service\OpenIdConnectService: public: true + + Causal\Oidc\EventListener\ProcessRequestTokenListener: + tags: + - name: event.listener + identifier: 'causal/oidc-request-token' + event: TYPO3\CMS\Core\Authentication\Event\BeforeRequestTokenProcessedEvent From df5865e89a60af4ff174c85cd76b37452830768a Mon Sep 17 00:00:00 2001 From: Eike Starkmann Date: Wed, 3 Apr 2024 16:01:42 +0200 Subject: [PATCH 126/142] [TASK] make 12.4 compatible again --- ext_localconf.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext_localconf.php b/ext_localconf.php index d593e37..65f14d0 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -44,7 +44,7 @@ ); // Add typoscript for custom login plugin -\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPItoST43('oidc', '', '_login'); +ExtensionManagementUtility::addPItoST43('oidc', '', '_login'); // Require 3rd-party libraries, in case TYPO3 does not run in composer mode $pharFileName = ExtensionManagementUtility::extPath('oidc') . 'Libraries/league-oauth2-client.phar'; From fd035df8660711ea075b9a27998c57efe7863fb8 Mon Sep 17 00:00:00 2001 From: Eike Starkmann Date: Thu, 4 Apr 2024 10:11:23 +0200 Subject: [PATCH 127/142] [TASK] decalre strict, use PSR-12 / CGL Ref: #130 --- Classes/EventListener/ProcessRequestTokenListener.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Classes/EventListener/ProcessRequestTokenListener.php b/Classes/EventListener/ProcessRequestTokenListener.php index 898c8ad..26af4b7 100644 --- a/Classes/EventListener/ProcessRequestTokenListener.php +++ b/Classes/EventListener/ProcessRequestTokenListener.php @@ -1,4 +1,6 @@ Date: Thu, 4 Apr 2024 11:44:21 +0200 Subject: [PATCH 128/142] [TASK] add version check Ref: #130 --- Classes/Service/AuthenticationService.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 48fb94c..1f92f36 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -46,7 +46,6 @@ use TYPO3\CMS\Core\TypoScript\TemplateService; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\RootlineUtility; -use TYPO3\CMS\Extbase\Object\ObjectManager; use TYPO3\CMS\Extbase\SignalSlot\Dispatcher; use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; @@ -134,10 +133,10 @@ public function getUser() // provided by the authentication server $versionInformation = GeneralUtility::makeInstance(Typo3Version::class); if ($versionInformation->getMajorVersion() < 12) { - //can not use import statement, Dispatcher is no longer available in TYPO3 12 - /** @var Dispatcher $dispatcher */ - $dispatcher = GeneralUtility::makeInstance(ObjectManager::class)->get(Dispatcher::class); - $dispatcher->dispatch(__CLASS__, 'getUser', [$user]); + //can not use import statement, Dispatcher is no longer available in typo3 12 + /** @var TYPO3\CMS\Extbase\SignalSlot\Dispatcher $dispatcher */ + $dispatcher = GeneralUtility::makeInstance(TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class); + $dispatcher->dispatch(__CLASS__, 'getUser', ['user' => $user]); } $event = new AuthenticationGetUserEvent($user, $this); From cbf00845ef0613771767a59dadf819582efa8b20 Mon Sep 17 00:00:00 2001 From: Eike Starkmann Date: Thu, 11 Apr 2024 14:01:39 +0200 Subject: [PATCH 129/142] [TASK] change typo3 to TYPO3 in comment and user 12.4 as dependency --- Classes/Service/AuthenticationService.php | 2 +- ext_emconf.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 1f92f36..512043a 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -133,7 +133,7 @@ public function getUser() // provided by the authentication server $versionInformation = GeneralUtility::makeInstance(Typo3Version::class); if ($versionInformation->getMajorVersion() < 12) { - //can not use import statement, Dispatcher is no longer available in typo3 12 + //can not use import statement, Dispatcher is no longer available in TYPO3 12 /** @var TYPO3\CMS\Extbase\SignalSlot\Dispatcher $dispatcher */ $dispatcher = GeneralUtility::makeInstance(TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class); $dispatcher->dispatch(__CLASS__, 'getUser', ['user' => $user]); diff --git a/ext_emconf.php b/ext_emconf.php index 661b2b3..4bcdd6a 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -11,7 +11,7 @@ 'constraints' => [ 'depends' => [ 'php' => '7.4.0-8.3.99', - 'typo3' => '11.5.0-12.9.99', + 'typo3' => '11.5.0-12.4.99', ], 'conflicts' => [], 'suggests' => [], From 382ee47bb582ac57c002ff9194910c506bd2a294 Mon Sep 17 00:00:00 2001 From: gemeinling Date: Fri, 26 Jul 2024 09:14:54 +0200 Subject: [PATCH 130/142] Requested changes --- Classes/EventListener/ProcessRequestTokenListener.php | 5 +++-- Classes/Service/AuthenticationService.php | 10 ---------- composer.json | 8 ++++---- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/Classes/EventListener/ProcessRequestTokenListener.php b/Classes/EventListener/ProcessRequestTokenListener.php index 26af4b7..4a699d4 100644 --- a/Classes/EventListener/ProcessRequestTokenListener.php +++ b/Classes/EventListener/ProcessRequestTokenListener.php @@ -1,6 +1,7 @@ getRequest()->getQueryParams()['tx_oidc'])) { return; } $event->setRequestToken(RequestToken::create('core/user-auth/' . strtolower($user->loginType))); diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index 512043a..db88010 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -37,7 +37,6 @@ use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction; use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction; use TYPO3\CMS\Core\Http\ServerRequestFactory; -use TYPO3\CMS\Core\Information\Typo3Version; use TYPO3\CMS\Core\Routing\PageArguments; use TYPO3\CMS\Core\Routing\RouteNotFoundException; use TYPO3\CMS\Core\Routing\SiteMatcher; @@ -46,7 +45,6 @@ use TYPO3\CMS\Core\TypoScript\TemplateService; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\RootlineUtility; -use TYPO3\CMS\Extbase\SignalSlot\Dispatcher; use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; @@ -131,14 +129,6 @@ public function getUser() // dispatch a signal (containing the user with his access token if auth was successful) // so other extensions can use them to make further requests to an API // provided by the authentication server - $versionInformation = GeneralUtility::makeInstance(Typo3Version::class); - if ($versionInformation->getMajorVersion() < 12) { - //can not use import statement, Dispatcher is no longer available in TYPO3 12 - /** @var TYPO3\CMS\Extbase\SignalSlot\Dispatcher $dispatcher */ - $dispatcher = GeneralUtility::makeInstance(TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class); - $dispatcher->dispatch(__CLASS__, 'getUser', ['user' => $user]); - } - $event = new AuthenticationGetUserEvent($user, $this); $eventDispatcher->dispatch($event); $user = $event->getUser(); diff --git a/composer.json b/composer.json index 66ad466..d57b8ac 100644 --- a/composer.json +++ b/composer.json @@ -24,9 +24,9 @@ "require": { "php": ">= 7.4.0, <= 8.3.99", "ext-json": "*", - "typo3/cms-core": "^11.5 | ^12.4", - "typo3/cms-frontend": "^11.5 | ^12.4", - "typo3/cms-felogin": "^11.5 | ^12.4", + "typo3/cms-core": "^11.5 || ^12.4", + "typo3/cms-frontend": "^11.5 || ^12.4", + "typo3/cms-felogin": "^11.5 || ^12.4", "league/oauth2-client": "^2.7", "firebase/php-jwt": "^6.10" }, @@ -84,4 +84,4 @@ "typo3/class-alias-loader": true } } -} +} \ No newline at end of file From e2432e5c77dc1baf7042be4cc252474df393573b Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Tue, 30 Jul 2024 11:17:26 +0200 Subject: [PATCH 131/142] [FEATURE] Add event to modify where-conditions for fe_users Resolves: #164 --- CHANGELOG.md | 1 + .../Event/AuthenticationFetchUserEvent.php | 75 +++++++++++++++++++ Classes/Service/AuthenticationService.php | 30 +++++--- 3 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 Classes/Event/AuthenticationFetchUserEvent.php diff --git a/CHANGELOG.md b/CHANGELOG.md index cf027c3..d74bbec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,5 +2,6 @@ ## Version 2.x +* Added an event allowing to adjust the where-conditions for fetching the existing fe_users [#164](https://github.com/xperseguers/t3ext-oidc/issues/164) * Enhanced events to include a reference to the AuthenticationService [#136](https://github.com/xperseguers/t3ext-oidc/issues/136) * Added a user groups event to map groups by a different pattern than "Roles", e.g. "claims" [#129](https://github.com/xperseguers/t3ext-oidc/pull/129) diff --git a/Classes/Event/AuthenticationFetchUserEvent.php b/Classes/Event/AuthenticationFetchUserEvent.php new file mode 100644 index 0000000..f31ca64 --- /dev/null +++ b/Classes/Event/AuthenticationFetchUserEvent.php @@ -0,0 +1,75 @@ + + */ + protected array $conditions; + + protected QueryBuilder $queryBuilder; + + protected AbstractAuthenticationService $authenticationService; + + public function __construct( + array $resourceInfo, + array $conditions, + QueryBuilder $queryBuilder, + AbstractAuthenticationService $authenticationService + ) { + $this->resourceInfo = $resourceInfo; + $this->conditions = $conditions; + $this->queryBuilder = $queryBuilder; + $this->authenticationService = $authenticationService; + } + + public function getResourceInfo(): array { + return $this->resourceInfo; + } + + /** + * @return array + */ + public function getConditions(): array + { + return $this->conditions; + } + + public function setConditions(array $conditions): void + { + $this->conditions = $conditions; + } + + public function getQueryBuilder(): QueryBuilder + { + return $this->queryBuilder; + } + + public function getAuthenticationService(): AbstractAuthenticationService + { + return $this->authenticationService; + } +} diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index eb5e1a1..8d487ce 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -17,6 +17,7 @@ namespace Causal\Oidc\Service; +use Causal\Oidc\Event\AuthenticationFetchUserEvent; use Causal\Oidc\Event\AuthenticationGetUserEvent; use Causal\Oidc\Event\AuthenticationGetUserGroupsEvent; use Causal\Oidc\Event\AuthenticationPreUserEvent; @@ -302,6 +303,8 @@ public function authUser(array $user): int */ protected function convertResourceOwner(array $info) { + $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); + $mode = $this->authInfo['loginType']; $userTable = $this->db_user['table']; $userGroupTable = $mode === 'FE' ? 'fe_groups' : 'be_groups'; @@ -309,19 +312,25 @@ protected function convertResourceOwner(array $info) $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable($userTable); $queryBuilder->getRestrictions()->removeAll(); + + $userFetchConditions = [ + $queryBuilder->expr()->in('pid', $queryBuilder->createNamedParameter( + GeneralUtility::intExplode(',', $this->config['usersStoragePid']), + Connection::PARAM_INT_ARRAY + )), + $queryBuilder->expr()->orX( + $queryBuilder->expr()->eq('tx_oidc', $queryBuilder->createNamedParameter($info['sub'])), + $queryBuilder->expr()->eq('username', $queryBuilder->createNamedParameter($info['email'])) + ) + ]; + + $event = new AuthenticationFetchUserEvent($info, $userFetchConditions, $queryBuilder, $this); + $eventDispatcher->dispatch($event); + $row = $queryBuilder ->select('*') ->from($userTable) - ->where( - $queryBuilder->expr()->in('pid', $queryBuilder->createNamedParameter( - GeneralUtility::intExplode(',', $this->config['usersStoragePid']), - Connection::PARAM_INT_ARRAY - )), - $queryBuilder->expr()->orX( - $queryBuilder->expr()->eq('tx_oidc', $queryBuilder->createNamedParameter($info['sub'])), - $queryBuilder->expr()->eq('username', $queryBuilder->createNamedParameter($info['email'])) - ) - ) + ->where(...$event->getConditions()) ->execute() ->fetchAssociative(); @@ -437,7 +446,6 @@ protected function convertResourceOwner(array $info) // emit a generic groups mapping event // to customize the groups if the resource structure pattern "Roles" does not fit $event = new AuthenticationGetUserGroupsEvent($userGroupTable, $newUserGroups, $info, $this); - $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); $eventDispatcher->dispatch($event); if ($newUserGroups !== $event->getUserGroups()) { $this->logger->debug('Got customized user groups by AuthenticationGetUserGroupsEvent', [ From f508689375af68a2d47ec22ebf4fb0b723a94cf6 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Wed, 31 Jul 2024 10:21:27 +0200 Subject: [PATCH 132/142] [BUGFIX] Use TYPO3 request factory and request In order to adhere to global guzzle TYPO3 settings the OAuth provider is extended to pass the necessary factories along. Related #166 --- .../Factory/GenericOAuthProviderFactory.php | 18 +++++++++- Classes/Factory/RequestFactory.php | 36 +++++++++++++++++++ Configuration/Services.yaml | 6 ++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 Classes/Factory/RequestFactory.php diff --git a/Classes/Factory/GenericOAuthProviderFactory.php b/Classes/Factory/GenericOAuthProviderFactory.php index 44f39e4..e874348 100644 --- a/Classes/Factory/GenericOAuthProviderFactory.php +++ b/Classes/Factory/GenericOAuthProviderFactory.php @@ -19,13 +19,27 @@ use League\OAuth2\Client\Provider\AbstractProvider; use League\OAuth2\Client\Provider\GenericProvider; +use TYPO3\CMS\Core\Http\Client\GuzzleClientFactory; use TYPO3\CMS\Core\Utility\GeneralUtility; final class GenericOAuthProviderFactory implements OAuthProviderFactoryInterface { + private RequestFactory $requestFactory; + + public function __construct(RequestFactory $requestFactory) + { + $this->requestFactory = $requestFactory; + } public function create(array $settings): AbstractProvider { + // @todo Use DI for GuzzleClientFactory once TYPO3 v11 support is dropped + $clientFactory = GeneralUtility::makeInstance(GuzzleClientFactory::class); + $collaborators = [ + 'httpClient' => $clientFactory->getClient(), + 'requestFactory' => $this->requestFactory, + ]; + return new GenericProvider([ 'clientId' => $settings['oidcClientKey'], 'clientSecret' => $settings['oidcClientSecret'], @@ -34,6 +48,8 @@ public function create(array $settings): AbstractProvider 'urlAccessToken' => $settings['oidcEndpointToken'], 'urlResourceOwnerDetails' => $settings['oidcEndpointUserInfo'], 'scopes' => GeneralUtility::trimExplode(',', $settings['oidcClientScopes'], true), - ]); + ], + $collaborators + ); } } diff --git a/Classes/Factory/RequestFactory.php b/Classes/Factory/RequestFactory.php new file mode 100644 index 0000000..3dee780 --- /dev/null +++ b/Classes/Factory/RequestFactory.php @@ -0,0 +1,36 @@ +requestFactory = $requestFactory; + } + + public function getRequest( + $method, + $uri, + array $headers = [], + $body = null, + $version = '1.1' + ) { + $request = $this->requestFactory->createRequest($method, $uri); + foreach ($headers as $name => $value) { + $request = $request->withHeader((string)$name, $value); + } + if ($body !== '' && $body !== null) { + $request = $request->withBody(Utils::streamFor($body)); + } + return $request->withProtocolVersion($version); + } +} diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index 1f8c492..b83a956 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -19,3 +19,9 @@ services: Causal\Oidc\Service\OpenIdConnectService: public: true + + Causal\Oidc\Factory\RequestFactory: + public: true + + Causal\Oidc\Factory\GenericOAuthProviderFactory: + public: true From 952b0f4281d4173d3a938bd9d0c8c86965450fe7 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Wed, 31 Jul 2024 14:40:37 +0200 Subject: [PATCH 133/142] [BUGFIX] Add fallback for setting authenticationUrlRoute The newly introduced `authenticationUrlRoute` setting may not be set in existing installations. Avoid PHP warnings by adding a fallback. Related #158 --- Classes/Service/OpenIdConnectService.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Classes/Service/OpenIdConnectService.php b/Classes/Service/OpenIdConnectService.php index 9f2cf3d..a3f301e 100644 --- a/Classes/Service/OpenIdConnectService.php +++ b/Classes/Service/OpenIdConnectService.php @@ -12,6 +12,7 @@ use Psr\Log\LoggerAwareTrait; use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; use TYPO3\CMS\Core\Http\Uri; +use TYPO3\CMS\Core\Site\Entity\SiteLanguage; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; @@ -36,8 +37,9 @@ public function __construct(OAuthService $OAuthService, array $config = []) public function isAuthenticationRequest(ServerRequestInterface $request): bool { + /** @var SiteLanguage $language */ $language = $request->getAttribute('language'); - return $request->getUri()->getPath() === $language->getBase()->getPath() . $this->config['authenticationUrlRoute']; + return $request->getUri()->getPath() === $this->getAuthenticationUrlRoutePath($language); } public function getAuthenticationRequestUrl(): ?UriInterface @@ -54,7 +56,7 @@ public function getAuthenticationRequestUrl(): ?UriInterface 'validation_hash' => GeneralUtility::hmac($loginUrl . $redirectUrl, 'oidc'), ]); return $tsfe->getLanguage()->getBase() - ->withPath($tsfe->getLanguage()->getBase()->getPath() . $this->config['authenticationUrlRoute']) + ->withPath($this->getAuthenticationUrlRoutePath($tsfe->getLanguage())) ->withQuery($query); } return null; @@ -175,4 +177,9 @@ protected function getCodeChallengeOptions($codeChallenge): array 'code_challenge_method' => 'S256', ]; } + + protected function getAuthenticationUrlRoutePath(SiteLanguage $language): string + { + return $language->getBase()->getPath() . ($this->config['authenticationUrlRoute'] ?? 'oidc/authentication'); + } } From e1bd46d7253f3dcc079b771f3d35e9dba317984c Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Thu, 1 Aug 2024 23:16:51 +0200 Subject: [PATCH 134/142] [BUGFIX] No hash validation for authentication url Ignore hash validation for authentication url if neither login nor redirect url are provided. --- Classes/Service/OpenIdConnectService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Service/OpenIdConnectService.php b/Classes/Service/OpenIdConnectService.php index 9f2cf3d..36fde64 100644 --- a/Classes/Service/OpenIdConnectService.php +++ b/Classes/Service/OpenIdConnectService.php @@ -73,7 +73,7 @@ public function generateAuthenticationContext(ServerRequestInterface $request, a $loginUrl = $request->getQueryParams()['login_url'] ?? ''; $redirectUrl = $request->getQueryParams()['redirect_url'] ?? ''; $hash = $request->getQueryParams()['validation_hash'] ?? ''; - if (GeneralUtility::hmac($loginUrl . $redirectUrl, 'oidc') !== $hash) { + if (($loginUrl || $redirectUrl) && GeneralUtility::hmac($loginUrl . $redirectUrl, 'oidc') !== $hash) { throw new InvalidArgumentException('Invalid query string', 1719003567); } From 2c7cbe73d44ffc42f8441545c9f8049f25836ccb Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Thu, 1 Aug 2024 23:37:39 +0200 Subject: [PATCH 135/142] [BUGFIX] LoginController should redirect a logged-in user If a user is already logged in do not restart the authentication process, but simply redirect as it would happen after a successful login. Resolves: #161 --- Classes/Controller/LoginController.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php index 9b00a75..fdb9143 100644 --- a/Classes/Controller/LoginController.php +++ b/Classes/Controller/LoginController.php @@ -18,6 +18,7 @@ namespace Causal\Oidc\Controller; use Causal\Oidc\Service\OpenIdConnectService; +use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Http\PropagateResponseException; use TYPO3\CMS\Core\Http\RedirectResponse; use TYPO3\CMS\Core\Http\ServerRequest; @@ -66,8 +67,9 @@ public function login(string $_, ?array $pluginConfiguration) $this->pluginConfiguration = $pluginConfiguration; } + $context = GeneralUtility::makeInstance(Context::class); $loginType = $this->request->getParsedBody()['logintype'] ?? $this->request->getQueryParams()['logintype'] ?? ''; - if ($loginType === 'login') { + if ($loginType === 'login' || $context->getAspect('frontend.user')->isLoggedIn()) { $redirectUrl = $this->determineRedirectUrl(); $this->redirect($redirectUrl); } From a783ba7e826bfeab4c403d65240e454c5c61f290 Mon Sep 17 00:00:00 2001 From: Markus Klein Date: Wed, 21 Aug 2024 18:07:18 +0200 Subject: [PATCH 136/142] [BUGFIX] Handle missing language request attribute If some weird arbitrary URLs are called for a website, the language may not be identified at all. Adjust the authentication request detection to cope with this situation. --- Classes/Service/OpenIdConnectService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Service/OpenIdConnectService.php b/Classes/Service/OpenIdConnectService.php index 36fde64..e7aac26 100644 --- a/Classes/Service/OpenIdConnectService.php +++ b/Classes/Service/OpenIdConnectService.php @@ -37,7 +37,7 @@ public function __construct(OAuthService $OAuthService, array $config = []) public function isAuthenticationRequest(ServerRequestInterface $request): bool { $language = $request->getAttribute('language'); - return $request->getUri()->getPath() === $language->getBase()->getPath() . $this->config['authenticationUrlRoute']; + return $language && $request->getUri()->getPath() === $language->getBase()->getPath() . $this->config['authenticationUrlRoute']; } public function getAuthenticationRequestUrl(): ?UriInterface From 5c404119bc97b0b6900921b0e9158ccef0661217 Mon Sep 17 00:00:00 2001 From: gemeinling Date: Mon, 2 Sep 2024 09:00:54 +0200 Subject: [PATCH 137/142] unnecessary todo removed --- Classes/EventListener/ProcessRequestTokenListener.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Classes/EventListener/ProcessRequestTokenListener.php b/Classes/EventListener/ProcessRequestTokenListener.php index 4a699d4..ec946f3 100644 --- a/Classes/EventListener/ProcessRequestTokenListener.php +++ b/Classes/EventListener/ProcessRequestTokenListener.php @@ -17,7 +17,6 @@ public function __invoke(BeforeRequestTokenProcessedEvent $event): void // fine, there is a valid request-token return; } - // @TODO how to improve security? Maybe get rid of redirects? if (!isset($event->getRequest()->getQueryParams()['tx_oidc'])) { return; } From f39514f44391d2e0e20dbb5ccda5db1622665042 Mon Sep 17 00:00:00 2001 From: Xavier Perseguers Date: Sat, 21 Sep 2024 18:02:41 +0200 Subject: [PATCH 138/142] [TASK] Add second developer thanks to many contributions --- composer.json | 7 ++++++- ext_emconf.php | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index d57b8ac..ceb0ed9 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,11 @@ "email": "xavier@causal.ch", "homepage": "https://www.causal.ch", "role": "Developer" + }, + { + "name": "Markus Klein", + "homepage": "https://reelworx.at/", + "role": "Developer" } ], "license": "GPL-2.0-or-later", @@ -84,4 +89,4 @@ "typo3/class-alias-loader": true } } -} \ No newline at end of file +} diff --git a/ext_emconf.php b/ext_emconf.php index 4bcdd6a..7c0f4ba 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -3,8 +3,8 @@ 'title' => 'OpenID Connect Authentication', 'description' => 'This extension uses OpenID Connect to authenticate users.', 'category' => 'services', - 'author' => 'Xavier Perseguers', - 'author_company' => 'Causal Sàrl', + 'author' => 'Xavier Perseguers, Markus Klein', + 'author_company' => 'Causal Sàrl, Reelworx GmbH', 'author_email' => 'xavier@causal.ch', 'state' => 'stable', 'version' => '2.1.0', From 614686146692bfea979aa6578a13ca9ac5ff43f0 Mon Sep 17 00:00:00 2001 From: Hannes Lau Date: Tue, 9 Jul 2024 17:59:59 +0200 Subject: [PATCH 139/142] [FEATURE] Pass oidc resource owner info to ModifyUser event --- Classes/Event/ModifyUserEvent.php | 15 +++++++++++++-- Classes/Service/AuthenticationService.php | 4 ++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Classes/Event/ModifyUserEvent.php b/Classes/Event/ModifyUserEvent.php index 67c1b8e..09a2dbf 100644 --- a/Classes/Event/ModifyUserEvent.php +++ b/Classes/Event/ModifyUserEvent.php @@ -26,12 +26,18 @@ final class ModifyUserEvent */ protected array $user; + protected array $oidcResourceOwner; + protected AbstractAuthenticationService $authenticationService; - public function __construct(array $user, AbstractAuthenticationService $authenticationService) - { + public function __construct( + array $user, + AbstractAuthenticationService $authenticationService, + array $oidcResourceOwner + ) { $this->user = $user; $this->authenticationService = $authenticationService; + $this->oidcResourceOwner = $oidcResourceOwner; } public function getUser(): array @@ -48,4 +54,9 @@ public function getAuthenticationService(): AbstractAuthenticationService { return $this->authenticationService; } + + public function getOidcResourceOwner(): array + { + return $this->oidcResourceOwner; + } } diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index a182f51..77dc73c 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -457,7 +457,7 @@ protected function convertResourceOwner(array $info) $data['usergroup'] = implode(',', $newUserGroups); $user = array_merge($row, $data); - $event = new ModifyUserEvent($user, $this); + $event = new ModifyUserEvent($user, $this, $info); $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); $eventDispatcher->dispatch($event); $user = $event->getUser(); @@ -491,7 +491,7 @@ protected function convertResourceOwner(array $info) 'tx_oidc' => $info['sub'], ]); - $event = new ModifyUserEvent($data, $this); + $event = new ModifyUserEvent($data, $this, $info); $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); $eventDispatcher->dispatch($event); $data = $event->getUser(); From 6b803aca3137d2965d14c83a1ed0d3f73e2477b6 Mon Sep 17 00:00:00 2001 From: Hannes Lau Date: Wed, 10 Jul 2024 00:49:06 +0200 Subject: [PATCH 140/142] [BUGFIX] Fix type error for if the userinfo claims contain ints --- Classes/Service/AuthenticationService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index a182f51..0a6320b 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -652,7 +652,7 @@ protected function mergeSimple(array $oidc, array $typo3, string $field, string if (is_array($oidcValue)) { $oidcValue = $oidcValue[0]; } - $sectionValue = str_replace($fullMatchedMarker, $oidcValue, $sectionValue); + $sectionValue = str_replace($fullMatchedMarker, (string)$oidcValue, $sectionValue); } else { $sectionValue = str_replace($fullMatchedMarker, '', $sectionValue); } From bfa6f017184a3bc69037d280fa0a67e30403dfb8 Mon Sep 17 00:00:00 2001 From: Hannes Lau Date: Wed, 10 Jul 2024 11:39:36 +0200 Subject: [PATCH 141/142] [TASK] Remove fe_login extension from dependencies This oidc extension is not dependent on the fe_login extension. --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index ceb0ed9..90c3a36 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,6 @@ "ext-json": "*", "typo3/cms-core": "^11.5 || ^12.4", "typo3/cms-frontend": "^11.5 || ^12.4", - "typo3/cms-felogin": "^11.5 || ^12.4", "league/oauth2-client": "^2.7", "firebase/php-jwt": "^6.10" }, From 9aeeb8f64b15b2b58272255c9838727650852f7c Mon Sep 17 00:00:00 2001 From: Hannes Lau Date: Wed, 10 Jul 2024 00:48:08 +0200 Subject: [PATCH 142/142] [BUGFIX] Fix stdWrap in user fields mapping --- Classes/Service/AuthenticationService.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index a182f51..6890493 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -28,6 +28,7 @@ use League\OAuth2\Client\Token\AccessToken; use LogicException; use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Http\Message\ServerRequestInterface; use RuntimeException; use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException; @@ -37,6 +38,7 @@ use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction; use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction; use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction; +use TYPO3\CMS\Core\Http\ServerRequest; use TYPO3\CMS\Core\Http\ServerRequestFactory; use TYPO3\CMS\Core\Routing\PageArguments; use TYPO3\CMS\Core\Routing\RouteNotFoundException; @@ -93,7 +95,7 @@ public function getUser() $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); $user = false; - $request = $this->authInfo['request'] ?? $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals(); + $request = $this->getRequest(); $params = $request->getQueryParams()['tx_oidc'] ?? []; $code = $params['code'] ?? null; if ($code !== null) { @@ -598,6 +600,7 @@ protected function applyMapping(string $table, array $oidc, array $typo3, array /** @var $contentObj ContentObjectRenderer */ $contentObj = GeneralUtility::makeInstance(ContentObjectRenderer::class); + $contentObj->setRequest($this->getRequest()); $contentObj->start($oidc); // Process every TypoScript definition @@ -747,7 +750,7 @@ protected function generatePassword(): string protected function getLocalTSFE(): TypoScriptFrontendController { - $request = $this->authInfo['request'] ?? $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals(); + $request = $this->getRequest(); $siteMatcher = GeneralUtility::makeInstance(SiteMatcher::class); $routeResult = $siteMatcher->matchRequest($request); if ($routeResult instanceof SiteRouteResult) { @@ -773,4 +776,9 @@ protected function getLocalTSFE(): TypoScriptFrontendController } throw new InvalidArgumentException('Failed to initialize TSFE'); } + + protected function getRequest(): ServerRequestInterface + { + return $this->authInfo['request'] ?? $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals(); + } }