diff --git a/lib/SimpleSAML/Configuration.php b/lib/SimpleSAML/Configuration.php
index dbc37ebc239ea7d1a044389b3ff30ddc954f8108..25627f565133a43b6541fa5c9062f8c50c35af71 100644
--- a/lib/SimpleSAML/Configuration.php
+++ b/lib/SimpleSAML/Configuration.php
@@ -608,11 +608,12 @@ class Configuration implements Utils\ClearableState
             $path = $this->configuration[$name];
         }
 
+        $path = $this->resolvePath($path);
         if ($path === null) {
             return null;
         }
 
-        return $this->resolvePath($path).'/';
+        return $path.'/';
     }
 
 
diff --git a/lib/SimpleSAML/Database.php b/lib/SimpleSAML/Database.php
index 685fe568f7472f898688aa67e3274e4b5635f818..8a1b67fe2b8e389266cd83e54628156b6b43313c 100644
--- a/lib/SimpleSAML/Database.php
+++ b/lib/SimpleSAML/Database.php
@@ -195,7 +195,7 @@ class Database
      * @param array  $params Parameters
      *
      * @throws \Exception If an error happens while trying to execute the query.
-     * @return bool|\PDOStatement object
+     * @return \PDOStatement object
      */
     private function query($db, $stmt, $params)
     {
diff --git a/lib/SimpleSAML/IdP.php b/lib/SimpleSAML/IdP.php
index 161ec110f9c4001d3c5dfe5231f000faa068b8e1..d9c895ad8acf9d0cc793e28086225e8d6ba13377 100644
--- a/lib/SimpleSAML/IdP.php
+++ b/lib/SimpleSAML/IdP.php
@@ -40,9 +40,9 @@ class IdP
      * We use this to support cross-protocol logout until
      * we implement a cross-protocol IdP.
      *
-     * @var string|null
+     * @var string
      */
-    private $associationGroup = null;
+    private $associationGroup;
 
     /**
      * The configuration for this IdP.
@@ -70,6 +70,7 @@ class IdP
         assert(is_string($id));
 
         $this->id = $id;
+        $this->associationGroup = $id;
 
         $metadata = MetaDataStorageHandler::getMetadataHandler();
         $globalConfig = Configuration::getInstance();
@@ -98,11 +99,7 @@ class IdP
                 // probably no SAML 2 IdP configured for this host. Ignore the error
             }
         } else {
-            assert(false);
-        }
-
-        if ($this->associationGroup === null) {
-            $this->associationGroup = $this->id;
+            throw new \Exception("Protocol not implemented.");
         }
 
         $auth = $this->config->getString('auth');
@@ -435,7 +432,6 @@ class IdP
      * Find the logout handler of this IdP.
      *
      * @return IdP\LogoutHandlerInterface The logout handler class.
-     *
      * @throws Exception If we cannot find a logout handler.
      */
     public function getLogoutHandler()
@@ -453,6 +449,7 @@ class IdP
                 throw new Error\Exception('Unknown logout handler: '.var_export($logouttype, true));
         }
 
+        /** @var IdP\LogoutHandlerInterface */
         return new $handler($this);
     }
 
@@ -505,8 +502,10 @@ class IdP
 
         $this->authSource->logout($returnTo);
 
-        $handler = $this->getLogoutHandler();
-        $handler->startLogout($state, $assocId);
+        if ($assocId !== null) {
+            $handler = $this->getLogoutHandler();
+            $handler->startLogout($state, $assocId);
+        }
         assert(false);
     }
 
@@ -526,8 +525,11 @@ class IdP
         assert(is_string($assocId));
         assert(is_string($relayState) || $relayState === null);
 
+        $index = strpos($assocId, ':');
+        assert(is_int($index));
+
         $session = Session::getSessionFromRequest();
-        $session->deleteData('core:idp-ssotime', $this->id.';'.substr($assocId, strpos($assocId, ':') + 1));
+        $session->deleteData('core:idp-ssotime', $this->id.';'.substr($assocId, $index + 1));
 
         $handler = $this->getLogoutHandler();
         $handler->onResponse($assocId, $relayState, $error);
