Skip to content

Commit

Permalink
Allow selection of secondary OIDC provider
Browse files Browse the repository at this point in the history
Add the ability to define a secondary OIDC provider for authentication
and select it using a query param on the request URL.

E.g. Use the 'sample_provider' provider by modifying the AtoM URL before
pressing "Log in with SSO":

http://127.0.0.1:63001/index.php?secondary=sample_provider
  • Loading branch information
sbreker committed Jun 11, 2024
1 parent 00eda91 commit 3e0b828
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 57 deletions.
89 changes: 61 additions & 28 deletions plugins/arOidcPlugin/config/app.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,46 @@
## OIDC Plugin configuration.
all:
oidc:
# 'providers' is a list of one or many oidc providers. The provider identified in
# 'primary_provider_name' setting will be selected when authenticating using the "Log in
# with SSO" button in AtoM. Additional providers can be selected by adding a query param
# to the URL before selecting "Log in with SSO". See 'provider_query_param_name' setting
# for more info.
#
# Any number of secondary providers can be configured in this list. Provider names must be
# unique.
#
# OIDC provider endpoint settings:
# Default for Dex: 'http://dex:5556/dex'
# Default for Keycloak direct using https: 'https://keycloak:8443/realms/artefactual'
# NOTE: Always configure using SSL in production.
provider_url: 'https://keycloak:8443/realms/artefactual'
client_id: 'artefactual-atom'
client_secret: 'example-secret'
# Default for Keycloak direct using https: 'https://keycloak:8443/realms/client'
# NOTE: Always configure url using SSL in production.
providers:
primary:
url: 'https://keycloak:8443/realms/artefactual'
client_id: 'artefactual-atom'
client_secret: 'example-secret'

#sample_provider:
#url: 'https://keycloak:8443/realms/sample'
#client_id: 'sample-atom'
#client_secret: 'example-secret'

# Identifies the primary OIDC provider and corresponds to an entry in 'providers' above.
# The default provider name is 'primary'. If this setting is not defined, AtoM will default
# to the provider named "primary".
#
# primary_provider_name: primary

# This setting enables the use of additional providers and identifies the name of the query
# param that can be added to the refering URL when selecting "Log in with SSO" button in AtoM.
# To use: append the query param and the provider name to the URL before selecting
# "Log in with SSO". Must be uncommented to activate use of secondary providers.
#
# E.g. Use the 'sample_provider' provider by modifying the URL before pressing "Log in with SSO":
# http://127.0.0.1:63001/index.php?secondary=sample_provider
#
# provider_query_param_name: secondary

# Localhost port 63001 (127.0.0.1:63001) is used as a placeholder and
# should be replaced with your AtoM site's public IP and port.
# NOTE: Always configure using SSL in production.
Expand Down Expand Up @@ -43,43 +76,43 @@ all:
server_cert: false

scopes:
- 'openid'
# Use with Dex
# - 'offline_access'
- 'profile'
- 'email'
# Use with Dex
# - 'groups'
- 'openid'
# Use with Dex
# - 'offline_access'
- 'profile'
- 'email'
# Use with Dex
# - 'groups'

# Settings for parsing OIDC groups into AtoM group membership.
# Set set_groups_from_attributes to true to enable.
set_groups_from_attributes: true
user_groups:
administrator:
attribute_value: 'atom-admin'
group_id: 100
editor:
attribute_value: 'atom-editor'
group_id: 101
contributor:
attribute_value: 'atom-contributor'
group_id: 102
translator:
attribute_value: 'atom-translator'
group_id: 103
administrator:
attribute_value: 'atom-admin'
group_id: 100
editor:
attribute_value: 'atom-editor'
group_id: 101
contributor:
attribute_value: 'atom-contributor'
group_id: 102
translator:
attribute_value: 'atom-translator'
group_id: 103

# Identify token which contains role claims. Options are 'access-token',
# 'id-token', 'verified-claims', or 'user-info'.
# 'set_groups_from_attributes' must be 'true' to enable.
roles_source: access-token
roles_source: 'access-token'

# Identify the location of role claims within the token identified in
# `roles_source` above. This is an array containing the node path to
# locate the roles array in the OIDC token. By default this is found
# in Keycloak's access token under 'realm_access'/'roles'.
roles_path:
- realm_access
- roles
- 'realm_access'
- 'roles'

