From 696b970fcaf73d30eb9b7bf2b9f6e4ef3026a968 Mon Sep 17 00:00:00 2001
From: Olav Morken <olav.morken@uninett.no>
Date: Wed, 19 Aug 2009 08:31:19 +0000
Subject: [PATCH] saml: Add new authentication module which handles both
 Shibboleth 1.3 and SAML 2.0.

git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@1695 44740490-163a-0410-bde0-09ae8108e29a
---
 config-templates/authsources.php     |  28 ++
 modules/saml/default-enable          |   3 +
 modules/saml/lib/Auth/Source/SP.php  | 377 +++++++++++++++++++++++++++
 modules/saml/www/disco.php           |   8 +
 modules/saml/www/sp/discoresp.php    |  29 +++
 modules/saml/www/sp/metadata.php     |  43 +++
 modules/saml/www/sp/saml1-acs.php    |  46 ++++
 modules/saml/www/sp/saml2-acs.php    |  63 +++++
 modules/saml/www/sp/saml2-logout.php |  73 ++++++
 9 files changed, 670 insertions(+)
 create mode 100644 modules/saml/default-enable
 create mode 100644 modules/saml/lib/Auth/Source/SP.php
 create mode 100644 modules/saml/www/disco.php
 create mode 100644 modules/saml/www/sp/discoresp.php
 create mode 100644 modules/saml/www/sp/metadata.php
 create mode 100644 modules/saml/www/sp/saml1-acs.php
 create mode 100644 modules/saml/www/sp/saml2-acs.php
 create mode 100644 modules/saml/www/sp/saml2-logout.php

diff --git a/config-templates/authsources.php b/config-templates/authsources.php
index aee707ce1..fe88544e3 100644
--- a/config-templates/authsources.php
+++ b/config-templates/authsources.php
@@ -69,6 +69,34 @@ $config = array(
 		'saml2:SP',
 	),
 	
