From 7fb40d25346670569704ea8e66baf9f2f6c76a70 Mon Sep 17 00:00:00 2001 From: Steve Breker Date: Thu, 4 Jul 2024 17:29:43 -0700 Subject: [PATCH] Allow independent OIDC config by provider Allow OIDC provider settings to be configured independently for each provider. This corrects an issue where these settings applied to every configured OIDC provider. OIDC app.yml settings now configurable per provider: - send_oidc_logout - enable_refresh_token_use - server_cert - set_groups_from_attributes - user_groups - scopes - roles_source - roles_path - user_matching_source - auto_create_atom_user --- .../templates/_showActions.mod_ext_auth.php | 2 +- .../templates/editSuccess.mod_ext_auth.php | 4 +- .../templates/listSuccess.mod_ext_auth.php | 2 +- plugins/arOidcPlugin/config/app.yml | 187 ++++++++++-------- plugins/arOidcPlugin/lib/arOidc.class.php | 24 --- plugins/arOidcPlugin/lib/oidcUser.class.php | 99 +++++++++- .../modules/user/actions/editAction.class.php | 8 +- .../modules/user/templates/_showActions.php | 2 +- .../modules/user/templates/editSuccess.php | 4 +- .../modules/user/templates/listSuccess.php | 2 +- 10 files changed, 209 insertions(+), 125 deletions(-) diff --git a/plugins/arDominionB5Plugin/modules/user/templates/_showActions.mod_ext_auth.php b/plugins/arDominionB5Plugin/modules/user/templates/_showActions.mod_ext_auth.php index 11db848840..38f088ef26 100644 --- a/plugins/arDominionB5Plugin/modules/user/templates/_showActions.mod_ext_auth.php +++ b/plugins/arDominionB5Plugin/modules/user/templates/_showActions.mod_ext_auth.php @@ -9,7 +9,7 @@
  • 'user', 'action' => 'delete'], ['class' => 'btn atom-btn-outline-danger']); ?>
  • - + user->getProviderConfigValue('auto_create_atom_user', true)) { ?>
  • 'user', 'action' => 'add'], ['class' => 'btn atom-btn-outline-light']); ?>
  • diff --git a/plugins/arDominionB5Plugin/modules/user/templates/editSuccess.mod_ext_auth.php b/plugins/arDominionB5Plugin/modules/user/templates/editSuccess.mod_ext_auth.php index a2679f57a2..51a6db2295 100644 --- a/plugins/arDominionB5Plugin/modules/user/templates/editSuccess.mod_ext_auth.php +++ b/plugins/arDominionB5Plugin/modules/user/templates/editSuccess.mod_ext_auth.php @@ -27,7 +27,7 @@
    - + user->getProviderConfigValue('auto_create_atom_user', true)) { ?> username); ?> email, null, ['type' => 'email']); ?> @@ -79,7 +79,7 @@
  • 'user', 'action' => 'list'], ['class' => 'btn atom-btn-outline-light', 'role' => 'button']); ?>
  • - + user->getProviderConfigValue('auto_create_atom_user', true)) { ?>
  • diff --git a/plugins/arDominionB5Plugin/modules/user/templates/listSuccess.mod_ext_auth.php b/plugins/arDominionB5Plugin/modules/user/templates/listSuccess.mod_ext_auth.php index 2d95a4ca41..25951b09d3 100644 --- a/plugins/arDominionB5Plugin/modules/user/templates/listSuccess.mod_ext_auth.php +++ b/plugins/arDominionB5Plugin/modules/user/templates/listSuccess.mod_ext_auth.php @@ -80,7 +80,7 @@ $pager]); ?> - +user->getProviderConfigValue('auto_create_atom_user', true)) { ?>
    'user', 'action' => 'add'], ['class' => 'btn atom-btn-outline-light']); ?>
    diff --git a/plugins/arOidcPlugin/config/app.yml b/plugins/arOidcPlugin/config/app.yml index a9a8f40615..d71929e332 100644 --- a/plugins/arOidcPlugin/config/app.yml +++ b/plugins/arOidcPlugin/config/app.yml @@ -20,10 +20,118 @@ all: client_id: 'artefactual-atom' client_secret: 'example-secret' + # Set to true if OIDC endpoint supports logout. + # Setting examples for tested OpenID providers: + # -------- + # Keycloak via Dex: + # send_oidc_logout: false + # Keycloak direct: + # send_oidc_logout: true + send_oidc_logout: true + + # Set to true if OIDC endpoint is configured to send refresh tokens. + enable_refresh_token_use: true + + # OIDC server SSL certificate location for server validation. + # Accepts a filepath or false (to disable, e.g. for development). + # Examples + # -------- + # Relative path to sf_root_dir: 'data/oidc/cert/mycert.pem' + # Absolute path: '/usr/var/certif/xxx.pem' + # Disable server validation: false + server_cert: false + + # 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 + + scopes: + - 'openid' + # Use with Dex + # - 'offline_access' + - 'profile' + - 'email' + # Use with Dex + # - 'groups' + + # 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' + + # 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' + + # Identify how IAM users are matched to users in AtoM. Two values are allowed: + # user_matching_source: oidc-email + # user_matching_source: oidc-username + # Using oidc-username will work without additional scopes being requested. + # + # 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' + + # Activate or disable the automatic creation of AtoM user records from OIDC + # endpoint details. Allowed settings are: + # + # true (default): AtoM will automatically create a user record on first login. + # + # false: AtoM will not automatically create a user record on first login - AtoM + # user must be created in advance to successfully authenticate in AtoM. + auto_create_atom_user: true + + # The following is an example secondary provider called 'sample_provider'. If + # uncommented, a second OIDC provider in the 'sample' Keycloak realm will be available. #sample_provider: #url: 'https://keycloak:8443/realms/sample' #client_id: 'sample-atom' #client_secret: 'example-secret' + #send_oidc_logout: true + #enable_refresh_token_use: true + #server_cert: false + #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 + #scopes: + # - 'openid' + # - 'profile' + # - 'email' + #roles_source: 'access-token' + #roles_path: + # - 'realm_access' + # - 'roles' + #user_matching_source: 'oidc-email' + #auto_create_atom_user: true # 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 @@ -46,15 +154,6 @@ all: # NOTE: Always configure using SSL in production. redirect_url: 'http://127.0.0.1:63001/index.php/oidc/login' - # Set to true if OIDC endpoint supports logout. - # Setting examples for tested OpenID providers: - # -------- - # Keycloak via Dex: - # send_oidc_logout: false - # Keycloak direct: - # send_oidc_logout: true - send_oidc_logout: true - # OIDC logout requires a URL to redirect to. Use this setting to # specify a page to redirect the user to on logout when # 'send_oidc_logout' is 'true'. Localhost port 63001 (127.0.0.1:63001) @@ -62,73 +161,3 @@ all: # public IP and port. # NOTE: Always configure using SSL in production. logout_redirect_url: 'http://127.0.0.1:63001' - - # Set to true if OIDC endpoint is configured to send refresh tokens. - enable_refresh_token_use: true - - # OIDC server SSL certificate location for server validation. - # Accepts a filepath or false (to disable, e.g. for development). - # Examples - # -------- - # Relative path to sf_root_dir: 'data/oidc/cert/mycert.pem' - # Absolute path: '/usr/var/certif/xxx.pem' - # Disable server validation: false - server_cert: false - - scopes: - - '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 - - # 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' - - # 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' - - # Identify how IAM users are matched to users in AtoM. Two values are allowed: - # user_matching_source: oidc-email - # user_matching_source: oidc-username - # Using oidc-username will work without additional scopes being requested. - # - # 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' - - # Activate or disable the automatic creation of AtoM user records from OIDC - # endpoint details. Allowed settings are: - # - # true (default): AtoM will automatically create a user record on first login. - # - # false: AtoM will not automatically create a user record on first login - AtoM - # user must be created in advance to successfully authenticate in AtoM. - auto_create_atom_user: true diff --git a/plugins/arOidcPlugin/lib/arOidc.class.php b/plugins/arOidcPlugin/lib/arOidc.class.php index 0a43c097fa..f9d450032e 100644 --- a/plugins/arOidcPlugin/lib/arOidc.class.php +++ b/plugins/arOidcPlugin/lib/arOidc.class.php @@ -39,16 +39,6 @@ public static function getOidcInstance() $oidc = new OpenIDConnectClient(); - // Validate requested scopes. - $scopesArray = sfConfig::get('app_oidc_scopes', []); - $validScopes = self::validateScopes($scopesArray); - // Add scopes only if the array is not empty - if (!empty($validScopes)) { - $oidc->addScope($validScopes); - } else { - throw new Exception('No valid scopes found in app_oidc_scopes.'); - } - // Validate redirect URL. $redirectUrl = sfConfig::get('app_oidc_redirect_url', ''); if (empty($redirectUrl)) { @@ -56,20 +46,6 @@ public static function getOidcInstance() } $oidc->setRedirectURL($redirectUrl); - // Validate the server SSL certificate according to configuration. - $certPath = sfConfig::get('app_oidc_server_cert', false); - if (0 === !strpos($certPath, '/')) { - $certPath = sfConfig::get('sf_root_dir').DIRECTORY_SEPARATOR.$certPath; - } - - if (file_exists($certPath)) { - $oidc->setCertPath($certPath); - } elseif (false === $certPath) { - // OIDC server SSL certificate disabled. - } else { - throw new Exception('Invalid SSL certificate settings. Please review the app_oidc_server_cert parameter in plugin app.yml.'); - } - self::$oidcIsInitialized = true; return $oidc; diff --git a/plugins/arOidcPlugin/lib/oidcUser.class.php b/plugins/arOidcPlugin/lib/oidcUser.class.php index 79bd598f6f..e2dc8d507d 100644 --- a/plugins/arOidcPlugin/lib/oidcUser.class.php +++ b/plugins/arOidcPlugin/lib/oidcUser.class.php @@ -55,13 +55,17 @@ public function authenticate($username = null, $password = null): bool $providerId = $this->getSessionProviderId(); // Validate and set provider ID in session. if (null !== $providerId = $this->validateProviderId($providerId, true)) { - // Set Provider details in OIDC client. + // Set provider details in OIDC client. $result = $this->setOidcProviderDetails($providerId); } if (null === $providerId || !isset($result) || false === $result) { return $authenticated; } + // Set provider server cert. + $this->setServerCert($this->getProviderConfigValue('server_cert', false)); + $this->setOidcScopes($this->getProviderConfigValue('scopes', [])); + if (isset($_REQUEST['code'])) { $this->logger->info('OIDC request "code" is set.'); } @@ -79,7 +83,7 @@ public function authenticate($username = null, $password = null): bool $expiryTime = $this->oidcClient->getVerifiedClaims('exp'); $this->setAttribute('oidc-expiry', $expiryTime); - if (true == sfConfig::get('app_oidc_enable_refresh_token_use', false)) { + if (true == $this->getProviderConfigValue('enable_refresh_token_use', false)) { $this->setAttribute('oidc-refresh', $this->oidcClient->getRefreshToken()); } } @@ -91,7 +95,7 @@ public function authenticate($username = null, $password = null): bool if ($authenticateResult) { // Validate user source setting. - $userMatchingSource = sfConfig::get('app_oidc_user_matching_source', ''); + $userMatchingSource = $this->getProviderConfigValue('user_matching_source', ''); if (!arOidc::validateUserMatchingSource($userMatchingSource)) { $this->logger->err('OIDC user matching source is configured but is not set properly. Unable to match OIDC users to AtoM users.'); $this->logout(); @@ -113,7 +117,7 @@ public function authenticate($username = null, $password = null): bool $user = QubitUser::getOne($criteria); } - $autoCreateUser = sfConfig::get('app_oidc_auto_create_atom_user', true); + $autoCreateUser = $this->getProviderConfigValue('auto_create_atom_user', true); if (!is_bool($autoCreateUser)) { $this->logger->err('OIDC auto_create_atom_user is configured but is not set properly - value should be of type bool. Unable to match OIDC users to AtoM users.'); $this->logout(); @@ -148,7 +152,7 @@ public function authenticate($username = null, $password = null): bool // Parse OIDC group claims into group memberships. If enabled, we perform this // check each time a user authenticates so that changes made on the OIDC // server are applied in AtoM on the next login. - $setGroupsFromClaims = sfConfig::get('app_oidc_set_groups_from_attributes', false); + $setGroupsFromClaims = $this->getProviderConfigValue('set_groups_from_attributes', false); if (!is_bool($setGroupsFromClaims)) { $this->logger->err('OIDC set_groups_from_attributes is configured but is not set properly - value should be of type bool. Unable to complete authentication.'); $this->logout(); @@ -156,8 +160,8 @@ public function authenticate($username = null, $password = null): bool return $authenticated; } if (true == $setGroupsFromClaims) { - $rolesPath = sfConfig::get('app_oidc_roles_path', []); - $rolesSource = sfConfig::get('app_oidc_roles_source', ''); + $rolesPath = $this->getProviderConfigValue('roles_path', []); + $rolesSource = $this->getProviderConfigValue('roles_source', ''); // Validate Settings. if (!arOidc::validateRolesSource($rolesSource) || empty($rolesPath)) { @@ -202,7 +206,7 @@ public function isAuthenticated(): bool { $authenticated = parent::isAuthenticated(); - if (false == sfConfig::get('app_oidc_enable_refresh_token_use', false) || false === $authenticated) { + if (false == $this->getProviderConfigValue('enable_refresh_token_use', false) || false === $authenticated) { return $authenticated; } @@ -225,6 +229,10 @@ public function isAuthenticated(): bool $providerId = $this->getSessionProviderId(); // Set provider details in the OIDC client using provider id. if (true === $this->setOidcProviderDetails($providerId)) { + // Set provider server cert. + $this->setServerCert($this->getProviderConfigValue('server_cert', false)); + $this->setOidcScopes($this->getProviderConfigValue('scopes', [])); + $refreshResult = $this->oidcClient->refreshToken($refreshToken); } @@ -267,15 +275,20 @@ public function logout(): void $this->unsetAttributes(); $this->signOut(); - if (true == sfConfig::get('app_oidc_send_oidc_logout', false) && !empty($idToken)) { + if (true == $this->getProviderConfigValue('send_oidc_logout', false) && !empty($idToken)) { $logoutRedirectUrl = sfConfig::get('app_oidc_logout_redirect_url', ''); if (empty($logoutRedirectUrl)) { $logoutRedirectUrl = null; $this->logger->err('Setting "app_oidc_logout_redirect_url" invalid. Unable to redirect on sign out.'); } + // Set provider server cert. + $this->setServerCert($this->getProviderConfigValue('server_cert', false)); + $this->setOidcScopes($this->getProviderConfigValue('scopes', [])); + // Get saved session provider id. $providerId = $this->getSessionProviderId(); + // Unset session provider Id. $this->setSessionProviderId(); // Set provider details in the OIDC client using provider id. @@ -292,6 +305,36 @@ public function logout(): void } } + // Get provider specific config vals from app.yml. + public function getProviderConfigValue(string $configVariableName = '', $default = null) + { + // Get saved session provider id. + $providerId = $this->getSessionProviderId(); + + // 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 $default; + } + + // Get provider from list. + if (!empty($providerId) + && isset($providers[$providerId]) + ) { + $provider = $providers[$providerId]; + } + + // Get config var from $provider array. + if (isset($provider[$configVariableName]) + ) { + return $provider[$configVariableName]; + } + + return $default; + } + // 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 @@ -434,6 +477,42 @@ protected function getTokenContents($tokenType) return null; } + /** + * Set provider server cert. + * + * @param mixed $certPath + */ + protected function setServerCert($certPath = false) + { + // Validate the server SSL certificate according to configuration. + if (0 === !strpos($certPath, '/')) { + $certPath = sfConfig::get('sf_root_dir').DIRECTORY_SEPARATOR.$certPath; + } + + if (file_exists($certPath)) { + $this->oidcClient->setCertPath($certPath); + } elseif (false === $certPath) { + // OIDC server SSL certificate disabled. + } else { + throw new Exception('Invalid OIDC SSL certificate settings. Please review the app_oidc_server_cert parameter in plugin app.yml.'); + } + } + + /** + * Set provider OIDC scopes from config. + */ + protected function setOidcScopes(array $scopes = []): void + { + // Validate requested scopes. + $validScopes = arOidc::validateScopes($scopes); + // Add scopes only if the array is not empty + if (!empty($validScopes)) { + $this->oidcClient->addScope($validScopes); + } else { + throw new Exception('No valid OIDC scopes found in app_oidc_scopes.'); + } + } + /** * Parse group claims for role info returned by OIDC server. Returns * array containing assigned roles. Claims are searched based on nodes @@ -494,7 +573,7 @@ protected function setGroupsFromOidcGroups($user, array $groups) // Add the user to AclUserGroups based on the presence of expected OIDC // group values as set in app_oidc_user_groups. - $userGroups = sfConfig::get('app_oidc_user_groups'); + $userGroups = $this->getProviderConfigValue('user_groups', []); foreach ($userGroups as $item) { if (null !== $group = QubitAclGroup::getById($item['group_id'])) { $expectedValue = $item['attribute_value']; diff --git a/plugins/arOidcPlugin/modules/user/actions/editAction.class.php b/plugins/arOidcPlugin/modules/user/actions/editAction.class.php index db9ccc3235..af57443c00 100644 --- a/plugins/arOidcPlugin/modules/user/actions/editAction.class.php +++ b/plugins/arOidcPlugin/modules/user/actions/editAction.class.php @@ -104,7 +104,7 @@ protected function earlyExecute() { $this->form->getValidatorSchema()->setOption('allow_extra_fields', true); - if (false === sfConfig::get('app_oidc_auto_create_atom_user', true)) { + if (false === sfContext::getinstance()->user->getProviderConfigValue('auto_create_atom_user', true)) { $this->form->getValidatorSchema()->setPostValidator( new sfValidatorCallback(['callback' => [$this, 'exists']]) ); @@ -136,7 +136,7 @@ protected function addField($name) { switch ($name) { case 'username': - if (false === sfConfig::get('app_oidc_auto_create_atom_user', true)) { + if (false === sfContext::getinstance()->user->getProviderConfigValue('auto_create_atom_user', true)) { $this->form->setDefault('username', $this->resource->username); $this->form->setValidator('username', new sfValidatorString(['required' => true])); $this->form->setWidget('username', new sfWidgetFormInput()); @@ -145,7 +145,7 @@ protected function addField($name) break; case 'email': - if (false === sfConfig::get('app_oidc_auto_create_atom_user', true)) { + if (false === sfContext::getinstance()->user->getProviderConfigValue('auto_create_atom_user', true)) { $this->form->setDefault('email', $this->resource->email); $this->form->setValidator('email', new sfValidatorEmail(['required' => true])); $this->form->setWidget('email', new sfWidgetFormInput()); @@ -315,7 +315,7 @@ protected function processField($field) case 'username': case 'email': - if (false === sfConfig::get('app_oidc_auto_create_atom_user', true)) { + if (false === sfContext::getinstance()->user->getProviderConfigValue('auto_create_atom_user', true)) { $this->resource[$name] = $this->form->getValue($name); } diff --git a/plugins/arOidcPlugin/modules/user/templates/_showActions.php b/plugins/arOidcPlugin/modules/user/templates/_showActions.php index 435ed65751..76d94d209f 100644 --- a/plugins/arOidcPlugin/modules/user/templates/_showActions.php +++ b/plugins/arOidcPlugin/modules/user/templates/_showActions.php @@ -10,7 +10,7 @@
  • 'user', 'action' => 'delete'], ['class' => 'c-btn c-btn-delete']); ?>
  • - + user->getProviderConfigValue('auto_create_atom_user', true)) { ?>
  • 'user', 'action' => 'add'], ['class' => 'c-btn']); ?>
  • diff --git a/plugins/arOidcPlugin/modules/user/templates/editSuccess.php b/plugins/arOidcPlugin/modules/user/templates/editSuccess.php index ad62633f3b..e15e709cf2 100644 --- a/plugins/arOidcPlugin/modules/user/templates/editSuccess.php +++ b/plugins/arOidcPlugin/modules/user/templates/editSuccess.php @@ -24,7 +24,7 @@ - + user->getProviderConfigValue('auto_create_atom_user', true)) { ?> username->renderRow(); ?> email->renderRow(); ?> @@ -71,7 +71,7 @@
  • 'user', 'action' => 'list'], ['class' => 'c-btn']); ?>
  • - + user->getProviderConfigValue('auto_create_atom_user', true)) { ?>
  • diff --git a/plugins/arOidcPlugin/modules/user/templates/listSuccess.php b/plugins/arOidcPlugin/modules/user/templates/listSuccess.php index 41fc14071c..9256e4db85 100644 --- a/plugins/arOidcPlugin/modules/user/templates/listSuccess.php +++ b/plugins/arOidcPlugin/modules/user/templates/listSuccess.php @@ -54,7 +54,7 @@ $pager]); ?> - +user->getProviderConfigValue('auto_create_atom_user', true)) { ?>
    • 'user', 'action' => 'add'], ['class' => 'c-btn']); ?>