From b97b910e5ea80a151d42202a3545d62a3eb962e3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaime=20P=C3=A9rez=20Crespo?= <jaime.perez@uninett.no>
Date: Tue, 13 Aug 2013 10:28:38 +0000
Subject: [PATCH] Complete bugfix for issue #561. HTTP-Post supported for SLO
 not initiated by the SP. Working for both traditional logout and iframe
 version.

git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@3262 44740490-163a-0410-bde0-09ae8108e29a
---
 lib/SimpleSAML/IdP/LogoutTraditional.php |   3 +-
 modules/core/templates/logout-iframe.php |  12 ++-
 modules/core/www/idp/logout-iframe.php   |   3 +-
 modules/core/www/idp/performlogout.php   |  59 +++++++++++++
 modules/saml/lib/IdP/SAML2.php           | 105 +++++++++++++++++------
 templates/includes/header-embed.php      |   2 +-
 www/resources/default.css                |   6 ++
 7 files changed, 157 insertions(+), 33 deletions(-)
 create mode 100644 modules/core/www/idp/performlogout.php

diff --git a/lib/SimpleSAML/IdP/LogoutTraditional.php b/lib/SimpleSAML/IdP/LogoutTraditional.php
index 5a4846608..202db7b33 100644
--- a/lib/SimpleSAML/IdP/LogoutTraditional.php
+++ b/lib/SimpleSAML/IdP/LogoutTraditional.php
@@ -29,8 +29,7 @@ class SimpleSAML_IdP_LogoutTraditional extends SimpleSAML_IdP_LogoutHandler {
 
 		try {
 			$idp = SimpleSAML_IdP::getByState($association);
-			$url = call_user_func(array($association['Handler'], 'getLogoutURL'), $idp, $association, $relayState);
-			SimpleSAML_Utilities::redirect($url);
+			call_user_func(array($association['Handler'], 'sendLogoutRequest'), $idp, $association, $relayState);
 		} catch (Exception $e) {
 			SimpleSAML_Logger::warning('Unable to initialize logout to ' . var_export($id, TRUE) . '.');
 			$this->idp->terminateAssociation($id);
diff --git a/modules/core/templates/logout-iframe.php b/modules/core/templates/logout-iframe.php
index fb31cdc04..d77213440 100644
--- a/modules/core/templates/logout-iframe.php
+++ b/modules/core/templates/logout-iframe.php
@@ -74,7 +74,10 @@ if ($type === 'embed') {
 } else {
 	$this->includeAtTemplateBase('includes/header.php');
 }
-
+?>
+<div id="wrap">
+  <div id="content">
+<?php
 if ($from !== NULL) {
 
 	echo('<div><img style="float: left; margin-right: 12px" src="/' . $this->data['baseurlpath'] . 'resources/icons/checkmark.48x48.png" alt="Successful logout" />');
@@ -167,7 +170,6 @@ echo('<form method="post" action="logout-iframe-done.php" id="failed-form" targe
 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) {
@@ -203,10 +205,14 @@ if ($type === 'js') {
 <?php
 }
 ?>
-
+  </div>
+  <!-- #content -->
 <?php
 if ($type === 'embed') {
 	$this->includeAtTemplateBase('includes/footer-embed.php');
 } else {
 	$this->includeAtTemplateBase('includes/footer.php');
 }
+?>
+</div>
+<!-- #wrap -->
diff --git a/modules/core/www/idp/logout-iframe.php b/modules/core/www/idp/logout-iframe.php
index 1b751e931..9d77bebe8 100644
--- a/modules/core/www/idp/logout-iframe.php
+++ b/modules/core/www/idp/logout-iframe.php
@@ -83,7 +83,8 @@ if ($type === 'js' || $type === 'nojs') {
 
 		try {
 			$assocIdP = SimpleSAML_IdP::getByState($sp);
-			$url = call_user_func(array($sp['Handler'], 'getLogoutURL'), $assocIdP, $sp, NULL);
+			$params = array('id' => $id, 'sp' => $sp['id'], 'type' => $type);
+			$url = SimpleSAML_Module::getModuleURL('core/idp/performlogout.php', $params);
 			$sp['core:Logout-IFrame:URL'] = $url;
 		} catch (Exception $e) {
 			$sp['core:Logout-IFrame:State'] = 'failed';
diff --git a/modules/core/www/idp/performlogout.php b/modules/core/www/idp/performlogout.php
new file mode 100644
index 000000000..6c17a1c9f
--- /dev/null
+++ b/modules/core/www/idp/performlogout.php
@@ -0,0 +1,59 @@
+<?php
+
+if (!isset($_REQUEST['id'])) {
+	throw new SimpleSAML_Error_BadRequest('Missing id-parameter.');
+}
+$id = (string)$_REQUEST['id'];
+if (!isset($_REQUEST['sp'])) {
+	throw new SimpleSAML_Error_BadRequest('Missing sp-parameter.');
+}
+$sp = urldecode($_REQUEST['sp']);
+$type = @(string)$_REQUEST['type'];
+
+$state = SimpleSAML_Auth_State::loadState($id, 'core:Logout-IFrame');
+$idp = SimpleSAML_IdP::getByState($state);
+
+$associations = $state['core:Logout-IFrame:Associations'];
+if (!isset($associations[$sp])) {
+	exit;
+}
+
+$association = $associations[$sp];
+
+$metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler();
+$idpMetadata = $idp->getConfig();
+$spMetadata = $metadata->getMetaDataConfig($association['saml:entityID'], 'saml20-sp-remote');
+
+$lr = sspmod_saml_Message::buildLogoutRequest($idpMetadata, $spMetadata);
+$lr->setSessionIndex($association['saml:SessionIndex']);
+$lr->setNameId($association['saml:NameID']);
+
+$assertionLifetime = $spMetadata->getInteger('assertion.lifetime', NULL);
+if ($assertionLifetime === NULL) {
+	$assertionLifetime = $idpMetadata->getInteger('assertion.lifetime', 300);
+}
+$lr->setNotOnOrAfter(time() + $assertionLifetime);
+
+$encryptNameId = $spMetadata->getBoolean('nameid.encryption', NULL);
+if ($encryptNameId === NULL) {
+	$encryptNameId = $idpMetadata->getBoolean('nameid.encryption', FALSE);
+}
+if ($encryptNameId) {
+	$lr->encryptNameId(sspmod_saml_Message::getEncryptionKey($spMetadata));
+}
+
+SimpleSAML_Stats::log('saml:idp:LogoutRequest:sent', array(
+	'spEntityID' => $association['saml:entityID'],
+	'idpEntityID' => $idpMetadata->getString('entityid'),
+));
+
+$bindings = array(SAML2_Const::BINDING_HTTP_REDIRECT);
+if ($type === 'js') {
+	array_push($bindings, SAML2_Const::BINDING_HTTP_POST);
+}
+
+$dst = $spMetadata->getDefaultEndpoint('SingleLogoutService', $bindings);
+$binding = SAML2_Binding::getBinding($dst['Binding']);
+$lr->setDestination($dst['Location']);
+
+$binding->send($lr);
diff --git a/modules/saml/lib/IdP/SAML2.php b/modules/saml/lib/IdP/SAML2.php
index 8b8897eb4..bc5613fd9 100644
--- a/modules/saml/lib/IdP/SAML2.php
+++ b/modules/saml/lib/IdP/SAML2.php
@@ -382,9 +382,43 @@ class sspmod_saml_IdP_SAML2 {
 	}
 
 
+	/**
+	 * Send a logout request to a given association.
+	 *
+	 * @param SimpleSAML_IdP $idp  The IdP we are sending a logout request from.
+	 * @param array $association  The association that should be terminated.
+	 * @param string|NULL $relayState  An id that should be carried across the logout.
+	 */
+	public static function sendLogoutRequest(SimpleSAML_IdP $idp, array $association, $relayState) {
+		assert('is_string($relayState) || is_null($relayState)');
+
+		SimpleSAML_Logger::info('Sending SAML 2.0 LogoutRequest to: '. var_export($association['saml:entityID'], TRUE));
+
+		$metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler();
+		$idpMetadata = $idp->getConfig();
+		$spMetadata = $metadata->getMetaDataConfig($association['saml:entityID'], 'saml20-sp-remote');
+
+		SimpleSAML_Stats::log('saml:idp:LogoutRequest:sent', array(
+			'spEntityID' => $association['saml:entityID'],
+			'idpEntityID' => $idpMetadata->getString('entityid'),
+		));
+
+		$dst = $spMetadata->getDefaultEndpoint('SingleLogoutService', array(
+			SAML2_Const::BINDING_HTTP_REDIRECT,
+			SAML2_Const::BINDING_HTTP_POST)
+		);
+		$binding = SAML2_Binding::getBinding($dst['Binding']);
+		$lr = self::buildLogoutRequest($idpMetadata, $spMetadata, $association, $relayState);
+		$lr->setDestination($dst['Location']);
+
+		$binding->send($lr);
+	}
+
+
 	/**
 	 * Send a logout response.
 	 *
+	 * @param SimpleSAML_IdP $idp  The IdP we are sending a logout request from.
 	 * @param array &$state  The logout state array.
 	 */
 	public static function sendLogoutResponse(SimpleSAML_IdP $idp, array $state) {
@@ -512,7 +546,8 @@ class sspmod_saml_IdP_SAML2 {
 
 
 	/**
-	 * Retrieve a logout URL for a given logout association.
+     * Retrieve a logout URL for a given logout association. Only for logout endpoints that support
+     * HTTP-Redirect binding.
 	 *
 	 * @param SimpleSAML_IdP $idp  The IdP we are sending a logout request from.
 	 * @param array $association  The association that should be terminated.
@@ -527,29 +562,11 @@ class sspmod_saml_IdP_SAML2 {
 		$idpMetadata = $idp->getConfig();
 		$spMetadata = $metadata->getMetaDataConfig($association['saml:entityID'], 'saml20-sp-remote');
 
-		$lr = sspmod_saml_Message::buildLogoutRequest($idpMetadata, $spMetadata);
-		$lr->setRelayState($relayState);
-		$lr->setSessionIndex($association['saml:SessionIndex']);
-		$lr->setNameId($association['saml:NameID']);
-
-		$assertionLifetime = $spMetadata->getInteger('assertion.lifetime', NULL);
-		if ($assertionLifetime === NULL) {
-			$assertionLifetime = $idpMetadata->getInteger('assertion.lifetime', 300);
-		}
-		$lr->setNotOnOrAfter(time() + $assertionLifetime);
-
-		$encryptNameId = $spMetadata->getBoolean('nameid.encryption', NULL);
-		if ($encryptNameId === NULL) {
-			$encryptNameId = $idpMetadata->getBoolean('nameid.encryption', FALSE);
-		}
-		if ($encryptNameId) {
-			$lr->encryptNameId(sspmod_saml_Message::getEncryptionKey($spMetadata));
-		}
-
-		SimpleSAML_Stats::log('saml:idp:LogoutRequest:sent', array(
-			'spEntityID' => $association['saml:entityID'],
-			'idpEntityID' => $idpMetadata->getString('entityid'),
-		));
+		// It doesn't make sense to use HTTP-Post when asking for a logout URL, therefore we allow only
+		// HTTP-Redirect.
+		$dst = $spMetadata->getDefaultEndpoint('SingleLogoutService', array(SAML2_Const::BINDING_HTTP_REDIRECT));
+		$lr = self::buildLogoutRequest($idpMetadata, $spMetadata, $association, $relayState);
+		$lr->setDestination($dst['Location']);
 
 		$binding = new SAML2_HTTPRedirect();
 		return $binding->getRedirectURL($lr);
@@ -693,7 +710,8 @@ class sspmod_saml_IdP_SAML2 {
 	 * @param SimpleSAML_Configuration $spMetadata  The metadata of the SP.
 	 * @return string  The NameFormat.
 	 */
-	private static function getAttributeNameFormat(SimpleSAML_Configuration $idpMetadata, SimpleSAML_Configuration $spMetadata) {
+	private static function getAttributeNameFormat(SimpleSAML_Configuration $idpMetadata,
+		SimpleSAML_Configuration $spMetadata) {
 
 		/* Try SP metadata first. */
 		$attributeNameFormat = $spMetadata->getString('attributes.NameFormat', NULL);
@@ -940,6 +958,40 @@ class sspmod_saml_IdP_SAML2 {
 	}
 
 
+	/**
+	 * Build a logout request based on information in the metadata.
+	 *
+	 * @param SimpleSAML_Configuration idpMetadata  The metadata of the IdP.
+	 * @param SimpleSAML_Configuration spMetadata  The metadata of the SP.
+	 * @param array $association  The SP association.
+	 * @param string|NULL $relayState  An id that should be carried across the logout.
+	 */
+	private static function buildLogoutRequest(SimpleSAML_Configuration $idpMetadata,
+		SimpleSAML_Configuration $spMetadata, array $association, $relayState) {
+
+		$lr = sspmod_saml_Message::buildLogoutRequest($idpMetadata, $spMetadata);
+		$lr->setRelayState($relayState);
+		$lr->setSessionIndex($association['saml:SessionIndex']);
+		$lr->setNameId($association['saml:NameID']);
+
+		$assertionLifetime = $spMetadata->getInteger('assertion.lifetime', NULL);
+		if ($assertionLifetime === NULL) {
+			$assertionLifetime = $idpMetadata->getInteger('assertion.lifetime', 300);
+		}
+		$lr->setNotOnOrAfter(time() + $assertionLifetime);
+
+		$encryptNameId = $spMetadata->getBoolean('nameid.encryption', NULL);
+		if ($encryptNameId === NULL) {
+			$encryptNameId = $idpMetadata->getBoolean('nameid.encryption', FALSE);
+		}
+		if ($encryptNameId) {
+			$lr->encryptNameId(sspmod_saml_Message::getEncryptionKey($spMetadata));
+		}
+
+		return $lr;
+	}
+
+
 	/**
 	 * Build a authentication response based on information in the metadata.
 	 *
@@ -947,7 +999,8 @@ class sspmod_saml_IdP_SAML2 {
 	 * @param SimpleSAML_Configuration $spMetadata  The metadata of the SP.
 	 * @param string $consumerURL  The Destination URL of the response.
 	 */
-	private static function buildResponse(SimpleSAML_Configuration $idpMetadata, SimpleSAML_Configuration $spMetadata, $consumerURL) {
+	private static function buildResponse(SimpleSAML_Configuration $idpMetadata,
+		SimpleSAML_Configuration $spMetadata, $consumerURL) {
 
 		$signResponse = $spMetadata->getBoolean('saml20.sign.response', NULL);
 		if ($signResponse === NULL) {
diff --git a/templates/includes/header-embed.php b/templates/includes/header-embed.php
index dce6cba61..7666371cd 100644
--- a/templates/includes/header-embed.php
+++ b/templates/includes/header-embed.php
@@ -19,5 +19,5 @@ if(array_key_exists('head', $this->data)) {
 }
 ?>
 </head>
-<body>
+<body class="body-embed">
 
diff --git a/www/resources/default.css b/www/resources/default.css
index 2d2584112..dbce4317c 100644
--- a/www/resources/default.css
+++ b/www/resources/default.css
@@ -16,6 +16,12 @@ body {
 	font: 83%/1.5 arial,tahoma,verdana,sans-serif;
 }
 
+.body-embed {
+	padding: 0;
+	background: #ffffff;
+	font: 83%/1.5 arial,tahoma,verdana,sans-serif;
+}
+
 img {
 	border: none;
 	display: block;
-- 
GitLab