diff --git a/modules/saml/lib/SP/LogoutStore.php b/modules/saml/lib/SP/LogoutStore.php new file mode 100644 index 0000000000000000000000000000000000000000..787e563f68ca8c9a32c3ee052c045a736b949070 --- /dev/null +++ b/modules/saml/lib/SP/LogoutStore.php @@ -0,0 +1,264 @@ +<?php + +/** + * A directory over logout information. + * + * @package simpleSAMLphp + * @version $Id$ + */ +class sspmod_saml_SP_LogoutStore { + + /** + * Create logout table in SQL, if it is missing. + * + * @param SimpleSAML_Store_SQL $store The datastore. + */ + private static function createLogoutTable(SimpleSAML_Store_SQL $store) { + + if ($store->getTableVersion('saml_LogoutStore') === 1) { + return; + } + + $query = 'CREATE TABLE ' . $store->prefix . '_saml_LogoutStore ( + _authSource VARCHAR(30) NOT NULL, + _nameId VARCHAR(40) NOT NULL, + _sessionIndex VARCHAR(50) NOT NULL, + _expire TIMESTAMP NOT NULL, + _sessionId VARCHAR(50) NOT NULL, + UNIQUE (_authSource, _nameID, _sessionIndex) + )'; + $store->pdo->exec($query); + + $query = 'CREATE INDEX ' . $store->prefix . '_saml_LogoutStore_expire ON ' . $store->prefix . '_saml_LogoutStore (_expire)'; + $store->pdo->exec($query); + + $query = 'CREATE INDEX ' . $store->prefix . '_saml_LogoutStore_nameId ON ' . $store->prefix . '_saml_LogoutStore (_authSource, _nameId)'; + $store->pdo->exec($query); + + $store->setTableVersion('saml_LogoutStore', 1); + } + + + /** + * Clean the logout table of expired entries. + * + * @param SimpleSAML_Store_SQL $store The datastore. + */ + private static function cleanLogoutStore(SimpleSAML_Store_SQL $store) { + + SimpleSAML_Logger::debug('saml.LogoutStore: Cleaning logout store.'); + + $query = 'DELETE FROM ' . $store->prefix . '_saml_LogoutStore WHERE _expire < :now'; + $params = array('now' => gmdate('Y-m-d H:i:s')); + + $query = $store->pdo->prepare($query); + $query->execute($params); + } + + + /** + * Register a session in the SQL datastore. + * + * @param SimpleSAML_Store_SQL $store The datastore. + * @param string $authId The authsource ID. + * @param string $nameId The hash of the users NameID. + * @param string $sessionIndex The SessionIndex of the user. + */ + private static function addSessionSQL(SimpleSAML_Store_SQL $store, $authId, $nameId, $sessionIndex, $expire, $sessionId) { + assert('is_string($authId)'); + assert('is_string($nameId)'); + assert('is_string($sessionIndex)'); + assert('is_string($sessionId)'); + assert('is_int($expire)'); + + self::createLogoutTable($store); + + if (rand(0, 1000) < 10) { + self::cleanLogoutStore($store); + } + + $data = array( + '_authSource' => $authId, + '_nameId' => $nameId, + '_sessionIndex' => $sessionIndex, + '_expire' => gmdate('Y-m-d H:i:s', $expire), + '_sessionId' => $sessionId, + ); + $store->insertOrUpdate($store->prefix . '_saml_LogoutStore', array('_authSource', '_nameId', '_sessionIndex'), $data); + } + + + /** + * Retrieve sessions from the SQL datastore. + * + * @param SimpleSAML_Store_SQL $store The datastore. + * @param string $authId The authsource ID. + * @param string $nameId The hash of the users NameID. + * @return array Associative array of SessionIndex => SessionId. + */ + private static function getSessionsSQL(SimpleSAML_Store_SQL $store, $authId, $nameId) { + assert('is_string($authId)'); + assert('is_string($nameId)'); + + self::createLogoutTable($store); + + $params = array( + '_authSource' => $authId, + '_nameId' => $nameId, + 'now' => gmdate('Y-m-d H:i:s'), + ); + + $query = 'SELECT _sessionIndex, _sessionId FROM ' . $store->prefix . '_saml_LogoutStore' . + ' WHERE _authSource = :_authSource AND _nameId = :_nameId AND _expire >= :now'; + $query = $store->pdo->prepare($query); + $query->execute($params); + + $res = array(); + while ( ($row = $query->fetch(PDO::FETCH_ASSOC)) !== FALSE) { + $res[$row['_sessionIndex']] = $row['_sessionId']; + } + + return $res; + } + + + /** + * Retrieve all session IDs from a key-value store. + * + * @param SimpleSAML_Store_SQL $store The datastore. + * @param string $authId The authsource ID. + * @param string $nameId The hash of the users NameID. + * @param array $sessionIndexes The session indexes. + * @return array Associative array of SessionIndex => SessionId. + */ + private static function getSessionsStore(SimpleSAML_Store $store, $authId, $nameId, array $sessionIndexes) { + assert('is_string($authId)'); + assert('is_string($nameId)'); + + $res = array(); + foreach ($sessionIndexes as $sessionIndex) { + $sessionId = $store->get('saml.LogoutStore', $strNameId . ':' . $sessionIndex); + if ($sessionId === NULL) { + continue; + } + assert('is_string($sessionId)'); + $res[$sessionIndex] = $sessionId; + } + + return $res; + } + + + /** + * Register a new session in the datastore. + * + * @param string $authId The authsource ID. + * @param array $nameId The NameID of the user. + * @param string|NULL $sessionIndex The SessionIndex of the user. + */ + public static function addSession($authId, array $nameId, $sessionIndex, $expire) { + assert('is_string($authId)'); + assert('is_string($sessionIndex)'); + assert('is_int($expire)'); + + $store = SimpleSAML_Store::getInstance(); + if ($store === FALSE) { + /* We don't have a datastore. */ + return; + } + + /* Normalize NameID. */ + ksort($nameId); + $strNameId = serialize($nameId); + $strNameId = sha1($strNameId); + + /* Normalize SessionIndex. */ + if (strlen($sessionIndex) > 50) { + $sessionIndex = sha1($sessionIndex); + } + + $session = SimpleSAML_Session::getInstance(); + $sessionId = $session->getSessionId(); + + if ($store instanceof SimpleSAML_Store_SQL) { + self::addSessionSQL($store, $authId, $strNameId, $sessionIndex, $expire, $sessionId); + } else { + $store->set('saml.LogoutStore', $strNameId . ':' . $sessionIndex, $sessionId, $expire); + } + } + + + /** + * Log out of the given sessions. + * + * @param string $authId The authsource ID. + * @param array $nameId The NameID of the user. + * @param array $sessionIndexes The SessionIndexes we should log out of. Logs out of all if this is empty. + * @returns bool TRUE if OK, FALSE if not supported. + */ + public static function logoutSessions($authId, array $nameId, array $sessionIndexes) { + assert('is_string($authId)'); + + $store = SimpleSAML_Store::getInstance(); + if ($store === FALSE) { + /* We don't have a datastore. */ + return FALSE; + } + + /* Normalize NameID. */ + ksort($nameId); + $strNameId = serialize($nameId); + $strNameId = sha1($strNameId); + + /* Normalize SessionIndexes. */ + foreach ($sessionIndexes as &$sessionIndex) { + assert('is_string($sessionIndex)'); + if (strlen($sessionIndex) > 50) { + $sessionIndex = sha1($sessionIndex); + } + } + unset($sessionIndex); // Remove reference + + if ($store instanceof SimpleSAML_Store_SQL) { + $sessions = self::getSessionsSQL($store, $authId, $strNameId); + } elseif (empty($sessionIndexes)) { + /* We cannot fetch all sessions without a SQL store. */ + return FALSE; + } else { + $sessions = self::getSessionsStore($store, $authId, $strNameId, $sessionIndexes); + + } + + if (empty($sessionIndexes)) { + $sessionIndexes = array_keys($sessions); + } + + $sessionHandler = SimpleSAML_SessionHandler::getSessionHandler(); + + foreach ($sessionIndexes as $sessionIndex) { + if (!isset($sessions[$sessionIndex])) { + SimpleSAML_Logger::info('saml.LogoutStore: Logout requested for unknown SessionIndex.'); + continue; + } + + $sessionId = $sessions[$sessionIndex]; + + $session = SimpleSAML_Session::getSession($sessionId); + if ($session === NULL) { + SimpleSAML_Logger::info('saml.LogoutStore: Skipping logout of missing session.'); + continue; + } + + if (!$session->isValid($authId)) { + SimpleSAML_Logger::info('saml.LogoutStore: Skipping logout of session because it isn\'t authenticated.'); + continue; + } + + SimpleSAML_Logger::info('saml.LogoutStore: Logging out of session with trackId [' . $session->getTrackId() . '].'); + $session->doLogout(); + } + + return TRUE; + } + +} diff --git a/modules/saml/www/sp/saml2-acs.php b/modules/saml/www/sp/saml2-acs.php index fcbe7eda9b6118b74367084ef03ee0e8be9aabae..2bfcc89af02509ca0b2d17e6d2984697d6490636 100644 --- a/modules/saml/www/sp/saml2-acs.php +++ b/modules/saml/www/sp/saml2-acs.php @@ -57,6 +57,15 @@ try { $nameId = $assertion->getNameId(); $sessionIndex = $assertion->getSessionIndex(); +$expire = $assertion->getSessionNotOnOrAfter(); +if ($expire === NULL) { + /* Just expire the logout associtaion 24 hours into the future. */ + $expire = time() + 24*60*60; +} + +/* Register this session in the logout store. */ +sspmod_saml_SP_LogoutStore::addSession($sourceId, $nameId, $sessionIndex, $expire); + /* We need to save the NameID and SessionIndex for logout. */ $logoutState = array( 'saml:logout:Type' => 'saml2', diff --git a/modules/saml/www/sp/saml2-logout.php b/modules/saml/www/sp/saml2-logout.php index e1777157cc0363b70a990fc0df09d9e188ffb7a3..5f968a0aacec300af7de7982f99689d11f493df6 100644 --- a/modules/saml/www/sp/saml2-logout.php +++ b/modules/saml/www/sp/saml2-logout.php @@ -57,8 +57,13 @@ if ($message instanceof SAML2_LogoutResponse) { SimpleSAML_Logger::debug('module/saml2/sp/logout: Request from ' . $idpEntityId); SimpleSAML_Logger::stats('saml20-idp-SLO idpinit ' . $spEntityId . ' ' . $idpEntityId); - /* Notify source of logout, so that it may call logout callbacks. */ - $source->handleLogout($idpEntityId); + $nameId = $message->getNameId(); + $sessionIndexes = $message->getSessionIndexes(); + + if (!sspmod_saml_SP_LogoutStore::logoutSessions($sourceId, $nameId, $sessionIndexes)) { + /* This type of logout was unsupported. Use the old method. */ + $source->handleLogout($idpEntityId); + } /* Create an send response. */ $lr = sspmod_saml_Message::buildLogoutResponse($spMetadata, $idpMetadata);