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