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