diff --git a/.gitignore b/.gitignore index dbc11c3..1def6bd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /Libraries/* +/.idea/* diff --git a/Classes/Hooks/FeloginHook.php b/Classes/Hooks/FeloginHook.php index a3d3f24..e08fd0b 100644 --- a/Classes/Hooks/FeloginHook.php +++ b/Classes/Hooks/FeloginHook.php @@ -14,6 +14,7 @@ namespace Causal\Oidc\Hooks; +use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; /** @@ -25,6 +26,7 @@ class FeloginHook /** * @param array $params * @param \TYPO3\CMS\Felogin\Controller\FrontendLoginController $pObj + * @return string */ public function postProcContent(array $params, \TYPO3\CMS\Felogin\Controller\FrontendLoginController $pObj) { @@ -32,7 +34,10 @@ public function postProcContent(array $params, \TYPO3\CMS\Felogin\Controller\Fro static::getLogger()->debug('Post-processing markers for felogin form', ['request' => $requestId]); $markerArray['###OPENID_CONNECT###'] = ''; - $settings = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['oidc']); + $extConf = isset($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['oidc']) + ? unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['oidc']) + : []; + $settings = $this->getConfig($extConf); if (empty($settings['oidcClientKey']) || empty($settings['oidcClientSecret']) @@ -135,6 +140,60 @@ protected function getUniqueId() return $uniqueId; } + /** + * Returns the extension config and tries to override endpoints if /.well-known/configuration exists on + * the issuer's side. + * + * @param array $config + * @return array + */ + protected function getConfig(array $config) + { + $extConfig = $config; + + if(isset($extConfig['oidcConfigUrl'])) { + try { + $wellKnownConfig = $this->getWellKnownConfig( + $extConfig['oidcConfigUrl'], + (int)$extConfig['oidcWellKnownCacheLifetime'] + ); + $extConfig['oidcEndpointAuthorize'] = $wellKnownConfig['authorization_endpoint']; + $extConfig['oidcEndpointToken'] = $wellKnownConfig['token_endpoint']; + $extConfig['oidcEndpointUserInfo'] = $wellKnownConfig['userinfo_endpoint']; + $extConfig['oidcEndpointRevoke'] = $wellKnownConfig['revocation_endpoint']; + $extConfig['oidcEndpointLogout'] = $wellKnownConfig['end_session_endpoint']; + } catch (\Exception $e) { + static::getLogger()->error('Could not process Well-Known Config', [ + 'message' => $e->getMessage(), + ]); + $extConfig = $config; + } + } + + return $extConfig; + } + + /** + * Get cached config from /.well-known/configuration URL, refreshes daily + * + * @param string $wellKnownUrl + * @param int $lifetime + * @return mixed + */ + protected function getWellKnownConfig($wellKnownUrl, $lifetime = 86400) + { + $cache_path = PATH_site . 'typo3temp/'; + $filename = $cache_path . md5(ExtensionManagementUtility::extPath('oidc')); + + if(file_exists($filename) && (time() - $lifetime < filemtime($filename))) { + return json_decode(file_get_contents($filename), true); + } else { + $data = file_get_contents($wellKnownUrl); + file_put_contents($filename, $data); + return json_decode($data, true); + } + } + /** * Returns a logger. * diff --git a/Classes/Service/AuthenticationService.php b/Classes/Service/AuthenticationService.php index f865492..a3019ad 100644 --- a/Classes/Service/AuthenticationService.php +++ b/Classes/Service/AuthenticationService.php @@ -14,9 +14,11 @@ namespace Causal\Oidc\Service; +use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\HttpUtility; use Causal\Oidc\Service\OAuthService; +use TYPO3\CMS\Extbase\Utility\DebuggerUtility; /** * OpenID Connect authentication service. @@ -44,13 +46,20 @@ class AuthenticationService extends \TYPO3\CMS\Sv\AuthenticationService */ const STATUS_AUTHENTICATION_FAILURE_CONTINUE = 100; + /** + * @var array + */ + private $config; + /** * AuthenticationService constructor. */ public function __construct() { - $config = $GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['oidc']; - $this->config = $config ? unserialize($config) : []; + $extConf = isset($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['oidc']) + ? unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['oidc']) + : []; + $this->config = $this->getConfig($extConf); } /** @@ -178,7 +187,7 @@ protected function getUserFromAccessToken(OAuthService $service, $accessToken) $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 ); } @@ -189,7 +198,7 @@ protected function getUserFromAccessToken(OAuthService $service, $accessToken) /** * Authenticate a user * - * @oaram array $user + * @param array $user * @return int */ public function authUser(array $user) @@ -207,7 +216,7 @@ public function authUser(array $user) * Converts a resource owner into a TYPO3 Frontend user. * * @param array $info - * @return array + * @return array|bool * @throws \InvalidArgumentException */ protected function convertResourceOwner(array $info) @@ -263,7 +272,7 @@ protected function convertResourceOwner(array $info) $newUsergroups = []; $defaultUserGroups = GeneralUtility::intExplode(',', $this->config['usersDefaultGroup'], true); - if ($row) { + if ($row && !$this->config['overrideNonOIDCRoles']) { $currentUserGroups = GeneralUtility::intExplode(',', $row['usergroup'], true); if (!empty($currentUserGroups)) { $oidcUserGroups = $database->exec_SELECTgetRows( @@ -281,14 +290,18 @@ protected function convertResourceOwner(array $info) } // Map OIDC roles to TYPO3 user groups - if (!empty($info['Roles'])) { + if (!empty($info[$this->config['oidcRolesClaim']])) { + $roles = $info[$this->config['oidcRolesClaim']]; $typo3Roles = $database->exec_SELECTgetRows( 'uid, tx_oidc_pattern', $userGroupTable, 'tx_oidc_pattern<>\'\' AND hidden=0 AND deleted=0' ); - $roles = GeneralUtility::trimExplode(',', $info['Roles'], true); - $roles = ',' . implode(',', $roles) . ','; + + if(!is_array($roles)) { + $roles = GeneralUtility::trimExplode(',', $roles, true); + } + $roles = ',' . implode(',', $info[$this->config['oidcRolesClaim']]) . ','; foreach ($typo3Roles as $typo3Role) { // Convert the pattern into a proper regular expression @@ -331,6 +344,7 @@ protected function convertResourceOwner(array $info) 'usergroup' => implode(',', $newUsergroups), 'crdate' => $GLOBALS['EXEC_TIME'], 'tx_oidc' => $info['sub'], + 'tx_extbase_type' => $this->config['usersExtbaseType'] ]); $database->exec_INSERTquery( $userTable, @@ -598,6 +612,60 @@ protected function getDatabaseConnection() return $GLOBALS['TYPO3_DB']; } + /** + * Returns the extension config and tries to override endpoints if /.well-known/configuration exists on + * the issuer's side. + * + * @param array $config + * @return array + */ + protected function getConfig(array $config) + { + $extConfig = $config; + + if(isset($extConfig['oidcConfigUrl'])) { + try { + $wellKnownConfig = $this->getWellKnownConfig( + $extConfig['oidcConfigUrl'], + (int)$extConfig['oidcWellKnownCacheLifetime'] + ); + $extConfig['oidcEndpointAuthorize'] = $wellKnownConfig['authorization_endpoint']; + $extConfig['oidcEndpointToken'] = $wellKnownConfig['token_endpoint']; + $extConfig['oidcEndpointUserInfo'] = $wellKnownConfig['userinfo_endpoint']; + $extConfig['oidcEndpointRevoke'] = $wellKnownConfig['revocation_endpoint']; + $extConfig['oidcEndpointLogout'] = $wellKnownConfig['end_session_endpoint']; + } catch (\Exception $e) { + static::getLogger()->error('Could not process Well-Known Config', [ + 'message' => $e->getMessage(), + ]); + $extConfig = $config; + } + } + + return $extConfig; + } + + /** + * Get cached config from /.well-known/configuration URL depending on lifetime + * + * @param string $wellKnownUrl + * @param int $lifetime + * @return mixed + */ + protected function getWellKnownConfig($wellKnownUrl, $lifetime) + { + $cache_path = PATH_site . 'typo3temp/'; + $filename = $cache_path . md5(ExtensionManagementUtility::extPath('oidc')); + + if(file_exists($filename) && (time() - $lifetime < filemtime($filename))) { + return json_decode(file_get_contents($filename), true); + } else { + $data = file_get_contents($wellKnownUrl); + file_put_contents($filename, $data); + return json_decode($data, true); + } + } + /** * Returns a logger. * diff --git a/Classes/Service/OAuthService.php b/Classes/Service/OAuthService.php index 76c9067..49703d4 100644 --- a/Classes/Service/OAuthService.php +++ b/Classes/Service/OAuthService.php @@ -15,9 +15,7 @@ namespace Causal\Oidc\Service; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Backend\Utility\BackendUtility; use League\OAuth2\Client\Token\AccessToken; -use TYPO3\CMS\Core\Utility\HttpUtility; /** * Class OAuthService. @@ -124,7 +122,7 @@ public function getAccessTokenWithRequestPathAuthentication($username, $password curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_VERBOSE, 1); curl_setopt($ch, CURLOPT_HEADER, 1); - $content = curL_exec($ch); + $content = curl_exec($ch); if ($content === false) { throw new \RuntimeException('Curl ERROR: ' . curl_error($ch), 1510049345); diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index 3efc630..24fd009 100755 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -12,6 +12,18 @@ Undelete Frontend Users: If ticked, will automatically restore Frontend users marked as "deleted" upon successful authentication + + Override all non OIDC Roles: If ticked, all manually added user groups will be overridden by the oidc roles mapping + + + Default User Extbase Type + + + Config URL (usually /.well-known/configuration) - leave empty for manual configuration: + + + Cache lifetime(in seconds) for /.well-known/configuration: + Client Key @@ -36,6 +48,9 @@ Endpoint URI for revoking the token + + Custom roles claim - default is "Roles" + Use Request Path Authentication: When ticked, this value will use Request Path Authentication instead of standard Password Grant. diff --git a/ext_conf_template.txt b/ext_conf_template.txt index f0b401b..50ec5ca 100644 --- a/ext_conf_template.txt +++ b/ext_conf_template.txt @@ -7,19 +7,31 @@ reEnableFrontendUsers = 0 # cat=basic/enable/3; type=boolean; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.undeleteFrontendUsers undeleteFrontendUsers = 0 +# cat=basic/enable/4; type=boolean; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.overrideNonOIDCRoles +overrideNonOIDCRoles = 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//3; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcClientKey +# cat=basic//3; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.defaultUserExtbaseType +usersExtbaseType = 0 + +# cat=basic//4; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcConfigUrl +oidcConfigUrl = + +# cat=basic//5; type=int; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcWellKnownCacheLifetime +oidcWellKnownCacheLifetime = 86400 + +# cat=basic//6; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcClientKey oidcClientKey = -# cat=basic//4; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcClientSecret +# cat=basic//7; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcClientSecret oidcClientSecret = -# cat=basic//5; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcClientScopes +# cat=basic//8; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcClientScopes oidcClientScopes = openid # cat=advanced/links/1; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcEndpointAuthorize @@ -37,6 +49,9 @@ oidcEndpointLogout = https://ids02.sac-cas.ch/oauth2/logout # cat=advanced/links/5; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcEndpointRevoke oidcEndpointRevoke = https://ids02.sac-cas.ch/oauth2/revoke +# cat=advanced/links/6; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcRolesClaim +oidcRolesClaim = Roles + # cat=advanced/enable/1; type=boolean; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcUseRequestPathAuthentication oidcUseRequestPathAuthentication = 0