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);