diff --git a/config/vufind/Shibboleth.ini b/config/vufind/Shibboleth.ini new file mode 100644 index 0000000000000000000000000000000000000000..292fbcdf4a94478024c56e1100ff2584831f19e0 --- /dev/null +++ b/config/vufind/Shibboleth.ini @@ -0,0 +1,11 @@ +; Each section of the file contains IdP configuration. +; The section name is important, because it defines prefix used for creating user cat_username attribute, that is used +; by MultiBackend driver. +[instance1] +; EntityId - required +entityId = https://example.org/idp/shibboleth +; You can override attribute mapping - default is in section Shibboleth in config.ini +cat_username = unstructuredName + +[instance2] +entityId = https://some.other.institution/shibboleth/ diff --git a/config/vufind/config.ini b/config/vufind/config.ini index 6e0c5d4fb6d22e44521d71282662a8b80c2c4dff..9779d2d51306182fbcfe7e34f013361660b4253d 100644 --- a/config/vufind/config.ini +++ b/config/vufind/config.ini @@ -704,16 +704,25 @@ database = mysql://root@localhost/vufind ; Optional: Session ID parameter for SAML2 single logout support. If omitted, single ; logout support is disabled. Note that if SLO support is enabled, Shibboleth session ; ID's are tracked in external_session table which may need to be cleaned up with the -; expire_session_mappings command line utility. See +; util/expire_external_sessions command line utility. See ; https://vufind.org/wiki/configuration:shibboleth for more information on how ; to configure the single logout support. ;session_id = Shib-Session-ID +; Check for expired session - user is automatically logged out when Shibboleth +; session is not present (default = true if unset); note that expiration check +; also requires session_id to be set above. +;checkExpiredSession = false ; Optional: you may set attribute names and values to be used as a filter; ; users will only be logged into VuFind if they match these filters. ;userattribute_1 = entitlement ;userattribute_value_1 = urn:mace:dir:entitlement:common-lib-terms ;userattribute_2 = unscoped-affiliation ;userattribute_value_2 = member +; Set to true when shibboleth attributes must be read from headers instead of +; environment variables - for example if you use proxy server and shibboleth is +; running on proxy side. In that case you should protect against header spoofing: +; see https://wiki.shibboleth.net/confluence/display/SP3/SpoofChecking for details +;use_headers = false ; Required: the attribute Shibboleth uses to uniquely identify users. ;username = persistent-id ; Required: Shibboleth login URL. @@ -735,6 +744,9 @@ database = mysql://root@localhost/vufind ;college = HTTP_COLLEGE ;major = HTTP_MAJOR ;home_library = HTTP_HOME_LIBRARY +; Enable if you want to override mapping or required attributes for specific IdP +; in Shibboleth.ini +;allow_configuration_override = true ; CAS is optional. This section only needs to exist if the ; Authentication Method is set to CAS. diff --git a/module/VuFind/src/VuFind/Auth/Shibboleth.php b/module/VuFind/src/VuFind/Auth/Shibboleth.php index fd31e7da37672be0a60c0b19f9a85fc942ffebd6..321d324d1349e2cb0a38e7114931d5c5ff1bbf9b 100644 --- a/module/VuFind/src/VuFind/Auth/Shibboleth.php +++ b/module/VuFind/src/VuFind/Auth/Shibboleth.php @@ -27,11 +27,14 @@ * @author Bernd Oberknapp <bo@ub.uni-freiburg.de> * @author Demian Katz <demian.katz@villanova.edu> * @author Ere Maijala <ere.maijala@helsinki.fi> + * @author Vaclav Rosecky <vaclav.rosecky@mzk.cz> * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org Main Page */ namespace VuFind\Auth; +use Laminas\Http\PhpEnvironment\Request; +use Vufind\Auth\Shibboleth\ConfigurationLoaderInterface; use VuFind\Exception\Auth as AuthException; /** @@ -44,13 +47,30 @@ use VuFind\Exception\Auth as AuthException; * @author Bernd Oberknapp <bo@ub.uni-freiburg.de> * @author Demian Katz <demian.katz@villanova.edu> * @author Ere Maijala <ere.maijala@helsinki.fi> + * @author Vaclav Rosecky <vaclav.rosecky@mzk.cz> * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org Main Page */ class Shibboleth extends AbstractBase { + /** + * Header name for entityID of the IdP that authenticated the user. + */ const DEFAULT_IDPSERVERPARAM = 'Shib-Identity-Provider'; + /** + * This is array of attributes which $this->authenticate() + * method should check for. + * + * WARNING: can contain only such attributes, which are writeable to user table! + * + * @var array attribsToCheck + */ + protected $attribsToCheck = [ + 'cat_username', 'cat_password', 'email', 'lastname', 'firstname', + 'college', 'major', 'home_library' + ]; + /** * Session manager * @@ -58,14 +78,74 @@ class Shibboleth extends AbstractBase */ protected $sessionManager; + /** + * Configuration loading implementation + * + * @var ConfigurationLoaderInterface + */ + protected $configurationLoader; + + /** + * Http Request object + * + * @var \Laminas\Http\PhpEnvironment\Request + */ + protected $request; + + /** + * Read attributes from headers instead of environment variables + * + * @var boolean + */ + protected $useHeaders = false; + + /** + * Name of attribute with shibboleth identity provider + * + * @var string + */ + protected $shibIdentityProvider = self::DEFAULT_IDPSERVERPARAM; + + /** + * Name of attribute with shibboleth session ID + * + * @var string + */ + protected $shibSessionId = null; + /** * Constructor * - * @param \Laminas\Session\ManagerInterface $sessionManager Session manager + * @param \Laminas\Session\ManagerInterface $sessionManager Session + * manager + * @param ConfigurationLoaderInterface $configurationLoader Configuration + * loader + * @param \Laminas\Http\PhpEnvironment\Request $request Http + * request object */ - public function __construct(\Laminas\Session\ManagerInterface $sessionManager) - { + public function __construct(\Laminas\Session\ManagerInterface $sessionManager, + ConfigurationLoaderInterface $configurationLoader, + \Laminas\Http\PhpEnvironment\Request $request + ) { $this->sessionManager = $sessionManager; + $this->configurationLoader = $configurationLoader; + $this->request = $request; + } + + /** + * Set configuration. + * + * @param \Laminas\Config\Config $config Configuration to set + * + * @return void + */ + public function setConfig($config) + { + parent::setConfig($config); + $this->useHeaders = $this->config->Shibboleth->use_headers ?? false; + $this->shibIdentityProvider = $this->config->Shibboleth->idpserverparam + ?? self::DEFAULT_IDPSERVERPARAM; + $this->shibSessionId = $this->config->Shibboleth->session_id ?? null; } /** @@ -105,23 +185,30 @@ class Shibboleth extends AbstractBase */ public function authenticate($request) { + // validate config before authentication + $this->validateConfig(); // Check if username is set. - $shib = $this->getConfig()->Shibboleth; - $username = $request->getServer()->get($shib->username); + $entityId = $this->getCurrentEntityId($request); + $shib = $this->getConfigurationLoader()->getConfiguration($entityId); + $username = $this->getAttribute($request, $shib['username']); if (empty($username)) { + $details = ($this->useHeaders) ? $request->getHeaders()->toArray() + : $request->getServer()->toArray(); $this->debug( - "No username attribute ({$shib->username}) present in request: " - . print_r($request->getServer()->toArray(), true) + "No username attribute ({$shib['username']}) present in request: " + . print_r($details, true) ); throw new AuthException('authentication_error_admin'); } // Check if required attributes match up: - foreach ($this->getRequiredAttributes() as $key => $value) { - if (!preg_match('/' . $value . '/', $request->getServer()->get($key))) { + foreach ($this->getRequiredAttributes($shib) as $key => $value) { + if (!preg_match("/$value/", $this->getAttribute($request, $key))) { + $details = ($this->useHeaders) ? $request->getHeaders()->toArray() + : $request->getServer()->toArray(); $this->debug( "Attribute '$key' does not match required value '$value' in" - . ' request: ' . print_r($request->getServer()->toArray(), true) + . ' request: ' . print_r($details, true) ); throw new AuthException('authentication_error_denied'); } @@ -135,19 +222,19 @@ class Shibboleth extends AbstractBase $catPassword = null; // Has the user configured attributes to use for populating the user table? - $attribsToCheck = [ - 'cat_username', 'cat_password', 'email', 'lastname', 'firstname', - 'college', 'major', 'home_library' - ]; - foreach ($attribsToCheck as $attribute) { - if (isset($shib->$attribute)) { - $value = $request->getServer()->get($shib->$attribute); + foreach ($this->attribsToCheck as $attribute) { + if (isset($shib[$attribute])) { + $value = $this->getAttribute($request, $shib[$attribute]); if ($attribute == 'email') { $user->updateEmail($value); - } elseif ($attribute != 'cat_password') { - $user->$attribute = ($value === null) ? '' : $value; - } else { + } elseif ($attribute == 'cat_username' && isset($shib['prefix']) + && !empty($value) + ) { + $user->cat_username = $shib['prefix'] . '.' . $value; + } elseif ($attribute == 'cat_password') { $catPassword = $value; + } else { + $user->$attribute = ($value === null) ? '' : $value; } } } @@ -167,22 +254,7 @@ class Shibboleth extends AbstractBase ); } - // Add session id mapping to external_session table for single logout support - if (isset($shib->session_id)) { - $shibSessionId = $request->getServer()->get($shib->session_id); - if (null !== $shibSessionId) { - $localSessionId = $this->sessionManager->getId(); - $externalSession = $this->getDbTableManager() - ->get('ExternalSession'); - $externalSession->addSessionMapping( - $localSessionId, $shibSessionId - ); - $this->debug( - "Cached Shibboleth session id '$shibSessionId' for local session" - . " '$localSessionId'" - ); - } - } + $this->storeShibbolethSession($request); // Save and return the user object: $user->save(); @@ -201,11 +273,7 @@ class Shibboleth extends AbstractBase public function getSessionInitiator($target) { $config = $this->getConfig(); - if (isset($config->Shibboleth->target)) { - $shibTarget = $config->Shibboleth->target; - } else { - $shibTarget = $target; - } + $shibTarget = $config->Shibboleth->target ?? $target; $append = (strpos($shibTarget, '?') !== false) ? '&' : '?'; // Adding the auth_method parameter makes it possible to handle logins when // using an auth method that proxies others. @@ -229,17 +297,13 @@ class Shibboleth extends AbstractBase public function isExpired() { $config = $this->getConfig(); - if (isset($config->Shibboleth->username) - && isset($config->Shibboleth->logout) + if (!isset($this->shibSessionId) + || !($config->Shibboleth->checkExpiredSession ?? true) ) { - // It would be more proper to call getServer on a Laminas request - // object... except that the request object doesn't exist yet when - // this routine gets called. - $username = isset($_SERVER[$config->Shibboleth->username]) - ? $_SERVER[$config->Shibboleth->username] : null; - return empty($username); + return false; } - return false; + $sessionId = $this->getAttribute($this->request, $this->shibSessionId); + return !isset($sessionId); } /** @@ -267,22 +331,34 @@ class Shibboleth extends AbstractBase return $url; } + /** + * Return configuration loader + * + * @return ConfigurationLoaderInterface configuration loader + */ + protected function getConfigurationLoader() + { + return $this->configurationLoader; + } + /** * Extract required user attributes from the configuration. * + * @param array $config shibboleth configuration + * * @return array Only username and attribute-related values + * @throws AuthException */ - protected function getRequiredAttributes() + protected function getRequiredAttributes($config) { // Special case -- store username as-is to establish return array: $sortedUserAttributes = []; // Now extract user attribute values: - $shib = $this->getConfig()->Shibboleth; - foreach ($shib as $key => $value) { + foreach ($config as $key => $value) { if (preg_match("/userattribute_[0-9]{1,}/", $key)) { $valueKey = 'userattribute_value_' . substr($key, 14); - $sortedUserAttributes[$value] = $shib->$valueKey ?? null; + $sortedUserAttributes[$value] = $config[$valueKey] ?? null; // Throw an exception if attributes are missing/empty. if (empty($sortedUserAttributes[$value])) { @@ -295,4 +371,60 @@ class Shibboleth extends AbstractBase return $sortedUserAttributes; } + + /** + * Add session id mapping to external_session table for single logout support + * + * @param \Laminas\Http\PhpEnvironment\Request $request Request object containing + * account credentials. + * + * @return void + */ + protected function storeShibbolethSession($request) + { + if (!isset($this->shibSessionId)) { + return; + } + $shibSessionId = $this->getAttribute($request, $this->shibSessionId); + if (null === $shibSessionId) { + return; + } + $localSessionId = $this->sessionManager->getId(); + $externalSession = $this->getDbTableManager()->get('ExternalSession'); + $externalSession->addSessionMapping($localSessionId, $shibSessionId); + $this->debug( + "Cached Shibboleth session id '$shibSessionId' for local session" + . " '$localSessionId'" + ); + } + + /** + * Fetch entityId used for authentication + * + * @param \Laminas\Http\PhpEnvironment\Request $request Request object + * + * @return string entityId of IdP + */ + protected function getCurrentEntityId($request) + { + return $this->getAttribute($request, $this->shibIdentityProvider); + } + + /** + * Extract attribute from request. + * + * @param \Laminas\Http\PhpEnvironment\Request $request Request object + * @param string $attribute Attribute name + * + * @return string attribute value + */ + protected function getAttribute($request, $attribute) + { + if ($this->useHeaders) { + $header = $request->getHeader($attribute); + return ($header) ? $header->getFieldValue() : null; + } else { + return $request->getServer()->get($attribute, null); + } + } } diff --git a/module/VuFind/src/VuFind/Auth/Shibboleth/ConfigurationLoaderInterface.php b/module/VuFind/src/VuFind/Auth/Shibboleth/ConfigurationLoaderInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..7825121f90b7cac80cab608e7b173c1269179d54 --- /dev/null +++ b/module/VuFind/src/VuFind/Auth/Shibboleth/ConfigurationLoaderInterface.php @@ -0,0 +1,35 @@ +<?php +/** + * Configuration loader interface + * + * PHP version 7 + * + * @category VuFind + * @package Authentication + * @author Vaclav Rosecky <vaclav.rosecky@mzk.cz> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +namespace VuFind\Auth\Shibboleth; + +/** + * Configuration loader interface + * + * @category VuFind + * @package Authentication + * @author Vaclav Rosecky <vaclav.rosecky@mzk.cz> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +interface ConfigurationLoaderInterface +{ + /** + * Return shibboleth configuration. + * + * @param string $entityId entity ID of IdP + * + * @throws \VuFind\Exception\Auth + * @return array shibboleth configuration + */ + public function getConfiguration($entityId); +} diff --git a/module/VuFind/src/VuFind/Auth/Shibboleth/MultiIdPConfigurationLoader.php b/module/VuFind/src/VuFind/Auth/Shibboleth/MultiIdPConfigurationLoader.php new file mode 100644 index 0000000000000000000000000000000000000000..37a43a745c8d6ae518c3e763646efe16f24c23c7 --- /dev/null +++ b/module/VuFind/src/VuFind/Auth/Shibboleth/MultiIdPConfigurationLoader.php @@ -0,0 +1,88 @@ +<?php +/** + * Configuration loader for Multiple IdPs + * + * PHP version 7 + * + * @category VuFind + * @package Authentication + * @author Vaclav Rosecky <vaclav.rosecky@mzk.cz> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +namespace VuFind\Auth\Shibboleth; + +use VuFind\Exception\Auth as AuthException; + +/** + * Configuration loader for Multiple IdPs + * + * @category VuFind + * @package Authentication + * @author Vaclav Rosecky <vaclav.rosecky@mzk.cz> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +class MultiIdPConfigurationLoader implements ConfigurationLoaderInterface, + \Laminas\Log\LoggerAwareInterface +{ + use \VuFind\Log\LoggerAwareTrait; + + /** + * Configured IdPs with entityId and overridden attribute mapping + * + * @var \Laminas\Config\Config + */ + protected $config; + + /** + * Configured IdPs with entityId and overridden attribute mapping + * + * @var \Laminas\Config\Config + */ + protected $shibConfig; + + /** + * Constructor + * + * @param \Laminas\Config\Config $config Configuration + * @param \Laminas\Config\Config $shibConfig Shibboleth configuration for IdPs + */ + public function __construct(\Laminas\Config\Config $config, + \Laminas\Config\Config $shibConfig + ) { + $this->config = $config; + $this->shibConfig = $shibConfig; + } + + /** + * Return shibboleth configuration. + * + * @param string $entityId entity Id + * + * @throws \VuFind\Exception\Auth + * @return array shibboleth configuration + */ + public function getConfiguration($entityId) + { + $config = $this->config->Shibboleth->toArray(); + $idpConfig = null; + $prefix = null; + foreach ($this->shibConfig as $name => $configuration) { + if ($entityId == trim($configuration['entityId'])) { + $idpConfig = $configuration->toArray(); + $prefix = $name; + break; + } + } + if ($idpConfig == null) { + $this->debug( + "Missing configuration for Idp with entityId: {$entityId})" + ); + throw new AuthException('Missing configuration for IdP.'); + } + $config = array_merge($config, $idpConfig); + $config['prefix'] = $prefix; + return $config; + } +} diff --git a/module/VuFind/src/VuFind/Auth/Shibboleth/SingleIdPConfigurationLoader.php b/module/VuFind/src/VuFind/Auth/Shibboleth/SingleIdPConfigurationLoader.php new file mode 100644 index 0000000000000000000000000000000000000000..d736d2dd3a570fb3a40b79da6e2d92f807454b3c --- /dev/null +++ b/module/VuFind/src/VuFind/Auth/Shibboleth/SingleIdPConfigurationLoader.php @@ -0,0 +1,55 @@ +<?php +/** + * Configuration loader for single IdP + * + * PHP version 7 + * + * @category VuFind + * @package Authentication + * @author Vaclav Rosecky <vaclav.rosecky@mzk.cz> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +namespace VuFind\Auth\Shibboleth; + +/** + * Configuration loader for single IdP + * + * @category VuFind + * @package Authentication + * @author Vaclav Rosecky <vaclav.rosecky@mzk.cz> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +class SingleIdPConfigurationLoader implements ConfigurationLoaderInterface +{ + /** + * Configured IdPs with entityId and overridden attribute mapping + * + * @var \Laminas\Config\Config + */ + protected $config; + + /** + * Constructor + * + * @param \Laminas\Config\Config $config Configuration + */ + public function __construct(\Laminas\Config\Config $config) + { + $this->config = $config; + } + + /** + * Return shibboleth configuration. + * + * @param string $entityId entity Id + * + * @throws \VuFind\Exception\Auth + * @return array shibboleth configuration + */ + public function getConfiguration($entityId) + { + return $this->config->Shibboleth->toArray(); + } +} diff --git a/module/VuFind/src/VuFind/Auth/ShibbolethFactory.php b/module/VuFind/src/VuFind/Auth/ShibbolethFactory.php index 8d26bc042b0b6f486c590da91b2bbbe9ad57df2c..fa9d272f55286f5f0df3cb46dfcf93915af9cb19 100644 --- a/module/VuFind/src/VuFind/Auth/ShibbolethFactory.php +++ b/module/VuFind/src/VuFind/Auth/ShibbolethFactory.php @@ -31,6 +31,8 @@ use Interop\Container\ContainerInterface; use Interop\Container\Exception\ContainerException; use Laminas\ServiceManager\Exception\ServiceNotCreatedException; use Laminas\ServiceManager\Exception\ServiceNotFoundException; +use VuFind\Auth\Shibboleth\MultiIdPConfigurationLoader; +use VuFind\Auth\Shibboleth\SingleIdPConfigurationLoader; /** * Factory for Shibboleth authentication module. @@ -43,6 +45,8 @@ use Laminas\ServiceManager\Exception\ServiceNotFoundException; */ class ShibbolethFactory implements \Laminas\ServiceManager\Factory\FactoryInterface { + const SHIBBOLETH_CONFIG_FILE_NAME = "shibboleth"; + /** * Create an object * @@ -63,8 +67,35 @@ class ShibbolethFactory implements \Laminas\ServiceManager\Factory\FactoryInterf if (!empty($options)) { throw new \Exception('Unexpected options sent to factory.'); } + $loader = $this->getConfigurationLoader($container); + $request = $container->get('Request'); return new $requestedName( - $container->get(\Laminas\Session\SessionManager::class) + $container->get(\Laminas\Session\SessionManager::class), + $loader, $request ); } + + /** + * Return configuration loader for shibboleth + * + * @param ContainerInterface $container Service manager + * + * @return configuration loader + */ + public function getConfigurationLoader(ContainerInterface $container) + { + $config = $container->get(\VuFind\Config\PluginManager::class) + ->get('config'); + $override = $config->Shibboleth->allow_configuration_override ?? false; + $loader = null; + if ($override) { + $shibConfig = $container->get('VuFind\Config')->get( + self::SHIBBOLETH_CONFIG_FILE_NAME + ); + $loader = new MultiIdPConfigurationLoader($config, $shibConfig); + } else { + $loader = new SingleIdPConfigurationLoader($config); + } + return $loader; + } } diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Auth/ShibbolethTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Auth/ShibbolethTest.php index 7d7a7f82cceed197f2d039209c7b62ecd2870c64..86b69c4546276ebc40f5140b209b36de23b08041 100644 --- a/module/VuFind/tests/integration-tests/src/VuFindTest/Auth/ShibbolethTest.php +++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Auth/ShibbolethTest.php @@ -28,7 +28,10 @@ namespace VuFindTest\Auth; use Laminas\Config\Config; +use Laminas\Http\Headers; use VuFind\Auth\Shibboleth; +use VuFind\Auth\Shibboleth\MultiIdPConfigurationLoader; +use VuFind\Auth\Shibboleth\SingleIdPConfigurationLoader; /** * Shibboleth authentication test class. @@ -43,6 +46,35 @@ class ShibbolethTest extends \VuFindTest\Unit\DbTestCase { use \VuFindTest\Unit\UserCreationTrait; + protected $user1 = [ + 'Shib-Identity-Provider' => 'https://idp1.example.org/', + 'username' => 'testuser1', + 'userLibraryId' => 'testuser1', + 'mail' => 'testuser1@example.org', + ]; + + protected $user2 = [ + 'Shib-Identity-Provider' => 'https://idp2.example.org/', + 'eppn' => 'testuser2', + 'alephId' => '12345', + 'mail' => 'testuser2@example.org', + 'eduPersonScopedAffiliation' => 'member@example.org', + ]; + + protected $user3 = [ + 'Shib-Identity-Provider' => 'https://idp2.example.org/', + 'eppn' => 'testuser3', + 'alephId' => 'testuser3', + 'mail' => 'testuser3@example.org', + ]; + + protected $proxyUser = [ + 'Shib-Identity-Provider' => 'https://idp1.example.org/', + 'username' => 'testuser3', + 'userLibraryId' => 'testuser3', + 'mail' => 'testuser3@example.org', + ]; + /** * Standard setup method. * @@ -71,15 +103,25 @@ class ShibbolethTest extends \VuFindTest\Unit\DbTestCase * Get an authentication object. * * @param Config $config Configuration to use (null for default) + * @param Config $shibConfig Configuration with IdP + * @param boolean $useHeaders use HTTP headers instead of environment variables + * @param boolean $requiredAttributes required attributes * - * @return LDAP + * @return Shibboleth */ - public function getAuthObject($config = null) + public function getAuthObject($config = null, $shibConfig = null, $useHeaders = false, $requiredAttributes = true) { if (null === $config) { - $config = $this->getAuthConfig(); + $config = $this->getAuthConfig($useHeaders, $requiredAttributes); } - $obj = new Shibboleth($this->createMock(\Laminas\Session\ManagerInterface::class)); + $loader = null; + if ($shibConfig == null) { + $loader = new SingleIdPConfigurationLoader($config); + } else { + $loader = new MultiIdPConfigurationLoader($config, $shibConfig); + } + $obj = new Shibboleth($this->createMock(\Laminas\Session\ManagerInterface::class), $loader, + $this->createMock(\Laminas\Http\PhpEnvironment\Request::class)); $initializer = new \VuFind\ServiceManager\ServiceInitializer(); $initializer($this->getServiceManager(), $obj); $obj->setConfig($config); @@ -87,22 +129,58 @@ class ShibbolethTest extends \VuFindTest\Unit\DbTestCase } /** - * Get a working configuration for the LDAP object + * Get a working configuration for the Shibboleth object * * @return Config */ - public function getAuthConfig() + public function getAuthConfig($useHeaders = false, $requiredAttributes = true) { - $ldapConfig = new Config( + $config = [ + 'login' => 'http://myserver', + 'username' => 'username', + 'email' => 'email', + 'use_headers' => $useHeaders + ]; + if ($requiredAttributes) { + $config += [ + 'userattribute_1' => 'password', + 'userattribute_value_1' => 'testpass', + ]; + } + $shibConfig = new Config($config, true); + return new Config(['Shibboleth' => $shibConfig], true); + } + + /** + * Get a working configuration for the Shibboleth object + * + * @return Config + */ + public function getShibbolethConfig() + { + $example1 = new Config( [ - 'login' => 'http://myserver', + 'entityId' => 'https://idp1.example.org/', 'username' => 'username', 'email' => 'email', - 'userattribute_1' => 'password', - 'userattribute_value_1' => 'testpass' + 'cat_username' => 'userLibraryId', ], true ); - return new Config(['Shibboleth' => $ldapConfig], true); + $example2 = new Config( + [ + 'entityId' => 'https://idp2.example.org/', + 'username' => 'eppn', + 'email' => 'email', + 'cat_username' => 'alephId', + 'userattribute_1' => 'eduPersonScopedAffiliation', + 'userattribute_value_1' => 'member@example.org', + ], true + ); + $config = [ + 'example1' => $example1, + 'example2' => $example2, + ]; + return new Config($config, true); } /** @@ -119,18 +197,25 @@ class ShibbolethTest extends \VuFindTest\Unit\DbTestCase * Support method -- get parameters to log into an account (but allow override of * individual parameters so we can test different scenarios). * - * @param array $overrides Associative array of parameters to override. + * @param array $overrides Associative array of parameters to override. + * @param boolean $useHeaders Use headers instead of environment variables * * @return \Laminas\Http\Request */ - protected function getLoginRequest($overrides = []) + protected function getLoginRequest($overrides = [], $useHeaders = false) { $server = $overrides + [ 'username' => 'testuser', 'email' => 'user@test.com', 'password' => 'testpass' ]; $request = new \Laminas\Http\PhpEnvironment\Request(); - $request->setServer(new \Laminas\Stdlib\Parameters($server)); + if ($useHeaders) { + $headers = new Headers(); + $headers->addHeaders($server); + $request->setHeaders($headers); + } else { + $request->setServer(new \Laminas\Stdlib\Parameters($server)); + } return $request; } @@ -227,6 +312,57 @@ class ShibbolethTest extends \VuFindTest\Unit\DbTestCase $this->assertEquals('user@test.com', $user->email); } + /** + * Test successful login. + * + * @return void + */ + public function testLogin1() + { + $user = $this->getAuthObject(null, $this->getShibbolethConfig()) + ->authenticate($this->getLoginRequest($this->user1, false)); + $this->assertEquals($user->cat_username, 'example1.testuser1'); + $this->assertEquals($user->username, 'testuser1'); + } + + /** + * Test successful login. + * + * @return void + */ + public function testLogin2() + { + $user = $this->getAuthObject(null, $this->getShibbolethConfig()) + ->authenticate($this->getLoginRequest($this->user2, false)); + $this->assertEquals($user->cat_username, 'example2.12345'); + $this->assertEquals($user->username, 'testuser2'); + } + + /** + * Test failed login. + * + * @return void + */ + public function testFailedLogin() + { + $this->expectException(\VuFind\Exception\Auth::class); + $user = $this->getAuthObject(null, $this->getShibbolethConfig()) + ->authenticate($this->getLoginRequest($this->user3, false)); + } + + /** + * Test login using attributes passed in headers. + * + * @return void + */ + public function testProxyLogin() + { + $user = $this->getAuthObject(null, $this->getShibbolethConfig(), true, false) + ->authenticate($this->getLoginRequest($this->proxyUser, true)); + $this->assertEquals($user->cat_username, 'example1.testuser3'); + $this->assertEquals($user->username, 'testuser3'); + } + /** * Standard teardown method. * @@ -234,6 +370,6 @@ class ShibbolethTest extends \VuFindTest\Unit\DbTestCase */ public static function tearDownAfterClass(): void { - static::removeUsers('testuser'); + static::removeUsers(['testuser', 'testuser1', 'testuser2', 'testuser3']); } }