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']); ?>