Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] add /.well-known/ configuration and custom role claim support #33

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@

/Libraries/*

/.idea/*
61 changes: 60 additions & 1 deletion Classes/Hooks/FeloginHook.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

namespace Causal\Oidc\Hooks;

use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
Expand All @@ -25,14 +26,18 @@ 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)
{
$requestId = $this->getUniqueId();
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'])
Expand Down Expand Up @@ -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.
*
Expand Down
86 changes: 77 additions & 9 deletions Classes/Service/AuthenticationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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
);
}
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
*
Expand Down
4 changes: 1 addition & 3 deletions Classes/Service/OAuthService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down
15 changes: 15 additions & 0 deletions Resources/Private/Language/locallang_db.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@
<trans-unit id="settings.undeleteFrontendUsers">
<source>Undelete Frontend Users: If ticked, will automatically restore Frontend users marked as "deleted" upon successful authentication</source>
</trans-unit>
<trans-unit id="settings.overrideNonOIDCRoles">
<source>Override all non OIDC Roles: If ticked, all manually added user groups will be overridden by the oidc roles mapping</source>
</trans-unit>
<trans-unit id="settings.defaultUserExtbaseType">
<source>Default User Extbase Type</source>
</trans-unit>
<trans-unit id="settings.oidcConfigUrl">
<source>Config URL (usually /.well-known/configuration) - leave empty for manual configuration:</source>
</trans-unit>
<trans-unit id="settings.oidcWellKnownCacheLifetime">
<source>Cache lifetime(in seconds) for /.well-known/configuration:</source>
</trans-unit>
<trans-unit id="settings.oidcClientKey">
<source>Client Key</source>
</trans-unit>
Expand All @@ -36,6 +48,9 @@
<trans-unit id="settings.oidcEndpointRevoke">
<source>Endpoint URI for revoking the token</source>
</trans-unit>
<trans-unit id="settings.oidcRolesClaim">
<source>Custom roles claim - default is "Roles"</source>
</trans-unit>
<trans-unit id="settings.oidcUseRequestPathAuthentication">
<source>Use Request Path Authentication: When ticked, this value will use Request Path Authentication instead of standard Password Grant.</source>
</trans-unit>
Expand Down
21 changes: 18 additions & 3 deletions ext_conf_template.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down