# Identify how IAM users are matched to users in AtoM. Two values are allowed:
# user_matching_source: oidc-email
Expand All @@ -89,7 +122,7 @@ all:
# Using oidc-email requires the 'email' scope to be set above in the
# 'scopes' setting. 'email' is an optional user setup field in Keycloak but
# MUST be set if matching to pre-existing AtoM user accounts is going to work.
user_matching_source: oidc-email
user_matching_source: 'oidc-email'

# Activate or disable the automatic creation of AtoM user records from OIDC
# endpoint details. Allowed settings are:
Expand Down
17 changes: 2 additions & 15 deletions plugins/arOidcPlugin/lib/arOidc.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,7 @@ public static function getOidcInstance()
return;
}

$provider_url = sfConfig::get('app_oidc_provider_url', '');
if (empty($provider_url)) {
throw new Exception('Invalid OIDC provider URL. Please review the app_oidc_provider_url parameter in plugin app.yml.');
}
$client_id = sfConfig::get('app_oidc_client_id', '');
if (empty($client_id)) {
throw new Exception('Invalid OIDC client id. Please review the app_oidc_client_id parameter in plugin app.yml.');
}
$client_secret = sfConfig::get('app_oidc_client_secret', '');
if (empty($client_secret)) {
throw new Exception('Invalid OIDC client secret. Please review the app_oidc_client_secret parameter in plugin app.yml.');
}

$oidc = new OpenIDConnectClient($provider_url, $client_id, $client_secret);
$oidc = new OpenIDConnectClient();

// Validate requested scopes.
$scopesArray = sfConfig::get('app_oidc_scopes', []);
Expand All @@ -65,7 +52,7 @@ public static function getOidcInstance()
// Validate redirect URL.
$redirectUrl = sfConfig::get('app_oidc_redirect_url', '');
if (empty($redirectUrl)) {
throw new Exception('Invalid OIDC redirect URL. Please review the app_oidc_provider_url parameter in plugin app.yml.');
throw new Exception('Invalid OIDC redirect URL. Please review the app_oidc_redirect_url parameter in plugin app.yml.');
}
$oidc->setRedirectURL($redirectUrl);

