diff --git a/dictionaries/logout.definition.json b/dictionaries/logout.definition.json index c2cdeced45861633846ea1e588535e49862d316e..5fa28a3d4a35c0726e10a1bfe229f68cd102a2d3 100644 --- a/dictionaries/logout.definition.json +++ b/dictionaries/logout.definition.json @@ -79,5 +79,11 @@ }, "no": { "en": "No" + }, + "logging_out_from": { + "en": "Logging out of the following services:" + }, + "failedsps": { + "en": "Unable to log out of one or more services. To ensure that all your sessions are closed, you are encouraged to <i>close your webbrowser<\/i>." } } diff --git a/dictionaries/logout.translation.json b/dictionaries/logout.translation.json index c61452adb9a97bacff3ce922a191ae194d9a42d5..9ceea23cadbabe194fb425fc3a66bc5f2e98e9b2 100644 --- a/dictionaries/logout.translation.json +++ b/dictionaries/logout.translation.json @@ -453,5 +453,13 @@ "pt": "N\u00e3o", "pl": "Nie", "tr": "Hay\u0131r" + }, + "logging_out_from": { + "sl": "Odjava iz naslednjih storitev:", + "da": "Du logger ud af f\u00f8lgende services:" + }, + "failedsps": { + "sl": "Odjava z ene ali ve\u010d storitev ni uspela. Odjavo dokon\u010dajte tako, da <i>zaprete spletni brskalnik<\/i>.", + "da": "Kan ikke logge ud af en eller flere services. For at sikre at alle dine sessioner er lukket <i>skal du lukke din browser<\/i>." } } diff --git a/lib/SimpleSAML/IdP.php b/lib/SimpleSAML/IdP.php index 2a17c5476e0601f6db6093fbaa46d4a16badf7ca..93798dcb1e4e48fb72797f72a27e39044e6cc0a4 100644 --- a/lib/SimpleSAML/IdP.php +++ b/lib/SimpleSAML/IdP.php @@ -107,6 +107,84 @@ class SimpleSAML_IdP { } + /** + * Get SP name. + * + * @param string $assocId The association identifier. + * @return array|NULL The name of the SP, as an associative array of language=>text, or NULL if this isn't an SP. + */ + public function getSPName($assocId) { + assert('is_string($assocId)'); + + if (substr($assocId, 0, 5) !== 'saml:') { + return NULL; + } + + $spEntityId = substr($assocId, 5); + $metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); + try { + $spMetadata = $metadata->getMetaDataConfig($spEntityId, 'saml20-sp-remote'); + } catch (Exception $e) { + try { + $spMetadata = $metadata->getMetaDataConfig($spEntityId, 'shib13-sp-remote'); + } catch (Exception $e) { + return NULL; + } + } + + return $spMetadata->getLocalizedString('name', array('en' => $spEntityId)); + } + + + /** + * Retrieve list of SP associations. + * + * @return array List of SP associations. + */ + public function getAssociations() { + + $session = SimpleSAML_Session::getInstance(); + + $associations = array(); + + foreach ($session->get_sp_list() as $spEntityId) { + + $nameId = $session->getSessionNameId('saml20-sp-remote', $spEntityId); + if($nameId === NULL) { + $nameId = $this->getNameID(); + } + + $id = 'saml:' . $spEntityId; + + $associations[$id] = array( + 'id' => $id, + 'Handler' => 'sspmod_saml_IdP_SAML2', + 'saml:entityID' => $spEntityId, + 'saml:NameID' => $nameId, + 'saml:SessionIndex' => $session->getSessionIndex(), + ); + } + + return $associations; + } + + + /** + * Remove an SP association. + * + * @param string $assocId The association id. + */ + public function terminateAssociation($assocId) { + assert('is_string($assocId)'); + + $session = SimpleSAML_Session::getInstance(); + + if (substr($assocId, 0, 5) === 'saml:') { + $session->set_sp_logout_completed(substr($assocId, 5)); + } + } + + /** * Is the current user authenticated? * @@ -279,4 +357,149 @@ class SimpleSAML_IdP { } } + + /** + * Find the logout handler of this IdP. + * + * @return string The logout handler class. + */ + public function getLogoutHandler() { + + /* Find the logout handler. */ + $logouttype = $this->getConfig()->getString('logouttype', 'traditional'); + switch ($logouttype) { + case 'traditional': + $handler = 'SimpleSAML_IdP_LogoutTraditional'; + break; + case 'iframe': + $handler = 'SimpleSAML_IdP_LogoutIFrame'; + break; + default: + throw new SimpleSAML_Error_Exception('Unknown logout handler: ' . var_export($logouttype, TRUE)); + } + + return new $handler($this); + + } + + + /** + * Finish the logout operation. + * + * This function will never return. + * + * @param array &$state The logout request state. + */ + public function finishLogout(array &$state) { + assert('isset($state["Responder"])'); + + call_user_func($state['Responder'], $this, $state); + assert('FALSE'); + } + + + /** + * Process a logout request. + * + * This function will never return. + * + * @param array &$state The logout request state. + * @param string|NULL $assocId The association we received the logout request from, or NULL if there was no association. + */ + public function handleLogoutRequest(array &$state, $assocId) { + assert('isset($state["Responder"])'); + assert('is_string($assocId) || is_null($assocId)'); + + $state['core:IdP'] = $this->id; + $state['core:TerminatedAssocId'] = $assocId; + + if ($assocId !== NULL) { + $this->terminateAssociation($assocId); + } + + /* Terminate the local session. */ + $session = SimpleSAML_Session::getInstance(); + $authority = $session->getAuthority(); + if ($authority !== NULL) { + /* We are logged in. */ + + $id = SimpleSAML_Auth_State::saveState($state, 'core:Logout:afterbridge'); + $returnTo = SimpleSAML_Module::getModuleURL('core/idp/resumelogout.php', + array('id' => $id) + ); + + if ($authority === $this->config->getString('auth')) { + /* This is probably an authentication source. */ + SimpleSAML_Auth_Default::initLogoutReturn($returnTo); + } elseif ($authority === 'saml2') { + /* SAML 2 SP which isn't an authentication source. */ + SimpleSAML_Utilities::redirect('/' . $config->getBaseURL() . 'saml2/sp/initSLO.php', + array('RelayState' => $returnTo) + ); + } else { + /* A different old-style authentication file. */ + $session->doLogout(); + } + } + + $handler = $this->getLogoutHandler(); + $handler->startLogout($state, $assocId); + assert('FALSE'); + } + + + /** + * Process a logout response. + * + * This function will never return. + * + * @param string $assocId The association that is terminated. + * @param string|NULL $relayState The RelayState from the start of the logout. + * @param SimpleSAML_Error_Exception|NULL $error The error that occured during session termination (if any). + */ + public function handleLogoutResponse($assocId, $relayState, SimpleSAML_Error_Exception $error = NULL) { + assert('is_string($assocId)'); + assert('is_string($relayState) || is_null($relayState)'); + + $handler = $this->getLogoutHandler(); + $handler->onResponse($assocId, $relayState, $error); + + assert('FALSE'); + } + + + /** + * Log out, then redirect to an URL. + * + * This function never returns. + * + * @param string $url The URL the user should be returned to after logout. + */ + public function doLogoutRedirect($url) { + assert('is_string($url)'); + + $state = array( + 'Responder' => array('SimpleSAML_IdP', 'finishLogoutRedirect'), + 'core:Logout:URL' => $url, + ); + + $this->handleLogoutRequest($state, NULL); + assert('FALSE'); + } + + + /** + * Redirect to an URL after logout. + * + * This function never returns. + * + * @param array &$state The logout state from doLogoutRedirect(). + */ + public static function finishLogoutRedirect(SimpleSAML_IdP $idp, array $state) { + assert('isset($state["core:Logout:URL"])'); + + SimpleSAML_Utilities::redirect($state['core:Logout:URL']); + assert('FALSE'); + } + } diff --git a/lib/SimpleSAML/IdP/LogoutHandler.php b/lib/SimpleSAML/IdP/LogoutHandler.php new file mode 100644 index 0000000000000000000000000000000000000000..44a393d21c7822e224d8a8a0c3265a2149cabebb --- /dev/null +++ b/lib/SimpleSAML/IdP/LogoutHandler.php @@ -0,0 +1,56 @@ +<?php + +/** + * Base class for logout handlers. + * + * @package simpleSAMLphp + * @version $Id$ + */ +abstract class SimpleSAML_IdP_LogoutHandler { + + /** + * The IdP we are logging out from. + * + * @var SimpleSAML_IdP + */ + protected $idp; + + + /** + * Initialize this logout handler. + * + * @param SimpleSAML_IdP $idp The IdP we are logging out from. + */ + public function __construct(SimpleSAML_IdP $idp) { + $this->idp = $idp; + } + + + /** + * Start a logout operation. + * + * This function must never return. + * + * @param array &$state The logout state. + * @param string|NULL $assocId The association that started the logout. + */ + abstract public function startLogout(array &$state, $assocId); + + + /** + * Handles responses to our logout requests. + * + * This function will never return. + * + * @param string $assocId The association that is terminated. + * @param string|NULL $relayState The RelayState from the start of the logout. + * @param SimpleSAML_Error_Exception|NULL $error The error that occured during session termination (if any). + */ + public function onResponse($assocId, $relayState, SimpleSAML_Error_Exception $error = NULL) { + assert('is_string($assocId)'); + assert('is_string($relayState) || is_null($relayState)'); + + /* Don't do anything by default. */ + } + +} diff --git a/lib/SimpleSAML/IdP/LogoutIFrame.php b/lib/SimpleSAML/IdP/LogoutIFrame.php new file mode 100644 index 0000000000000000000000000000000000000000..da99deed8336d16402b7fff11df1d1e267a8bc36 --- /dev/null +++ b/lib/SimpleSAML/IdP/LogoutIFrame.php @@ -0,0 +1,92 @@ +<?php + +/** + * Class which handles iframe logout. + * + * @package simpleSAMLphp + * @version $Id$ + */ +class SimpleSAML_IdP_LogoutIFrame extends SimpleSAML_IdP_LogoutHandler { + + /** + * Start the logout operation. + * + * @param array &$state The logout state. + * @param string|NULL $assocId The SP we are logging out from. + */ + public function startLogout(array &$state, $assocId) { + assert('is_string($assocId) || is_null($assocId)'); + + $associations = $this->idp->getAssociations(); + + if (count($associations) === 0) { + $this->idp->finishLogout($state); + } + + foreach ($associations as $id => &$association) { + $association['core:Logout-IFrame:Name'] = $this->idp->getSPName($id); + $association['core:Logout-IFrame:State'] = 'onhold'; + } + $state['core:Logout-IFrame:Associations'] = $associations; + + if (!is_null($assocId)) { + $spName = $this->idp->getSPName($assocId); + if ($spName === NULL) { + $spName = array('en' => $assocId); + } + + $state['core:Logout-IFrame:From'] = $spName; + } else { + $state['core:Logout-IFrame:From'] = NULL; + } + + $id = SimpleSAML_Auth_State::saveState($state, 'core:Logout-IFrame'); + $url = SimpleSAML_Module::getModuleURL('core/idp/logout-iframe.php', array('id' => $id)); + SimpleSAML_Utilities::redirect($url); + } + + + /** + * Continue the logout operation. + * + * This function will never return. + * + * @param string $assocId The association that is terminated. + * @param string|NULL $relayState The RelayState from the start of the logout. + * @param SimpleSAML_Error_Exception|NULL $error The error that occured during session termination (if any). + */ + public function onResponse($assocId, $relayState, SimpleSAML_Error_Exception $error = NULL) { + assert('is_string($assocId)'); + + $spId = sha1($assocId); + $cookieId = 'logout-iframe-' . $spId; + + $globalConfig = SimpleSAML_Configuration::getInstance(); + $cookiePath = '/' . $globalConfig->getBaseURL(); + + setcookie($cookieId, ($error ? 'failed' : 'completed'), time() + 5*60, $cookiePath); + + echo('<!DOCTYPE html> +<html> +<head> +<title>Logout response from ' . htmlspecialchars(var_export($assocId, TRUE)) . '</title> +<script> +'); + if ($error) { + $errorMsg = $error->getMessage(); + echo('window.parent.logoutFailed("' . $spId . '", "' . addslashes($errorMsg) . '");'); + } else { + echo('window.parent.logoutCompleted("' . $spId . '");'); + } + echo(' +</script> +</head> +<body> +</body> +</html> +'); + + exit(0); + } + +} diff --git a/lib/SimpleSAML/IdP/LogoutTraditional.php b/lib/SimpleSAML/IdP/LogoutTraditional.php new file mode 100644 index 0000000000000000000000000000000000000000..0db49e2755641a2e3a224d1cab2e9edcdbe13423 --- /dev/null +++ b/lib/SimpleSAML/IdP/LogoutTraditional.php @@ -0,0 +1,92 @@ +<?php + +/** + * Class which handles traditional logout. + * + * @package simpleSAMLphp + * @version $Id$ + */ +class SimpleSAML_IdP_LogoutTraditional extends SimpleSAML_IdP_LogoutHandler { + + /** + * Picks the next SP and issues a logout request. + * + * This function never returns. + * + * @param array &$state The logout state. + */ + private function logoutNextSP(array &$state) { + + $association = array_pop($state['core:LogoutTraditional:Remaining']); + if ($association === NULL) { + $this->idp->finishLogout($state); + } + + $relayState = SimpleSAML_Auth_State::saveState($state, 'core:LogoutTraditional', TRUE); + + $id = $association['id']; + SimpleSAML_Logger::info('Logging out of ' . var_export($id, TRUE) . '.'); + + try { + $url = call_user_func(array($association['Handler'], 'getLogoutURL'), $this->idp, $association, $relayState); + SimpleSAML_Utilities::redirect($url); + } catch (Exception $e) { + SimpleSAML_Logger::warning('Unable to initialize logout to ' . var_export($id, TRUE) . '.'); + $this->idp->terminateAssociation($id); + $state['core:Failed'] = TRUE; + + /* Try the next SP. */ + $this->logoutNextSP($state); + assert('FALSE'); + } + } + + + /** + * Start the logout operation. + * + * This function never returns. + * + * @param array &$state The logout state. + * @param string $assocId The association that started the logout. + */ + public function startLogout(array &$state, $assocId) { + + $state['core:LogoutTraditional:Remaining'] = $this->idp->getAssociations(); + + self::logoutNextSP($state); + } + + + /** + * Continue the logout operation. + * + * This function will never return. + * + * @param string $assocId The association that is terminated. + * @param string|NULL $relayState The RelayState from the start of the logout. + * @param SimpleSAML_Error_Exception|NULL $error The error that occured during session termination (if any). + */ + public function onResponse($assocId, $relayState, SimpleSAML_Error_Exception $error = NULL) { + assert('is_string($assocId)'); + assert('is_string($relayState) || is_null($relayState)'); + + if ($relayState === NULL) { + throw new SimpleSAML_Error_Exception('RelayState lost during logout.'); + } + + $state = SimpleSAML_Auth_State::loadState($relayState, 'core:LogoutTraditional'); + + if ($error === NULL) { + SimpleSAML_Logger::info('Logged out of ' . var_export($assocId, TRUE) . '.'); + $this->idp->terminateAssociation($assocId); + } else { + SimpleSAML_Logger::warning('Error received from ' . var_export($assocId, TRUE) . ' during logout:'); + $error->logWarning(); + $state['core:Failed'] = TRUE; + } + + self::logoutNextSP($state); + } + +} diff --git a/modules/core/templates/logout-iframe-wrapper.php b/modules/core/templates/logout-iframe-wrapper.php new file mode 100644 index 0000000000000000000000000000000000000000..3fafb77eec06498809c62a3acfe979516e45e604 --- /dev/null +++ b/modules/core/templates/logout-iframe-wrapper.php @@ -0,0 +1,29 @@ +<?php + +$id = $this->data['id']; +$SPs = $this->data['SPs']; +$timeout = $this->data['timeout']; + +$iframeURL = 'logout-iframe.php?type=embed&id=' . urlencode($id) . '&timeout=' . (string)$timeout; + +/* Pretty arbitrary height, but should have enough safety margins for most cases. */ +$iframeHeight = 25 + count($SPs) * 4; + +$this->data['header'] = $this->t('{logout:progress}'); +$this->includeAtTemplateBase('includes/header.php'); +echo '<iframe style="width:100%; height:' . $iframeHeight . 'em; border:0;" src="' . htmlspecialchars($iframeURL) . '"></iframe>'; + +foreach ($SPs AS $assocId => $sp) { + $spId = sha1($assocId); + + if ($sp['core:Logout-IFrame:State'] !== 'inprogress') { + continue; + } + assert('isset($sp["core:Logout-IFrame:URL"])'); + + $url = $sp["core:Logout-IFrame:URL"]; + + echo('<iframe style="width:0; height:0; border:0;" src="' . htmlspecialchars($url) . '"></iframe>'); +} + +$this->includeAtTemplateBase('includes/footer.php'); diff --git a/modules/core/templates/logout-iframe.php b/modules/core/templates/logout-iframe.php new file mode 100644 index 0000000000000000000000000000000000000000..d68f4572a2b40497fb99f2c45851e054f0dcc649 --- /dev/null +++ b/modules/core/templates/logout-iframe.php @@ -0,0 +1,205 @@ +<?php + +$id = $this->data['id']; +$type = $this->data['type']; +$from = $this->data['from']; +$SPs = $this->data['SPs']; +$timeout = $this->data['timeout']; + +$stateImage = array( + 'unsupported' => '/' . $this->data['baseurlpath'] . 'resources/icons/silk/delete.png', + 'completed' => '/' . $this->data['baseurlpath'] . 'resources/icons/silk/accept.png', + 'onhold' => '/' . $this->data['baseurlpath'] . 'resources/icons/bullet16_grey.png', + 'inprogress' => '/' . $this->data['baseurlpath'] . 'resources/progress.gif', + 'failed' => '/' . $this->data['baseurlpath'] . 'resources/icons/silk/exclamation.png', +); + +$stateText = array( + 'unsupported' => '', + 'completed' => $this->t('{logout:completed}'), + 'onhold' => '', + 'inprogress' => $this->t('{logout:progress}'), + 'failed' => $this->t('{logout:failed}'), +); + +$spStatus = array(); +$nFailed = 0; +$nProgress = 0; +foreach ($SPs as $assocId => $sp) { + assert('isset($sp["core:Logout-IFrame:State"])'); + $state = $sp['core:Logout-IFrame:State']; + $spStatus[sha1($assocId)] = $state; + if ($state === 'failed') { + $nFailed += 1; + } elseif ($state === 'inprogress') { + $nProgress += 1; + } +} + +if ($from !== NULL) { + $from = $this->getTranslation($from); +} + + +if (!isset($this->data['head'])) { + $this->data['head'] = ''; +} + +$this->data['head'] .= '<script type="text/javascript" src="/' . $this->data['baseurlpath'] . 'resources/jquery.js"></script>'; + +$this->data['head'] .= ' +<script type="text/javascript" language="JavaScript"> +window.stateImage = ' . json_encode($stateImage) . '; +window.stateText = ' . json_encode($stateText) . '; +window.spStatus = ' . json_encode($spStatus) . '; +window.type = "' . $type . '"; +window.timeoutIn = ' . (string)($timeout - time()) . '; +window.asyncURL = "logout-iframe.php?id=' . $id . '&type=async"; +</script>'; + +$this->data['head'] .= '<script type="text/javascript" src="logout-iframe.js"></script>'; + +if ($type === 'embed') { + $this->data['head'] .= '<meta http-equiv="refresh" content="1" />'; +} + +$this->data['header'] = $this->t('{logout:progress}'); +if ($type === 'embed') { + $this->includeAtTemplateBase('includes/header-embed.php'); +} else { + $this->includeAtTemplateBase('includes/header.php'); +} + +if ($from !== NULL) { + + echo('<div><img style="float: left; margin-right: 12px" src="/' . $this->data['baseurlpath'] . 'resources/icons/checkmark48.png" alt="Successful logout" />'); + echo('<p style="padding-top: 16px; ">' . $this->t('{logout:loggedoutfrom}', array('%SP%' => '<strong>' .htmlspecialchars($from).'</strong>')) . '</p>'); + echo('<p style="height: 0px; clear: left;"></p>'); + echo('</div>'); +} + +echo('<div style="margin-top: 3em; clear: both">'); + + +echo('<p style="margin-bottom: .5em">'); +if ($type === 'init') { + echo($this->t('{logout:also_from}')); +} else { + echo($this->t('{logout:logging_out_from}')); +} +echo('</p>'); + +echo '<table id="slostatustable">'; + +foreach ($SPs AS $assocId => $sp) { + if (isset($sp['core:Logout-IFrame:Name'])) { + $spName = $this->getTranslation($sp['core:Logout-IFrame:Name']); + } else { + $spName = $assocId; + } + + assert('isset($sp["core:Logout-IFrame:State"])'); + $spState = $sp['core:Logout-IFrame:State']; + + $spId = sha1($assocId); + + echo '<tr>'; + + echo '<td>'; + echo '<img class="logoutstatusimage" id="statusimage-' . $spId . '" src="' . htmlspecialchars($stateImage[$spState]) . '" alt="' . htmlspecialchars($stateText[$spState]) . '"/>'; + echo '</td>'; + + echo '<td>' . htmlspecialchars($spName) . '</td>'; + + echo '</tr>'; +} + +if (isset($from)) { + $logoutCancelText = $this->t('{logout:logout_only}', array('%SP%' => htmlspecialchars($from))); +} else { + $logoutCancelText = $this->t('{logout:no}'); +} + +?> +</table> +</div> + +<?php +if ($type === 'init') { +?> +<div id="confirmation" style="margin-top: 1em" > +<p><?php echo $this->t('{logout:logout_all_question}'); ?> <br /></p> + +<form id="startform" method="get" action="logout-iframe.php"> +<input type="hidden" name="id" value="<?php echo $id; ?>" /> +<input type="hidden" id="logout-type-selector" name="type" value="nojs" /> +<input type="submit" id="logout-all" name="ok" value="<?php echo $this->t('{logout:logout_all}'); ?>" /> +</form> + +<form method="get" action="logout-iframe-done.php"> +<input type="hidden" name="id" value="<?php echo $id; ?>" /> +<input type="submit" name="cancel" value="<?php echo $logoutCancelText; ?>" /> +</form> + +</div> + +<?php +} else { +?> + +<?php +if ($nFailed > 0) { + $displayStyle = ''; +} else { + $displayStyle = 'display: none;'; +} +echo('<div id="logout-failed-message" style="margin-top: 1em; border: 1px solid #ccc; padding: 1em; background: #eaeaea;' . $displayStyle . '">'); +echo('<img src="/' . $this->data['baseurlpath'] . 'resources/icons/caution.png" alt="" style="float: left; margin-right: 5px;" />'); +echo('<p>' . $this->t('{logout:failedsps}') . '</p>'); +echo('<form method="get" action="logout-iframe-done.php" target="_top">'); +echo('<input type="hidden" name="id" value="' . $id . '" />'); +echo('<input type="submit" name="continue" value="' . $this->t('{logout:return}'). '" />'); +echo('</form>'); + +echo('</div>'); + +if ($nProgress == 0 && $nFailed == 0) { + echo('<div id="logout-completed">'); +} else { + echo('<div id="logout-completed" style="display:none;">'); +} +echo('<p>' . $this->t('{logout:success}') . '</p>'); +?> +<form method="get" action="logout-iframe-done.php" id="done-form" target="_top"> + <input type="hidden" name="id" value="<?php echo $id; ?>" /> + <input type="submit" name="continue" value="<?php echo $this->t('{logout:return}'); ?>" /> +</form> +</div> + +<?php +if ($type === 'js') { + foreach ($SPs AS $assocId => $sp) { + $spId = sha1($assocId); + + if ($sp['core:Logout-IFrame:State'] !== 'inprogress') { + continue; + } + assert('isset($sp["core:Logout-IFrame:URL"])'); + + $url = $sp["core:Logout-IFrame:URL"]; + + echo('<iframe style="width:0; height:0; border:0;" src="' . htmlspecialchars($url) . '"></iframe>'); + } +} +?> + +<?php +} +?> + +<?php +if ($type === 'embed') { + $this->includeAtTemplateBase('includes/footer-embed.php'); +} else { + $this->includeAtTemplateBase('includes/footer.php'); +} diff --git a/modules/core/www/idp/logout-iframe-done.php b/modules/core/www/idp/logout-iframe-done.php new file mode 100644 index 0000000000000000000000000000000000000000..4625ca4db0bab431f89474de3cecb8977bcd3f25 --- /dev/null +++ b/modules/core/www/idp/logout-iframe-done.php @@ -0,0 +1,52 @@ +<?php + +if (!isset($_REQUEST['id'])) { + throw new SimpleSAML_Error_BadRequest('Missing required parameter: id'); +} +$id = (string)$_REQUEST['id']; + +$state = SimpleSAML_Auth_State::loadState($id, 'core:Logout-IFrame'); +$idp = SimpleSAML_IdP::getByState($state); + +$associations = $idp->getAssociations(); +$SPs = $state['core:Logout-IFrame:Associations']; + +$globalConfig = SimpleSAML_Configuration::getInstance(); +$cookiePath = '/' . $globalConfig->getBaseURL(); + +/* Find the status of all SPs. */ +foreach ($SPs as $assocId => &$sp) { + + $spId = sha1($assocId); + + $cookieId = 'logout-iframe-' . $spId; + if (isset($_COOKIE[$cookieId])) { + $cookie = $_COOKIE[$cookieId]; + if ($cookie == 'completed' || $cookie == 'failed') { + $sp['core:Logout-IFrame:State'] = $cookie; + } + setcookie($cookieId, '', time() - 3600, $cookiePath); + } + + if (!isset($associations[$assocId])) { + $sp['core:Logout-IFrame:State'] = 'completed'; + } + +} + + +/* Terminate the associations. */ +foreach ($SPs as $assocId => $sp) { + + if ($sp['core:Logout-IFrame:State'] === 'completed') { + $idp->terminateAssociation($assocId); + } else { + SimpleSAML_Logger::warning('Unable to terminate association with ' . var_export($assocId, TRUE) . '.'); + $state['core:Failed'] = TRUE; + } + +} + + +/* We are done. */ +$idp->finishLogout($state); diff --git a/modules/core/www/idp/logout-iframe.js b/modules/core/www/idp/logout-iframe.js new file mode 100644 index 0000000000000000000000000000000000000000..5937567f1ec6fc3d822e1e8f2383318b2cd70017 --- /dev/null +++ b/modules/core/www/idp/logout-iframe.js @@ -0,0 +1,75 @@ +function updateStatus() { + + var nFailed = 0; + var nProgress = 0; + for (sp in window.spStatus) { + switch (window.spStatus[sp]) { + case 'failed': + nFailed += 1; + break; + case 'inprogress': + nProgress += 1; + break; + } + } + + if (nFailed > 0) { + $('#logout-failed-message').show(); + } + + if (nProgress == 0 && nFailed == 0) { + $('#logout-completed').show(); + $('#done-form').submit(); + } +} + +function updateSPStatus(spId, status, reason) { + if (window.spStatus[spId] == status) { + /* Unchanged. */ + return; + } + + $('#statusimage-' + spId).attr('src', window.stateImage[status]).attr('alt', window.stateText[status]).attr('title', reason); + window.spStatus[spId] = status; + + updateStatus(); +} +function logoutCompleted(spId) { + updateSPStatus(spId, 'completed', ''); +} +function logoutFailed(spId, reason) { + updateSPStatus(spId, 'failed', reason); +} + +function timeoutSPs() { + for (sp in window.spStatus) { + if (window.spStatus[sp] == 'inprogress') { + logoutFailed(sp, 'Timeout'); + } + } +} + +function asyncUpdate() { + jQuery.getJSON(window.asyncURL, window.spStatus, function(data, textStatus) { + for (sp in data) { + if (data[sp] == 'completed') { + logoutCompleted(sp); + } else if (data[sp] == 'failed') { + logoutFailed(sp, 'async update'); + } + } + window.setTimeout(asyncUpdate, 1000); + }); +} + + +$('document').ready(function(){ + if (window.type == 'js') { + window.timeoutID = window.setTimeout(timeoutSPs, window.timeoutIn * 1000); + window.setTimeout(asyncUpdate, 1000); + updateStatus(); + } else if (window.type == 'init') { + $('#logout-type-selector').attr('value', 'js'); + $('#logout-all').focus(); + } +}); diff --git a/modules/core/www/idp/logout-iframe.php b/modules/core/www/idp/logout-iframe.php new file mode 100644 index 0000000000000000000000000000000000000000..3cb2c2efaf22bbcb03bd641ae687c714d86112fc --- /dev/null +++ b/modules/core/www/idp/logout-iframe.php @@ -0,0 +1,118 @@ +<?php + +if (!isset($_REQUEST['id'])) { + throw new SimpleSAML_Error_BadRequest('Missing required parameter: id'); +} +$id = (string)$_REQUEST['id']; + +if (isset($_REQUEST['type'])) { + $type = (string)$_REQUEST['type']; + if (!in_array($type, array('init', 'js', 'nojs', 'embed', 'async'), TRUE)) { + throw new SimpleSAML_Error_BadRequest('Invalid value for type.'); + } +} else { + $type = 'init'; +} + +if (isset($_REQUEST['timeout'])) { + $timeout = (int)$_REQUEST['timeout']; +} else { + $timeout = time() + 10; +} + +$state = SimpleSAML_Auth_State::loadState($id, 'core:Logout-IFrame'); +$idp = SimpleSAML_IdP::getByState($state); + +if ($type !== 'init') { + /* Update association state. */ + + $associations = $idp->getAssociations(); + + foreach ($state['core:Logout-IFrame:Associations'] as $assocId => &$sp) { + + $spId = sha1($assocId); + + /* Move SPs from 'onhold' to 'inprogress'. */ + if ($sp['core:Logout-IFrame:State'] === 'onhold') { + $sp['core:Logout-IFrame:State'] = 'inprogress'; + } + + /* Check for update by cookie. */ + $cookieId = 'logout-iframe-' . $spId; + if (isset($_COOKIE[$cookieId])) { + $cookie = $_COOKIE[$cookieId]; + if ($cookie == 'completed' || $cookie == 'failed') { + $sp['core:Logout-IFrame:State'] = $cookie; + } + } + + /* Check for update through request. */ + if (isset($_REQUEST[$spId])) { + $s = $_REQUEST[$spId]; + if ($s == 'completed' || $s == 'failed') { + $sp['core:Logout-IFrame:State'] = $s; + } + } + + /* In case we are refreshing a page. */ + if (!isset($associations[$assocId])) { + $sp['core:Logout-IFrame:State'] = 'completed'; + } + + /* Update the IdP. */ + if ($sp['core:Logout-IFrame:State'] === 'completed') { + $idp->terminateAssociation($assocId); + } + } +} + +if ($type === 'js' || $type === 'nojs') { + foreach ($state['core:Logout-IFrame:Associations'] as $assocId => &$sp) { + + if ($sp['core:Logout-IFrame:State'] !== 'inprogress') { + /* This SP isn't logging out. */ + continue; + } + + try { + $url = call_user_func(array($sp['Handler'], 'getLogoutURL'), $idp, $sp, NULL); + $sp['core:Logout-IFrame:URL'] = $url; + } catch (Exception $e) { + $sp['core:Logout-IFrame:State'] = 'failed'; + } + } +} + +$id = SimpleSAML_Auth_State::saveState($state, 'core:Logout-IFrame'); + +$globalConfig = SimpleSAML_Configuration::getInstance(); + +if ($type === 'nojs') { + $t = new SimpleSAML_XHTML_Template($globalConfig, 'core:logout-iframe-wrapper.php'); + $t->data['id'] = $id; + $t->data['SPs'] = $state['core:Logout-IFrame:Associations']; + $t->data['timeout'] = $timeout; + $t->show(); + exit(0); + +} elseif ($type == 'async') { + header('Content-Type: application/json'); + $res = array(); + foreach ($state['core:Logout-IFrame:Associations'] as $assocId => $sp) { + if ($sp['core:Logout-IFrame:State'] !== 'completed') { + continue; + } + $res[sha1($assocId)] = 'completed'; + } + echo(json_encode($res)); + exit(0); +} + +$t = new SimpleSAML_XHTML_Template($globalConfig, 'core:logout-iframe.php'); +$t->data['id'] = $id; +$t->data['type'] = $type; +$t->data['from'] = $state['core:Logout-IFrame:From']; +$t->data['SPs'] = $state['core:Logout-IFrame:Associations']; +$t->data['timeout'] = $timeout; +$t->show(); +exit(0);