diff --git a/lib/SimpleSAML/Logger.php b/lib/SimpleSAML/Logger.php
index ab19465d6432728b1a556037c9b4ead9fb1b9fe5..1bdc018740896e44faa3a0d77d9b14cfa9d087a1 100644
--- a/lib/SimpleSAML/Logger.php
+++ b/lib/SimpleSAML/Logger.php
@@ -13,9 +13,14 @@ namespace SimpleSAML;
 class Logger
 {
     /**
-     * @var \SimpleSAML\Logger\LoggingHandlerInterface|false|null
+     * @var \SimpleSAML\Logger\LoggingHandlerInterface
      */
-    private static $loggingHandler = null;
+    private static $loggingHandler;
+
+    /**
+     * @var bool
+     */
+    private static $initializing = false;
 
     /**
      * @var integer|null
@@ -402,8 +407,7 @@ class Logger
      */
     private static function createLoggingHandler($handler = null)
     {
-        // set to false to indicate that it is being initialized
-        self::$loggingHandler = false;
+        self::$initializing = true;
 
         // a set of known logging handlers
         $known_handlers = [
@@ -438,12 +442,17 @@ class Logger
             }
             $handler = $known_handlers[$handler];
         }
+
+        /** @var \SimpleSAML\Logger\LoggingHandlerInterface */
         self::$loggingHandler = new $handler($config);
 
         self::$format = $config->getString('logging.format', self::$format);
         self::$loggingHandler->setLogFormat(self::$format);
+
+        self::$initializing = false;
     }
 