Expand Down
157 changes: 146 additions & 11 deletions plugins/arOidcPlugin/lib/oidcUser.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,27 @@ public function initialize(sfEventDispatcher $dispatcher, sfStorage $storage, $o
*
* @param null|mixed $username
* @param null|mixed $password
*
* @return bool
*/
public function authenticate($username = null, $password = null)
public function authenticate($username = null, $password = null): bool
{
$authenticated = false;
$user = null;
$authenticateResult = false;
$email = null;

$this->logger->err(sprintf('%s', json_encode(sfConfig::getAll())));

// Get provider ID from session storage as it may have been set elsewhere.
$providerId = $this->getSessionProviderId();
// Validate and set provider ID in session.
if (null !== $providerId = $this->validateProviderId($providerId, true)) {
// Set Provider details in OIDC client.
$result = $this->setOidcProviderDetails($providerId);
}
if (null === $providerId || !isset($result) || false === $result) {
return $authenticated;
}

if (isset($_REQUEST['code'])) {
$this->logger->info('OIDC request "code" is set.');
}
Expand Down Expand Up @@ -183,10 +194,8 @@ public function authenticate($username = null, $password = null)

/**
* Returns bool value indicating if this user is authenticated.
*
* @return bool
*/
public function isAuthenticated()
public function isAuthenticated(): bool
{
$authenticated = parent::isAuthenticated();

Expand All @@ -210,7 +219,11 @@ public function isAuthenticated()
if (null !== $expiryTime && $currentTime >= $expiryTime) {
try {
$this->logger->info('ID token expired - using refresh token to extend session.');
$refreshResult = $this->oidcClient->refreshToken($refreshToken);
$providerId = $this->getSessionProviderId();
// Set provider details in the OIDC client using provider id.
if (true === $this->setOidcProviderDetails($providerId)) {
$refreshResult = $this->oidcClient->refreshToken($refreshToken);
}

// Validate the new refresh token. If the refresh token is invalid, the user is logged out.
if (!isset($refreshResult->refresh_token) || empty($refreshResult->refresh_token)) {
Expand Down Expand Up @@ -245,7 +258,7 @@ public function isAuthenticated()
/**
* Logout from AtoM and the OIDC server.
*/
public function logout()
public function logout(): void
{
$idToken = $this->getAttribute('oidc-token', null);
$this->unsetAttributes();
Expand All @@ -262,13 +275,135 @@ public function logout()
// so $this->oidcClient->signOut will fail.
// https://github.com/dexidp/dex/issues/1697
try {
$this->oidcClient->signOut($idToken, $logoutRedirectUrl);
// Get saved session provider id.
$providerId = $this->getSessionProviderId();
// Set provider details in the OIDC client using provider id.
if (true === $result = $this->setOidcProviderDetails($providerId)) {
$this->oidcClient->signOut($idToken, $logoutRedirectUrl);
}
$this->setProviderId();
} catch (Exception $e) {
$this->setProviderId();
$this->logger->err($e->__toString().PHP_EOL);
}
}
}

// Parse the query params from a URL. If a param matches the provider ID selector
// then return the value.
public function parseProviderIdFromURL(string $url): ?string
{
if (empty($url)) {
return null;
}

$providerQueryParamName = sfConfig::get('app_oidc_provider_query_param_name', '');
if (empty($providerQueryParamName)) {
return null;
}

$urlParts = parse_url(strip_tags($url));
parse_str($urlParts['query'], $queryParts);

// Test if valid query param selector name.
if (isset($queryParts[$providerQueryParamName])) {
$providerId = $queryParts[$providerQueryParamName];
}

// A provider ID was specified.
if (isset($providerId)) {
return $providerId;
}

return null;
}

// Get provider ID from session storage.
public function getSessionProviderId(): string
{
return $this->getAttribute('oidc-session-provider-id', '');
}

// Set provider in session storage.
public function setSessionProviderId(string $providerId = ''): void
{
$this->setAttribute('oidc-session-provider-id', $providerId);
}

// Determine provider ID, test if valid and if so, save in session storage.
public function validateProviderId(string $providerId = '', bool $setSessionProviderId = false): ?string
{
// If not available get primary provider.
if (empty($providerId)) {
$providerId = sfConfig::get('app_oidc_primary_provider_name', 'primary');
}

// Get OIDC provider list. If none are configured this is an error.
$providers = sfConfig::get('app_oidc_providers', []);
if (empty($providers)) {
$this->logger->err('OIDC providers not found in app.yml - check plugin configuration. Unable to authenticate using OIDC.');

return null;
}

// Test if provider ID is valid.
if (!empty($providerId)
&& isset($providers[$providerId])
) {
// Save provider ID in session storage.
if (true === $setSessionProviderId) {
$this->setSessionProviderId($providerId);
}

return $providerId;
}

// Provider ID specified does not match any configured providers.
$this->logger->err('OIDC provider matching unsuccessful - check plugin configuration. Unable to authenticate using OIDC.');

return null;
}

// Look up provider details from provider ID and set values in the OIDC client object.
protected function setOidcProviderDetails(string $providerId = ''): bool
{
if (empty($providerId)) {
$this->logger->err('OIDC providers is empty - ensure setSessionProviderId() is called before calling setOidcProviderDetails(). Unable to authenticate using OIDC.');

return false;
}

// Get configured provider details from ID.
$providers = sfConfig::get('app_oidc_providers', []);
if (empty($providers)) {
$this->logger->err('OIDC providers not found in app.yml - check plugin configuration. Unable to authenticate using OIDC.');

return false;
}

// Get provider from list.
if (!empty($providerId)
&& isset($providers[$providerId])
) {
$provider = $providers[$providerId];
}

// Set provider details in OIDC Client.
if (isset($provider['url'], $provider['client_id'], $provider['client_secret'])
) {
$this->oidcClient->setProviderUrl($provider['url']);
$this->oidcClient->setClientID($provider['client_id']);
$this->oidcClient->setClientSecret($provider['client_secret']);
$this->oidcClient->setIssuer($provider['url']);

return true;
}

$this->logger->err('OIDC provider matching unsuccessful - check plugin provider configuration. Unable to authenticate using OIDC.');

return false;
}

/**
* getTokenContents() maps the location of the role info as set in app.yml
* to a function that extracts these claims details.
Expand Down Expand Up @@ -306,7 +441,7 @@ protected function getTokenContents($tokenType)
*
* @return array $roles
*/
protected function parseOidcRoleClaims($claims, $pathArray)
protected function parseOidcRoleClaims($claims, $pathArray): array
{
$currentElement = $claims;
foreach ($pathArray as $key) {
Expand Down Expand Up @@ -373,7 +508,7 @@ protected function setGroupsFromOidcGroups($user, array $groups)
/**
* Clear out session vars holding auth info.
*/
private function unsetAttributes()
private function unsetAttributes(): void
{
$this->setAttribute('oidc-token', '');
$this->setAttribute('oidc-expiry', '');
Expand Down
Loading

0 comments on commit 3e0b828

Please sign in to comment.