+
+	/*
+	 * An authentication source which can authenticate against both SAML 2.0
+	 * and Shibboleth 1.3 IdPs.
+	 */
+	'example-saml' => array(
+
+		/*
+		 * The entity ID of this SP.
+		 * Can be NULL/unset, in which case an entity ID is generated based on the metadata URL.
+		 */
+		'entityId' => NULL,
+
+		/*
+		 * The entity ID of the IdP this should SP should contact.
+		 * Can be NULL/unset, in which case the user will be shown a list of available IdPs.
+		 */
+		'idp' => NULL,
+
+		/*
+		 * The URL to the discovery service.
+		 * Can be NULL/unset, in which case a builtin discovery service will be used.
+		 */
+		'discoURL' => NULL,
+
+	),
+
+
 	'facebook' => array(
 		'authfacebook:Facebook',
 		'api_key' => 'xxxxxxxxxxxxxxxx',
diff --git a/modules/saml/default-enable b/modules/saml/default-enable
new file mode 100644
index 000000000..25615cb47
--- /dev/null
+++ b/modules/saml/default-enable
@@ -0,0 +1,3 @@
+This file indicates that the default state of this module
+is enabled. To disable, create a file named disable in the
+same directory as this file.
diff --git a/modules/saml/lib/Auth/Source/SP.php b/modules/saml/lib/Auth/Source/SP.php
new file mode 100644
index 000000000..2127746b0
--- /dev/null
+++ b/modules/saml/lib/Auth/Source/SP.php
@@ -0,0 +1,377 @@
+<?php
+
+class sspmod_saml_Auth_Source_SP extends SimpleSAML_Auth_Source {
+
+	/**
+	 * The entity ID of this SP.
+	 *
+	 * @var string
+	 */
+	private $entityId;
+
+
+	/**
+	 * The metadata of this SP.
+	 *
+	 * @var SimpleSAML_Configuration.
+	 */
+	private $metadata;
+
+
+	/**
+	 * The IdP the user is allowed to log into.
+	 *
+	 * @var string|NULL  The IdP the user can log into, or NULL if the user can log into all IdPs.
+	 */
+	private $idp;
+
+
+	/**
+	 * URL to discovery service.
+	 *
+	 * @var string|NULL
+	 */
+	private $discoURL;
+
+
+	/**
+	 * Constructor for SAML SP authentication source.
+	 *
+	 * @param array $info  Information about this authentication source.
+	 * @param array $config  Configuration.
+	 */
+	public function __construct($info, $config) {
+		assert('is_array($info)');
+		assert('is_array($config)');
+
+		/* Call the parent constructor first, as required by the interface. */
+		parent::__construct($info, $config);
+
+		/* For compatibility with code that assumes that $metadata->getString('entityid') gives the entity id. */
+		if (array_key_exists('entityId', $config)) {
+			$config['entityid'] = $config['entityId'];
+		} else {
+			$config['entityid'] = SimpleSAML_Module::getModuleURL('saml/sp/metadata.php/' . urlencode($this->authId));
+		}
+
+		$this->metadata = SimpleSAML_Configuration::loadFromArray($config, 'authsources[' . var_export($this->authId, TRUE) . ']');
+		$this->entityId = $this->metadata->getString('entityid');
+		$this->idp = $this->metadata->getString('idp', NULL);
+		$this->discoURL = $this->metadata->getString('discoURL', NULL);
+	}
+
+
+	/**
+	 * Retrieve the entity id of this SP.
+	 *
+	 * @return string  The entity id of this SP.
+	 */
+	public function getEntityId() {
+
+		return $this->entityId;
+	}
+
+
+	/**
+	 * Retrieve the metadata of this SP.
+	 *
+	 * @return SimpleSAML_Configuration  The metadata of this SP.
+	 */
+	public function getMetadata() {
+
+		return $this->metadata;
+
+	}
+
+
+	/**
+	 * Retrieve the metadata of an IdP.
+	 *
+	 * @param string $entityId  The entity id of the IdP.
+	 * @return SimpleSAML_Configuration  The metadata of the IdP.
+	 */
+	public function getIdPMetadata($entityId) {
+		assert('is_string($entityId)');
+
+		if ($this->idp !== NULL && $this->idp !== $entityId) {
+			throw new SimpleSAML_Error_Exception('Cannot retrieve metadata for IdP ' . var_export($idp, TRUE) .
+				' because it isn\'t a valid IdP for this SP.');
+		}
+
+		$metadataHandler = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler();
+
+		/* First, look in saml20-idp-remote. */
+		try {
+			return $metadataHandler->getMetaDataConfig($entityId, 'saml20-idp-remote');
+		} catch (Exception $e) {
+			/* Metadata wasn't found. */
+		}
+
+
+		/* Not found in saml20-idp-remote, look in shib13-idp-remote. */
+		try {
+			return $metadataHandler->getMetaDataConfig($entityId, 'shib13-idp-remote');
+		} catch (Exception $e) {
+			/* Metadata wasn't found. */
+		}
+
+		/* Not found. */
+		throw new SimpleSAML_Error_Exception('Could not find the metadata of an IdP with entity ID ' . var_export($entityId, TRUE));
+	}
+
+
+	/**
+	 * Send a SAML1 SSO request to an IdP.
+	 *
+	 * @param SimpleSAML_Configuration $idpMetadata  The metadata of the IdP.
+	 * @param array $state  The state array for the current authentication.
+	 */
+	private function startSSO1(SimpleSAML_Configuration $idpMetadata, array $state) {
+
+		$idpEntityId = $idpMetadata->getString('entityid');
+
+		$ar = new SimpleSAML_XML_Shib13_AuthnRequest();
+		$ar->setIssuer($this->entityId);
+
+		$id = SimpleSAML_Auth_State::saveState($state, 'saml:sp:ssosent-saml1');
+		$ar->setRelayState($id);
+
+		$url = $ar->createRedirect($idpEntityId, SimpleSAML_Module::getModuleURL('saml/sp/saml1-acs.php'));
+
+		SimpleSAML_Logger::debug('Starting SAML 1 SSO to ' . var_export($idpEntityId, TRUE) .
+			' from ' . var_export($this->entityId, TRUE) . '.');
+		SimpleSAML_Utilities::redirect($url);
+	}
+
+
+	/**
+	 * Send a SAML2 SSO request to an IdP.
+	 *
+	 * @param SimpleSAML_Configuration $idpMetadata  The metadata of the IdP.
+	 * @param array $state  The state array for the current authentication.
+	 */
+	private function startSSO2(SimpleSAML_Configuration $idpMetadata, array $state) {
+
+		$ar = sspmod_saml2_Message::buildAuthnRequest($this->metadata, $idpMetadata);
+
+		$ar->setAssertionConsumerServiceURL(SimpleSAML_Module::getModuleURL('saml/sp/saml2-acs.php'));
+		$ar->setProtocolBinding(SAML2_Const::BINDING_HTTP_POST);
+
+		$id = SimpleSAML_Auth_State::saveState($state, 'saml:sp:ssosent-saml2');
+		$ar->setRelayState($id);
+
+		$b = new SAML2_HTTPRedirect();
+		$b->setDestination(sspmod_SAML2_Message::getDebugDestination());
+		$b->send($ar);
+
+		assert('FALSE');
+	}
+
+
+	/**
+	 * Send a SSO request to an IdP.
+	 *
+	 * @param string $idp  The entity ID of the IdP.
+	 * @param array $state  The state array for the current authentication.
+	 */
+	public function startSSO($idp, array $state) {
+		assert('is_string($idp)');
+
+		$idpMetadata = $this->getIdPMetadata($idp);
+
+		$type = $idpMetadata->getString('metadata-set');
+		switch ($type) {
+		case 'shib13-idp-remote':
+			$this->startSSO1($idpMetadata, $state);
+			assert('FALSE'); /* Should not return. */
+		case 'saml20-idp-remote':
+			$this->startSSO2($idpMetadata, $state);
+			assert('FALSE'); /* Should not return. */
+		default:
+			/* Should only be one of the known types. */
+			assert('FALSE');
+		}
+	}
+
+
+	/**
+	 * Start an IdP discovery service operation.
+	 *
+	 * @param array $state  The state array.
+	 */
+	private function startDisco(array $state) {
+
+		$id = SimpleSAML_Auth_State::saveState($state, 'saml:disco');
+
+		$config = SimpleSAML_Configuration::getInstance();
+
+		$discoURL = $this->discoURL;
+		if ($discoURL === NULL) {
+			/* Fallback to internal discovery service. */
+			$discoURL = SimpleSAML_Module::getModuleURL('saml/disco.php');
+		}
+
+		$returnTo = SimpleSAML_Module::getModuleURL('saml/sp/discoresp.php');
+		$returnTo = SimpleSAML_Utilities::addURLparameter($returnTo, array('AuthID' => $id));
+
+		SimpleSAML_Utilities::redirect($discoURL, array(
+			'entityID' => $this->entityId,
+			'return' => $returnTo,
+			'returnIDParam' => 'idpentityid')
+			);
+	}
+
+
+	/**
+	 * Start login.
+	 *
+	 * This function saves the information about the login, and redirects to the IdP.
+	 *
+	 * @param array &$state  Information about the current authentication.
+	 */
+	public function authenticate(&$state) {
+		assert('is_array($state)');
+
+		/* We are going to need the authId in order to retrieve this authentication source later. */
+		$state['saml:sp:AuthId'] = $this->authId;
+
+		if ($this->idp === NULL) {
+			$this->startDisco($state);
+			assert('FALSE');
+		}
+
+		$this->startSSO($this->idp, $state);
+		assert('FALSE');
+	}
+
+
+	/**
+	 * Start a SAML 2 logout operation.
+	 *
+	 * @param array $state  The logout state.
+	 */
+	public function startSLO2(&$state) {
+		assert('is_array($state)');
+		assert('array_key_exists("saml:logout:IdP", $state)');
+		assert('array_key_exists("saml:logout:NameID", $state)');
+		assert('array_key_exists("saml:logout:SessionIndex", $state)');
+
+		$id = SimpleSAML_Auth_State::saveState($state, 'saml:slosent');
+
+		$idp = $state['saml:logout:IdP'];
+		$nameId = $state['saml:logout:NameID'];
+		$sessionIndex = $state['saml:logout:SessionIndex'];
+
+		$idpMetadata = $this->getIdPMetadata($idp);
+
+		$lr = sspmod_saml2_Message::buildLogoutRequest($this->metadata, $idpMetadata);
+		$lr->setNameId($nameId);
+		$lr->setSessionIndex($sessionIndex);
+		$lr->setRelayState($id);
+
+		$b = new SAML2_HTTPRedirect();
+		$b->setDestination(sspmod_SAML2_Message::getDebugDestination());
+		$b->send($lr);
+
+		assert('FALSE');
+	}
+
+
+	/**
+	 * Start logout operation.
+	 *
+	 * @param array $state  The logout state.
+	 */
+	public function logout(&$state) {
+		assert('is_array($state)');
+		assert('array_key_exists("saml:logout:Type", $state)');
+
+		$logoutType = $state['saml:logout:Type'];
+		switch ($logoutType) {
+		case 'saml1':
+			/* Nothing to do. */
+			return;
+		case 'saml2':
+			$this->startSLO2($state);
+			assert('FALSE');
+		default:
+			/* Should never happen. */
+			assert('FALSE');
+		}
+	}
+
+
+	/**
+	 * Handle a response from a SSO operation.
+	 *
+	 * @param array $state  The authentication state.
+	 * @param string $idp  The entity id of the IdP.
+	 * @param array $attributes  The attributes.
+	 */
+	public function handleResponse(array $state, $idp, array $attributes) {
+		assert('is_string($idp)');
+		assert('array_key_exists("LogoutState", $state)');
+		assert('array_key_exists("saml:logout:Type", $state["LogoutState"])');
+
+		$idpMetadata = $this->getIdpMetadata($idp);
+
+		$spMetadataArray = $this->metadata->toArray();
+		$idpMetadataArray = $idpMetadata->toArray();
+
+		$authProcState = array(
+			'saml:sp:IdP' => $idp,
+			'saml:sp:State' => $state,
+			'ReturnCall' => array('sspmod_saml_Auth_Source_SP', 'onProcessingCompleted'),
+
+			'Attributes' => $attributes,
+			'Destination' => $spMetadataArray,
+			'Source' => $idpMetadataArray,
+		);
+
+		$pc = new SimpleSAML_Auth_ProcessingChain($idpMetadataArray, $spMetadataArray, 'sp');
+		$pc->processState($authProcState);
+
+		self::onProcessingCompleted($authProcState);
+	}
+
+
+	/**
+	 * Handle a logout request from an IdP.
+	 *
+	 * @param string $idpEntityId  The entity ID of the IdP.
+	 */
+	public function handleLogout($idpEntityId) {
+		assert('is_string($idpEntityId)');
+
+		/* Call the logout callback we registered in onProcessingCompleted(). */
+		$this->callLogoutCallback($idpEntityId);
+	}
+
+
+	/**
+	 * Called when we have completed the procssing chain.
+	 *
+	 * @param array $authProcState  The processing chain state.
+	 */
+	public static function onProcessingCompleted(array $authProcState) {
+		assert('array_key_exists("saml:sp:IdP", $authProcState)');
+		assert('array_key_exists("saml:sp:State", $authProcState)');
+		assert('array_key_exists("Attributes", $authProcState)');
+
+		$idp = $authProcState['saml:sp:IdP'];
+		$state = $authProcState['saml:sp:State'];
+
+		$sourceId = $state['saml:sp:AuthId'];
+		$source = SimpleSAML_Auth_Source::getById($sourceId);
+		if ($source === NULL) {
+			throw new Exception('Could not find authentication source with id ' . $sourceId);
+		}
+
+		/* Register a callback that we can call if we receive a logout request from the IdP. */
+		$source->addLogoutCallback($idp, $state);
+
+		$state['Attributes'] = $authProcState['Attributes'];
+		SimpleSAML_Auth_Source::completeAuth($state);
+	}
+
+}
\ No newline at end of file
diff --git a/modules/saml/www/disco.php b/modules/saml/www/disco.php
new file mode 100644
index 000000000..77c779979
--- /dev/null
+++ b/modules/saml/www/disco.php
@@ -0,0 +1,8 @@
+<?php
+
+/**
+ * Builtin IdP discovery service.
+ */
+
+$discoHandler = new SimpleSAML_XHTML_IdPDisco(array('saml20-idp-remote', 'shib13-idp-remote'), 'saml');
+$discoHandler->handleRequest();
diff --git a/modules/saml/www/sp/discoresp.php b/modules/saml/www/sp/discoresp.php
new file mode 100644
index 000000000..940d47bf1
--- /dev/null
+++ b/modules/saml/www/sp/discoresp.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * Handler for response from IdP discovery service.
+ */
+
+if (!array_key_exists('AuthID', $_REQUEST)) {
+	throw new SimpleSAML_Error_BadRequest('Missing AuthID to discovery service response handler');
+}
+
+if (!array_key_exists('idpentityid', $_REQUEST)) {
+	throw new SimpleSAML_Error_BadRequest('Missing idpentityid to discovery service response handler');
+}
+
+$state = SimpleSAML_Auth_State::loadState($_REQUEST['AuthID'], 'saml:disco');
+
+/* Find authentication source. */
+assert('array_key_exists("saml:sp:AuthId", $state)');
+$sourceId = $state['saml:sp:AuthId'];
+
+$source = SimpleSAML_Auth_Source::getById($sourceId);
+if ($source === NULL) {
+	throw new Exception('Could not find authentication source with id ' . $sourceId);
+}
+if (!($source instanceof sspmod_saml_Auth_Source_SP)) {
+	throw new SimpleSAML_Error_Exception('Source type changed?');
+}
+
+$source->startSSO($_REQUEST['idpentityid'], $state);
diff --git a/modules/saml/www/sp/metadata.php b/modules/saml/www/sp/metadata.php
new file mode 100644
index 000000000..47c8aaa99
--- /dev/null
+++ b/modules/saml/www/sp/metadata.php
@@ -0,0 +1,43 @@
+<?php
+
+
+if (!array_key_exists('PATH_INFO', $_SERVER)) {
+	throw new SimpleSAML_Error_BadRequest('Missing authentication source id in metadata URL');
+}
+
+$sourceId = substr($_SERVER['PATH_INFO'], 1);
+$source = SimpleSAML_Auth_Source::getById($sourceId);
+if ($source === NULL) {
+	throw new SimpleSAML_Error_NotFound('Could not find authentication source with id ' . $sourceId);
+}
+
+if (!($source instanceof sspmod_saml_Auth_Source_SP)) {
+	throw new SimpleSAML_Error_NotFound('Source isn\'t a SAML SP: ' . var_export($sourceId, TRUE));
+}
+
+$entityId = $source->getEntityId();
+
+$metaArray11 = array(
+	'AssertionConsumerService' => SimpleSAML_Module::getModuleURL('saml/sp/saml1-acs.php'),
+	);
+
+$metaArray20 = array(
+	'AssertionConsumerService' => SimpleSAML_Module::getModuleURL('saml/sp/saml2-acs.php'),
+	'SingleLogoutService' => SimpleSAML_Module::getModuleURL('saml/sp/saml2-logout.php/' . $sourceId),
+	);
+
+$metaBuilder = new SimpleSAML_Metadata_SAMLBuilder($entityId);
+$metaBuilder->addMetadataSP11($metaArray11);
+$metaBuilder->addMetadataSP20($metaArray20);
+
+$config = SimpleSAML_Configuration::getInstance();
+$metaBuilder->addContact('technical', array(
+	'emailAddress' => $config->getString('technicalcontact_email', NULL),
+	'name' => $config->getString('technicalcontact_name', NULL),
+	));
+
+$xml = $metaBuilder->getEntityDescriptorText();
+
+echo($xml);
+
+?>
diff --git a/modules/saml/www/sp/saml1-acs.php b/modules/saml/www/sp/saml1-acs.php
new file mode 100644
index 000000000..84cabd711
--- /dev/null
+++ b/modules/saml/www/sp/saml1-acs.php
@@ -0,0 +1,46 @@
+<?php
+
+if (!array_key_exists('SAMLResponse', $_REQUEST)) {
+	throw new SimpleSAML_Error_BadRequest('Missing SAMLResponse parameter.');
+}
+
+if (!array_key_exists('TARGET', $_REQUEST)) {
+	throw new SimpleSAML_Error_BadRequest('Missing TARGET parameter.');
+}
+
+
+$state = SimpleSAML_Auth_State::loadState($_REQUEST['TARGET'], 'saml:sp:ssosent-saml1');
+
+/* Find authentication source. */
+assert('array_key_exists("saml:sp:AuthId", $state)');
+$sourceId = $state['saml:sp:AuthId'];
+
+$source = SimpleSAML_Auth_Source::getById($sourceId);
+if ($source === NULL) {
+	throw new SimpleSAML_Error_Exception('Could not find authentication source with id ' . $sourceId);
+}
+if (!($source instanceof sspmod_saml_Auth_Source_SP)) {
+	throw new SimpleSAML_Error_Exception('Source type changed?');
+}
+
+
+$responseXML = $_REQUEST['SAMLResponse'];
+$responseXML = base64_decode($responseXML);
+
+$response = new SimpleSAML_XML_Shib13_AuthnResponse();
+$response->setXML($responseXML);
+
+$response->validate();
+
+$idp = $response->getIssuer();
+$attributes = $response->getAttributes();
+
+$logoutState = array(
+	'saml:logout:Type' => 'saml1'
+	);
+$state['LogoutState'] = $logoutState;
+
+$source->handleResponse($state, $idp, $attributes);
+assert('FALSE');
+
+?>
\ No newline at end of file
diff --git a/modules/saml/www/sp/saml2-acs.php b/modules/saml/www/sp/saml2-acs.php
new file mode 100644
index 000000000..df766fd5b
--- /dev/null
+++ b/modules/saml/www/sp/saml2-acs.php
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * Assertion consumer service handler for SAML 2.0 SP authentication client.
+ */
+
+$b = SAML2_Binding::getCurrentBinding();
+$response = $b->receive();
+if (!($response instanceof SAML2_Response)) {
+	throw new SimpleSAML_Error_BadRequest('Invalid message received to AssertionConsumerService endpoint.');
+}
+
+$relayState = $response->getRelayState();
+if (empty($relayState)) {
+	throw new SimpleSAML_Error_BadRequest('Missing relaystate in message received on AssertionConsumerService endpoint.');
+}
+
+$state = SimpleSAML_Auth_State::loadState($relayState, 'saml:sp:ssosent-saml2');
+
+/* Find authentication source. */
+assert('array_key_exists("saml:sp:AuthId", $state)');
+$sourceId = $state['saml:sp:AuthId'];
+
+$source = SimpleSAML_Auth_Source::getById($sourceId);
+if ($source === NULL) {
+	throw new Exception('Could not find authentication source with id ' . $sourceId);
+}
+if (!($source instanceof sspmod_saml_Auth_Source_SP)) {
+	throw new SimpleSAML_Error_Exception('Source type changed?');
+}
+
+
+$idp = $response->getIssuer();
+if ($idp === NULL) {
+	throw new Exception('Missing <saml:Issuer> in message delivered to AssertionConsumerService.');
+}
+
+$metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler();
+$idpMetadata = $source->getIdPmetadata($idp);
+$spMetadata = $source->getMetadata();
+
+try {
+	$assertion = sspmod_saml2_Message::processResponse($spMetadata, $idpMetadata, $response);
+} catch (sspmod_saml2_Error $e) {
+	/* The status of the response wasn't "success". */
+	$e = $e->toException();
+	SimpleSAML_Auth_State::throwException($state, $e);
+}
+
+$nameId = $assertion->getNameID();
+$sessionIndex = $assertion->getSessionIndex();
+
+/* We need to save the NameID and SessionIndex for logout. */
+$logoutState = array(
+	'saml:logout:Type' => 'saml2',
+	'saml:logout:IdP' => $idp,
+	'saml:logout:NameID' => $nameId,
+	'saml:logout:SessionIndex' => $sessionIndex,
+	);
+$state['LogoutState'] = $logoutState;
+
+$source->handleResponse($state, $idp, $assertion->getAttributes());
+assert('FALSE');
diff --git a/modules/saml/www/sp/saml2-logout.php b/modules/saml/www/sp/saml2-logout.php
new file mode 100644
index 000000000..0ec6704c4
--- /dev/null
+++ b/modules/saml/www/sp/saml2-logout.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * Logout endpoint handler for SAML SP authentication client.
+ *
+ * This endpoint handles both logout requests and logout responses.
+ */
+
+if (!array_key_exists('PATH_INFO', $_SERVER)) {
+	throw new SimpleSAML_Error_BadRequest('Missing authentication source id in logout URL');
+}
+
+$sourceId = substr($_SERVER['PATH_INFO'], 1);
+
+$source = SimpleSAML_Auth_Source::getById($sourceId);
+if ($source === NULL) {
+	throw new Exception('Could not find authentication source with id ' . $sourceId);
+}
+if (!($source instanceof sspmod_saml_Auth_Source_SP)) {
+	throw new SimpleSAML_Error_Exception('Source type changed?');
+}
+
+$binding = SAML2_Binding::getCurrentBinding();
+$message = $binding->receive();
+
+$idpEntityId = $message->getIssuer();
+if ($idpEntityId === NULL) {
+	/* Without an issuer we have no way to respond to the message. */
+	throw new SimpleSAML_Error_BadRequest('Received message on logout endpoint without issuer.');
+}
+
+$spEntityId = $source->getEntityId();
+
+$metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler();
+$idpMetadata = $source->getIdPMetadata($idpEntityId);
+$spMetadata = $source->getMetadata();
+
+sspmod_saml2_Message::validateMessage($idpMetadata, $spMetadata, $message);
+
+if ($message instanceof SAML2_LogoutResponse) {
+
+	$relayState = $message->getRelayState();
+	if ($relayState === NULL) {
+		/* Somehow, our RelayState has been lost. */
+		throw new SimpleSAML_Error_BadRequest('Missing RelayState in logout response.');
+	}
+
+	if (!$message->isSuccess()) {
+		SimpleSAML_Logger::warning('Unsuccessful logout. Status was: ' . sspmod_saml2_Message::getResponseError($message));
+	}
+
+	$state = SimpleSAML_Auth_State::loadState($relayState, 'saml:slosent');
+	SimpleSAML_Auth_Source::completeLogout($state);
+
+} elseif ($message instanceof SAML2_LogoutRequest) {
+
+	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);
+
+	/* Create an send response. */
+	$lr = sspmod_saml2_Message::buildLogoutResponse($spMetadata, $idpMetadata);
+	$lr->setRelayState($message->getRelayState());
+	$lr->setInResponseTo($message->getId());
+
+	$binding = new SAML2_HTTPRedirect();
+	$binding->setDestination(sspmod_SAML2_Message::getDebugDestination());
+	$binding->send($lr);
+} else {
+	throw new SimpleSAML_Error_BadRequest('Unknown message received on logout endpoint: ' . get_class($message));
+}
-- 
GitLab