+
     /**
      * @param int $level
      * @param string $string
@@ -452,20 +461,20 @@ class Logger
      */
     private static function log($level, $string, $statsLog = false)
     {
-        if (self::$loggingHandler === false) {
+        if (self::$initializing) {
             // some error occurred while initializing logging
             self::defer($level, $string, $statsLog);
             return;
         } elseif (php_sapi_name() === 'cli' || defined('STDIN')) {
             // we are being executed from the CLI, nowhere to log
-            if (is_null(self::$loggingHandler)) {
-                self::createLoggingHandler('SimpleSAML\Logger\StandardErrorLoggingHandler');
+            if (!isset(self::$loggingHandler)) {
+                self::createLoggingHandler(\SimpleSAML\Logger\StandardErrorLoggingHandler::class);
             }
             $_SERVER['REMOTE_ADDR'] = "CLI";
             if (self::$trackid === self::NO_TRACKID) {
                 self::$trackid = 'CL'.bin2hex(openssl_random_pseudo_bytes(4));
             }
-        } elseif (self::$loggingHandler === null) {
+        } elseif (!isset(self::$loggingHandler)) {
             // Initialize logging
             self::createLoggingHandler();
             self::flush();
diff --git a/lib/SimpleSAML/Memcache.php b/lib/SimpleSAML/Memcache.php
index 904649775459233862a753dd2c84bc10d630e893..5f6577ef578b1ef22f218898be023e8eb5e85f2a 100644
--- a/lib/SimpleSAML/Memcache.php
+++ b/lib/SimpleSAML/Memcache.php
@@ -26,7 +26,7 @@ class Memcache
     /**
      * Cache of the memcache servers we are using.
      *
-     * @var \Memcache[]|null
+     * @var \Memcache[]|\Memcached[]|null
      */
     private static $serverGroups = null;
 
@@ -167,7 +167,7 @@ class Memcache
 
         // store this object to all groups of memcache servers
         foreach (self::getMemcacheServers() as $server) {
-            if (self::$extension === '\memcached') {
+            if (self::$extension === \Memcached::class) {
                 $server->set($key, $savedInfoSerialized, $expire);
             } else {
                 $server->set($key, $savedInfoSerialized, 0, $expire);
@@ -212,7 +212,7 @@ class Memcache
      *    The timeout for contacting this server, in seconds.
      *    The default value is 3 seconds.
      *
-     * @param \Memcache $memcache The Memcache object we should add this server to.
+     * @param \Memcache|\Memcached $memcache The Memcache object we should add this server to.
      * @param array    $server An associative array with the configuration options for the server to add.
      * @return void
      *
@@ -290,7 +290,7 @@ class Memcache
         }
 
         // add this server to the Memcache object
-        if (self::$extension === '\memcached') {
+        if ($memcache instanceof \Memcached) {
             $memcache->addServer($hostname, $port);
         } else {
             $memcache->addServer($hostname, $port, true, $weight, $timeout, $timeout, true);
@@ -304,24 +304,27 @@ class Memcache
      *
      * @param array $group Array of servers which should be created as a group.
      *
-     * @return \Memcache A Memcache object of the servers in the group
+     * @return \Memcache|\Memcached A Memcache object of the servers in the group
      *
      * @throws \Exception If the servers configuration is invalid.
      */
     private static function loadMemcacheServerGroup(array $group)
     {
-        $class = class_exists('\Memcache') ? '\Memcache' : (class_exists('\Memcached') ? '\Memcached' : false);
-        if (!$class) {
+        if (class_exists(\Memcached::class)) {
+            $memcache = new \Memcached();
+            self::$extension = \Memcached::class;
+        } elseif (class_exists(\Memcache::class)) {
+            $memcache = new \Memcache();
+            self::$extension = \Memcache::class;
+        } else {
             throw new \Exception(
                 'Missing Memcached implementation. You must install either the Memcache or Memcached extension.'
             );
-        } elseif (strtolower($class) === '\memcache') {
-            Logger::warning("The use of PHP-extension memcache is deprecated. Please migrate to the memcached extension.");
         }
-        self::$extension = strtolower($class);
 
-        // create the \Memcache object
-        $memcache = new $class();
+        if (self::$extension === '\memcache') {
+            Logger::warning("The use of PHP-extension memcache is deprecated. Please migrate to the memcached extension.");
+        }
 
         // iterate over all the servers in the group and add them to the Memcache object
         foreach ($group as $index => $server) {
@@ -346,6 +349,7 @@ class Memcache
             self::addMemcacheServer($memcache, $server);
         }
 
+        /** @var \Memcache|\Memcached */
         return $memcache;
     }
 
@@ -354,7 +358,7 @@ class Memcache
      * This function gets a list of all configured memcache servers. This list is initialized based
      * on the content of 'memcache_store.servers' in the configuration.
      *
-     * @return \Memcache[] Array with Memcache objects.
+     * @return \Memcache[]|\Memcached[] Array with Memcache objects.
      *
      * @throws \Exception If the servers configuration is invalid.
      */
diff --git a/lib/SimpleSAML/Session.php b/lib/SimpleSAML/Session.php
index 164f2c5eab1c1c348a51d363e6ea4b4796c9a988..c4a32d6aa1a158b228d46a45774422afe0aeae12 100644
--- a/lib/SimpleSAML/Session.php
+++ b/lib/SimpleSAML/Session.php
@@ -32,7 +32,6 @@ class Session implements \Serializable, Utils\ClearableState
      */
     const DATA_TIMEOUT_SESSION_END = 'sessionEndTimeout';
 
-
     /**
      * The list of loaded session objects.
      *
@@ -42,13 +41,12 @@ class Session implements \Serializable, Utils\ClearableState
      */
     private static $sessions = [];
 
-
     /**
      * This variable holds the instance of the session - Singleton approach.
      *
      * Warning: do not set the instance manually, call Session::load() instead.
      */
-    private static $instance = null;
+    private static $instance;
 
     /**
      * The global configuration.
@@ -60,7 +58,7 @@ class Session implements \Serializable, Utils\ClearableState
     /**
      * The session ID of this session.
      *
-     * @var string
+     * @var string|null
      */
     private $sessionId;
 
@@ -163,7 +161,6 @@ class Session implements \Serializable, Utils\ClearableState
             $this->trackid = 'TR'.bin2hex(openssl_random_pseudo_bytes(4));
             Logger::setTrackId($this->trackid);
             $this->transient = true;
-
         } else {
             // regular session
             $sh = SessionHandler::getSessionHandler();
@@ -312,6 +309,7 @@ class Session implements \Serializable, Utils\ClearableState
         }
 
         // we must have a session now, either regular or transient
+        /** @var \SimpleSAML\Session */
         return self::$instance;
     }
 
@@ -523,7 +521,7 @@ class Session implements \Serializable, Utils\ClearableState
     /**
      * Retrieve the session ID of this session.
      *
-     * @return string  The session ID.
+     * @return string|null  The session ID, or null if this is a transient session.
      */
     public function getSessionId()
     {
diff --git a/lib/SimpleSAML/SessionHandler.php b/lib/SimpleSAML/SessionHandler.php
index 90cc5536961b4f49a38dc20b3c283a0c4adc52d7..91fc08b26df9f640200665288d395aa25a870a8a 100644
--- a/lib/SimpleSAML/SessionHandler.php
+++ b/lib/SimpleSAML/SessionHandler.php
@@ -21,9 +21,9 @@ abstract class SessionHandler
      * instance of the session handler. This variable will be NULL if
      * we haven't instantiated a session handler yet.
      *
-     * @var \SimpleSAML\SessionHandler|null
+     * @var \SimpleSAML\SessionHandler
      */
-    protected static $sessionHandler = null;
+    protected static $sessionHandler;
 
 
     /**
diff --git a/lib/SimpleSAML/SessionHandlerPHP.php b/lib/SimpleSAML/SessionHandlerPHP.php
index f76433f077fc89563fd7018a8eb1eb501b1d83e0..280584f70210f32fb873c3aa58b57c3ccff145de 100644
--- a/lib/SimpleSAML/SessionHandlerPHP.php
+++ b/lib/SimpleSAML/SessionHandlerPHP.php
@@ -77,6 +77,7 @@ class SessionHandlerPHP extends SessionHandler
 
         if (!headers_sent()) {
             if (version_compare(PHP_VERSION, '7.3.0', '>=')) {
+                /** @psalm-suppress InvalidArgument  This annotation may be removed in Psalm >=3.0.15 */
                 session_set_cookie_params([
                     'lifetime' => $params['lifetime'],
                     'path' => $params['path'],
@@ -168,6 +169,10 @@ class SessionHandlerPHP extends SessionHandler
             if (($sid_length * $sid_bits_per_char) < 128) {
                 Logger::warning("Unsafe defaults used for sessionId generation!");
             }
+            /**
+             * This annotation may be removed as soon as we start using vimeo/psalm 3.x
+             * @psalm-suppress TooFewArguments
+             */
             $sessionId = session_create_id();
         } else {
             $sessionId = bin2hex(openssl_random_pseudo_bytes(16));
@@ -358,6 +363,7 @@ class SessionHandlerPHP extends SessionHandler
         }
 
         if (version_compare(PHP_VERSION, '7.3.0', '>=')) {
+            /** @psalm-suppress InvalidArgument  This annotation may be removed in Psalm >=3.0.15 */
             session_set_cookie_params($cookieParams);
         } else {
             session_set_cookie_params(
@@ -369,7 +375,7 @@ class SessionHandlerPHP extends SessionHandler
             );
         }
 
-        session_id($sessionID);
+        session_id(strval($sessionID));
         @session_start();
     }
 }
diff --git a/lib/SimpleSAML/SessionHandlerStore.php b/lib/SimpleSAML/SessionHandlerStore.php
index 669a4c58f0b3db122613aa8720cce98481912021..89c7c16c45184214ffaf2051b72a969588b162fe 100644
--- a/lib/SimpleSAML/SessionHandlerStore.php
+++ b/lib/SimpleSAML/SessionHandlerStore.php
@@ -69,6 +69,12 @@ class SessionHandlerStore extends SessionHandlerCookie
      */
     public function saveSession(Session $session)
     {
+        if ($session->isTransient()) {
+            // transient session, nothing to save
+            return;
+        }
+
+        /** @var string $sessionId */
         $sessionId = $session->getSessionId();
 
         $config = Configuration::getInstance();
diff --git a/lib/SimpleSAML/Store.php b/lib/SimpleSAML/Store.php
index c2c4fceddad748f53535e609cbf50cc59d0c367b..50d5b3162200812474a28dab988d95389658fb8c 100644
--- a/lib/SimpleSAML/Store.php
+++ b/lib/SimpleSAML/Store.php
@@ -16,7 +16,7 @@ abstract class Store implements Utils\ClearableState
      *
      * This is false if the data store isn't enabled, and null if we haven't attempted to initialize it.
      *
-     * @var \SimpleSAML\Store|false
+     * @var \SimpleSAML\Store|false|null
      */
     private static $instance;
 
@@ -64,6 +64,7 @@ abstract class Store implements Utils\ClearableState
                         $c
                     );
                 }
+                /** @var \SimpleSAML\Store|false */
                 self::$instance = new $className();
         }
 
diff --git a/lib/SimpleSAML/Utilities.php b/lib/SimpleSAML/Utilities.php
index 33392a458a08e53577860092c8c1d3e53ebb23aa..3ea08caa743099613951ffc15b783f7b99893527 100644
--- a/lib/SimpleSAML/Utilities.php
+++ b/lib/SimpleSAML/Utilities.php
@@ -2,6 +2,8 @@
 
 namespace SimpleSAML;
 
+use SimpleSAML\Error\Error;
+
 /**
  * Misc static functions that is used several places.in example parsing and id generation.
  *
@@ -674,6 +676,7 @@ class Utilities
      * @param string $destination
      * @param array $post
      * @return string
+     * @throws Error If the current session is a transient session.
      */
     public static function createHttpPostRedirectLink($destination, $post)
     {
@@ -687,6 +690,10 @@ class Utilities
         ];
 
         $session = \SimpleSAML\Session::getSessionFromRequest();
+        if ($session->isTransient()) {
+            throw new Error('Cannot save data to a transient session');
+        }
+
         $session->setData('core_postdatalink', $postId, $postData);
 
         $redirInfo = base64_encode(\SimpleSAML\Utils\Crypto::aesEncrypt($session->getSessionId().':'.$postId));
diff --git a/lib/SimpleSAML/Utils/HTTP.php b/lib/SimpleSAML/Utils/HTTP.php
index 2ddc64ac584ea03002ea2a355880a06fee6fd9db..2fc9f2226f8793ae7c1cc64f5265e4fa67689570 100644
--- a/lib/SimpleSAML/Utils/HTTP.php
+++ b/lib/SimpleSAML/Utils/HTTP.php
@@ -32,13 +32,14 @@ class HTTP
         $session = Session::getSessionFromRequest();
         $id = self::savePOSTData($session, $destination, $data);
 
-        // get the session ID
-        $session_id = $session->getSessionId();
-        if (is_null($session_id)) {
+        if ($session->isTransient()) {
             // this is a transient session, it is pointless to continue
             throw new Error\Exception('Cannot save POST data to a transient session.');
         }
 
+        /** @var string $session_id */
+        $session_id = $session->getSessionId();
+
         // encrypt the session ID and the random ID
         $info = base64_encode(Crypto::aesEncrypt($session_id.':'.$id));
 
diff --git a/modules/saml/lib/SP/LogoutStore.php b/modules/saml/lib/SP/LogoutStore.php
index 6142654ac02c3c8f91eb0913f7d1c68eff1b4ad5..b4f04a7bf1237dabad7a2120e3b4aa4c1d4a2245 100644
--- a/modules/saml/lib/SP/LogoutStore.php
+++ b/modules/saml/lib/SP/LogoutStore.php
@@ -318,6 +318,12 @@ class LogoutStore
         assert(is_string($sessionIndex) || $sessionIndex === null);
         assert(is_int($expire));
 
+        $session = Session::getSessionFromRequest();
+        if ($session->isTransient()) {
+            // transient sessions are useless for this purpose, nothing to do
+            return;
+        }
+
         if ($sessionIndex === null) {
             /* This IdP apparently did not include a SessionIndex, and thus probably does not
              * support SLO. We still want to add the session to the data store just in case
@@ -346,7 +352,7 @@ class LogoutStore
             $sessionIndex = sha1($sessionIndex);
         }
 
-        $session = Session::getSessionFromRequest();
+        /** @var string $sessionId */
         $sessionId = $session->getSessionId();
 
         if ($store instanceof Store\SQL) {
diff --git a/psalm.xml b/psalm.xml
index da1d4dbaef71e6b2ce742152994351607b4d9ba2..0c1de8a54e3cf09ac4974c7fabf7873cef904922 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -8,20 +8,7 @@
     allowStringToStandInForClass="true"
 >
     <projectFiles>
-        <directory name="lib/SimpleSAML/Auth" />
-        <directory name="lib/SimpleSAML/Bindings" />
-        <directory name="lib/SimpleSAML/Error" />
-        <directory name="lib/SimpleSAML/HTTP" />
-        <directory name="lib/SimpleSAML/IdP" />
-        <directory name="lib/SimpleSAML/Locale" />
-        <directory name="lib/SimpleSAML/Logger" />
-        <directory name="lib/SimpleSAML/Metadata" />
-        <directory name="lib/SimpleSAML/Module" />
-        <directory name="lib/SimpleSAML/Stats" />
-        <directory name="lib/SimpleSAML/Store" />
-        <directory name="lib/SimpleSAML/Utils" />
-        <directory name="lib/SimpleSAML/XHTML" />
-        <directory name="lib/SimpleSAML/XML" />
+        <directory name="lib/SimpleSAML" />
 
         <!-- Replaces all modules/... with this one-liner for 2.0
         <directory name="modules" />
@@ -87,6 +74,7 @@
         <UnresolvableInclude>
             <errorLevel type="suppress">
                 <file name="bin/*.php" />
+                <file name="lib/SimpleSAML/Module.php" />
                 <file name="lib/SimpleSAML/XHTML/Template.php" />
                 <file name="modules/*/bin/*.php" />
                 <file name="tests/bootstrap.php" />