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

Allow selection of secondary OIDC provider #1824

Merged
merged 1 commit into from
Jun 20, 2024
Merged
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
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
167 changes: 150 additions & 17 deletions plugins/arOidcPlugin/lib/oidcUser.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,25 @@ 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;

// 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 +192,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 +217,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 +256,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 @@ -258,17 +269,139 @@ public function logout()
$this->logger->err('Setting "app_oidc_logout_redirect_url" invalid. Unable to redirect on sign out.');
}

// Dex does not yet implement end_session_endpoint with it's oidc connector
// so $this->oidcClient->signOut will fail.
// https://github.com/dexidp/dex/issues/1697
try {
$this->oidcClient->signOut($idToken, $logoutRedirectUrl);
} catch (Exception $e) {
$this->logger->err($e->__toString().PHP_EOL);
// Get saved session provider id.
$providerId = $this->getSessionProviderId();
$this->setSessionProviderId();

// Set provider details in the OIDC client using provider id.
if (true === $this->setOidcProviderDetails($providerId)) {
try {
// Dex does not yet implement end_session_endpoint with it's oidc connector
// so $this->oidcClient->signOut will fail.
// https://github.com/dexidp/dex/issues/1697
$this->oidcClient->signOut($idToken, $logoutRedirectUrl);
} catch (Exception $e) {
$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 providers.
$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 +439,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 +506,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
Loading