diff --git a/lib/SimpleSAML/XHTML/IdPDisco.php b/lib/SimpleSAML/XHTML/IdPDisco.php index 96b76267b5c8ea522665c26a50899be246b2108e..d84fcec45cc14961678a17e71353750ed31838f6 100644 --- a/lib/SimpleSAML/XHTML/IdPDisco.php +++ b/lib/SimpleSAML/XHTML/IdPDisco.php @@ -1,5 +1,6 @@ <?php + /** * This class implements a generic IdP discovery service, for use in various IdP * discovery service pages. This should reduce code duplication. @@ -12,560 +13,580 @@ * @author Andreas Ă…kre Solberg <andreas@uninett.no>, UNINETT AS. * @package SimpleSAMLphp */ -class SimpleSAML_XHTML_IdPDisco { - - /** - * An instance of the configuration class. - * - * @var SimpleSAML_Configuration - */ - protected $config; - - /** - * The identifier of this discovery service. - * - * @var string - */ - protected $instance; - - - /** - * An instance of the metadata handler, which will allow us to fetch metadata about IdPs. - * - * @var SimpleSAML_Metadata_MetaDataStorageHandler - */ - protected $metadata; - - - /** - * The users session. - * - * @var SimpleSAML_Session - */ - protected $session; - - - /** - * The metadata sets we find allowed entities in, in prioritized order. - * - * @var array - */ - protected $metadataSets; - - - /** - * The entity id of the SP which accesses this IdP discovery service. - * - * @var string - */ - protected $spEntityId; - - /** - * HTTP parameter from the request, indicating whether the discovery service - * can interact with the user or not. - * - * @var boolean - */ - protected $isPassive; - - /** - * The SP request to set the IdPentityID... - * - * @var string|null - */ - protected $setIdPentityID = NULL; - - - /** - * The name of the query parameter which should contain the users choice of IdP. - * This option default to 'entityID' for Shibboleth compatibility. - * - * @var string - */ - protected $returnIdParam; - - /** - * The list of scoped idp's. The intersection between the metadata idpList - * and scopedIDPList (given as a $_GET IDPList[] parameter) is presented to - * the user. If the intersection is empty the metadata idpList is used. - * - * @var array - */ - protected $scopedIDPList = array(); - - /** - * The URL the user should be redirected to after choosing an IdP. - * - * @var string - */ - protected $returnURL; - - - /** - * Initializes this discovery service. - * - * The constructor does the parsing of the request. If this is an invalid request, it will throw an exception. - * - * @param array $metadataSets Array with metadata sets we find remote entities in. - * @param string $instance The name of this instance of the discovery service. - * - * @throws Exception If the request is invalid. - */ - public function __construct(array $metadataSets, $instance) { - assert('is_string($instance)'); - - /* Initialize standard classes. */ - $this->config = SimpleSAML_Configuration::getInstance(); - $this->metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); - $this->session = SimpleSAML_Session::getSessionFromRequest(); - $this->instance = $instance; - $this->metadataSets = $metadataSets; - - $this->log('Accessing discovery service.'); - - - /* Standard discovery service parameters. */ - - if(!array_key_exists('entityID', $_GET)) { - throw new Exception('Missing parameter: entityID'); - } else { - $this->spEntityId = $_GET['entityID']; - } - - if(!array_key_exists('returnIDParam', $_GET)) { - $this->returnIdParam = 'entityID'; - } else { - $this->returnIdParam = $_GET['returnIDParam']; - } - - $this->log('returnIdParam initially set to [' . $this->returnIdParam . ']'); - - if(!array_key_exists('return', $_GET)) { - throw new Exception('Missing parameter: return'); - } else { - $this->returnURL = \SimpleSAML\Utils\HTTP::checkURLAllowed($_GET['return']); - } - - $this->isPassive = FALSE; - if (array_key_exists('isPassive', $_GET)) { - if ($_GET['isPassive'] === 'true') $this->isPassive = TRUE; - } - $this->log('isPassive initially set to [' . ($this->isPassive ? 'TRUE' : 'FALSE' ) . ']'); - - if (array_key_exists('IdPentityID', $_GET)) { - $this->setIdPentityID = $_GET['IdPentityID']; - } else { - $this->setIdPentityID = NULL; - } - - if (array_key_exists('IDPList', $_REQUEST)) { - $this->scopedIDPList = $_REQUEST['IDPList']; - } - - } - - - /** - * Log a message. - * - * This is an helper function for logging messages. It will prefix the messages with our - * discovery service type. - * - * @param string $message The message which should be logged. - */ - protected function log($message) { - SimpleSAML_Logger::info('idpDisco.' . $this->instance . ': ' . $message); - } - - - /** - * Retrieve cookie with the given name. - * - * This function will retrieve a cookie with the given name for the current discovery - * service type. - * - * @param string $name The name of the cookie. - * @return string The value of the cookie with the given name, or NULL if no cookie with that name exists. - */ - protected function getCookie($name) { - $prefixedName = 'idpdisco_' . $this->instance . '_' . $name; - if(array_key_exists($prefixedName, $_COOKIE)) { - return $_COOKIE[$prefixedName]; - } else { - return NULL; - } - } - - - /** - * Save cookie with the given name and value. - * - * This function will save a cookie with the given name and value for the current discovery - * service type. - * - * @param string $name The name of the cookie. - * @param string $value The value of the cookie. - */ - protected function setCookie($name, $value) { - $prefixedName = 'idpdisco_' . $this->instance . '_' . $name; - - $params = array( - /* We save the cookies for 90 days. */ - 'lifetime' => (60*60*24*90), - /* The base path for cookies. This should be the installation directory for simpleSAMLphp. */ - 'path' => ('/' . $this->config->getBaseUrl()), - 'httponly' => FALSE, - ); - - \SimpleSAML\Utils\HTTP::setCookie($prefixedName, $value, $params, FALSE); - } - - - /** - * Validates the given IdP entity id. - * - * Takes a string with the IdP entity id, and returns the entity id if it is valid, or - * NULL if not. - * - * @param string|null $idp The entity id we want to validate. This can be NULL, in which case we will return NULL. - * @return string|null The entity id if it is valid, NULL if not. - */ - protected function validateIdP($idp) { - if($idp === NULL) { - return NULL; - } - - if(!$this->config->getBoolean('idpdisco.validate', TRUE)) { - return $idp; - } - - foreach ($this->metadataSets AS $metadataSet) { - try { - $this->metadata->getMetaData($idp, $metadataSet); - return $idp; - } catch(Exception $e) { } - } - - $this->log('Unable to validate IdP entity id [' . $idp . '].'); - /* The entity id wasn't valid. */ - return NULL; - } - - - /** - * Retrieve the users choice of IdP. - * - * This function finds out which IdP the user has manually chosen, if any. - * - * @return string The entity id of the IdP the user has chosen, or NULL if the user has made no choice. - */ - protected function getSelectedIdP() { - - - /* Parameter set from the Extended IdP Metadata Discovery Service Protocol, - * indicating that the user prefers this IdP. - */ - if ($this->setIdPentityID) { - return $this->validateIdP($this->setIdPentityID); - } - - /* User has clicked on a link, or selected the IdP from a dropdown list. */ - if(array_key_exists('idpentityid', $_GET)) { - return $this->validateIdP($_GET['idpentityid']); - } - - /* Search for the IdP selection from the form used by the links view. - * This form uses a name which equals idp_<entityid>, so we search for that. - * - * Unfortunately, php replaces periods in the name with underscores, and there - * is no reliable way to get them back. Therefore we do some quick and dirty - * parsing of the query string. - */ - $qstr = $_SERVER['QUERY_STRING']; - $matches = array(); - if(preg_match('/(?:^|&)idp_([^=]+)=/', $qstr, $matches)) { - return $this->validateIdP(urldecode($matches[1])); - } - - /* No IdP chosen. */ - return NULL; - } - - - /** - * Retrieve the users saved choice of IdP. - * - * @return string The entity id of the IdP the user has saved, or NULL if the user hasn't saved any choice. - */ - protected function getSavedIdP() { - if(!$this->config->getBoolean('idpdisco.enableremember', FALSE)) { - /* Saving of IdP choices is disabled. */ - return NULL; - } - - if($this->getCookie('remember') === '1') { - $this->log('Return previously saved IdP because of remember cookie set to 1'); - return $this->getPreviousIdP(); - } - - if( $this->isPassive) { - $this->log('Return previously saved IdP because of isPassive'); - return $this->getPreviousIdP(); - } - - return NULL; - } - - - /** - * Retrieve the previous IdP the user used. - * - * @return string The entity id of the previous IdP the user used, or NULL if this is the first time. - */ - protected function getPreviousIdP() { - return $this->validateIdP($this->getCookie('lastidp')); - } - - - /** - * Retrieve a recommended IdP based on the IP address of the client. - * - * @return string|NULL The entity ID of the IdP if one is found, or NULL if not. - */ - protected function getFromCIDRhint() { - - foreach ($this->metadataSets as $metadataSet) { - $idp = $this->metadata->getPreferredEntityIdFromCIDRhint($metadataSet, $_SERVER['REMOTE_ADDR']); - if (!empty($idp)) { - return $idp; - } - } - - return NULL; - } - - - /** - * Try to determine which IdP the user should most likely use. - * - * This function will first look at the previous IdP the user has chosen. If the user - * hasn't chosen an IdP before, it will look at the IP address. - * - * @return string The entity id of the IdP the user should most likely use. - */ - protected function getRecommendedIdP() { - - $idp = $this->getPreviousIdP(); - if($idp !== NULL) { - $this->log('Preferred IdP from previous use [' . $idp . '].'); - return $idp; - } - - $idp = $this->getFromCIDRhint(); - - if(!empty($idp)) { - $this->log('Preferred IdP from CIDR hint [' . $idp . '].'); - return $idp; - } - - return NULL; - } - - - /** - * Save the current IdP choice to a cookie. - * - * @param string $idp The entityID of the IdP. - */ - protected function setPreviousIdP($idp) { - assert('is_string($idp)'); - - $this->log('Choice made [' . $idp . '] Setting cookie.'); - $this->setCookie('lastidp', $idp); - } - - - /** - * Determine whether the choice of IdP should be saved. - * - * @return boolean True if the choice should be saved, false otherwise. - */ - protected function saveIdP() { - if(!$this->config->getBoolean('idpdisco.enableremember', FALSE)) { - /* Saving of IdP choices is disabled. */ - return FALSE; - } - - if(array_key_exists('remember', $_GET)) { - return TRUE; - } - } - - - /** - * Determine which IdP the user should go to, if any. - * - * @return string The entity id of the IdP the user should be sent to, or NULL if the user should choose. - */ - protected function getTargetIdP() { - - /* First, check if the user has chosen an IdP. */ - $idp = $this->getSelectedIdP(); - if($idp !== NULL) { - /* The user selected this IdP. Save the choice in a cookie. */ - $this->setPreviousIdP($idp); - - if($this->saveIdP()) { - $this->setCookie('remember', '1'); - } else { - $this->setCookie('remember', '0'); - } - - return $idp; - } - - $this->log('getSelectedIdP() returned NULL'); - - /* Check if the user has saved an choice earlier. */ - $idp = $this->getSavedIdP(); - if($idp !== NULL) { - $this->log('Using saved choice [' . $idp . '].'); - return $idp; - } - - /* The user has made no choice. */ - return NULL; - } - - - /** - * Retrieve the list of IdPs which are stored in the metadata. - * - * @return array An array with entityid => metadata mappings. - */ - protected function getIdPList() { - - $idpList = array(); - foreach ($this->metadataSets AS $metadataSet) { - $newList = $this->metadata->getList($metadataSet); - /* - * Note that we merge the entities in reverse order. This ensuers - * that it is the entity in the first metadata set that "wins" if - * two metadata sets have the same entity. - */ - $idpList = array_merge($newList, $idpList); - } - - return $idpList; - } - - /** - * Return the list of scoped idp - * - * @return array An array of IdP entities - */ - protected function getScopedIDPList() { - return $this->scopedIDPList; - } - - - /** - * Filter the list of IdPs. - * - * This method returns the IdPs that comply with the following conditions: - * - The IdP does not have the 'hide.from.discovery' configuration option. - * - * @param array $list An associative array containing metadata for the IdPs to apply the filtering to. - * - * @return array An associative array containing metadata for the IdPs that were not filtered out. - */ - protected function filter($list) - { - foreach ($list as $entity => $metadata) { - if (array_key_exists('hide.from.discovery', $metadata) && $metadata['hide.from.discovery'] === true) { - unset($list[$entity]); - } - } - return $list; - } - - - /** - * Handles a request to this discovery service. - * - * The IdP disco parameters should be set before calling this function. - */ - public function handleRequest() { - - $idp = $this->getTargetIdp(); - if($idp !== NULL) { - - $extDiscoveryStorage = $this->config->getString('idpdisco.extDiscoveryStorage', NULL); - if ($extDiscoveryStorage !== NULL) { - $this->log('Choice made [' . $idp . '] (Forwarding to external discovery storage)'); - \SimpleSAML\Utils\HTTP::redirectTrustedURL($extDiscoveryStorage, array( - 'entityID' => $this->spEntityId, - 'IdPentityID' => $idp, - 'returnIDParam' => $this->returnIdParam, - 'isPassive' => 'true', - 'return' => $this->returnURL - )); - - } else { - $this->log('Choice made [' . $idp . '] (Redirecting the user back. returnIDParam=' . $this->returnIdParam . ')'); - \SimpleSAML\Utils\HTTP::redirectTrustedURL($this->returnURL, array($this->returnIdParam => $idp)); - } - - return; - } - - if ($this->isPassive) { - $this->log('Choice not made. (Redirecting the user back without answer)'); - \SimpleSAML\Utils\HTTP::redirectTrustedURL($this->returnURL); - return; - } - - /* No choice made. Show discovery service page. */ - - $idpList = $this->getIdPList(); - $idpList = $this->filter($idpList); - $preferredIdP = $this->getRecommendedIdP(); - - $idpintersection = array_intersect(array_keys($idpList), $this->getScopedIDPList()); - if (sizeof($idpintersection) > 0) { - $idpList = array_intersect_key($idpList, array_fill_keys($idpintersection, NULL)); - } - - $idpintersection = array_values($idpintersection); - - if(sizeof($idpintersection) == 1) { - $this->log('Choice made [' . $idpintersection[0] . '] (Redirecting the user back. returnIDParam=' . $this->returnIdParam . ')'); - \SimpleSAML\Utils\HTTP::redirectTrustedURL($this->returnURL, array($this->returnIdParam => $idpintersection[0])); - } - - /* - * Make use of an XHTML template to present the select IdP choice to the user. - * Currently the supported options is either a drop down menu or a list view. - */ - switch($this->config->getString('idpdisco.layout', 'links')) { - case 'dropdown': - $templateFile = 'selectidp-dropdown.php'; - break; - case 'links': - $templateFile = 'selectidp-links.php'; - break; - default: - throw new Exception('Invalid value for the \'idpdisco.layout\' option.'); - } - - $t = new SimpleSAML_XHTML_Template($this->config, $templateFile, 'disco'); - $t->data['idplist'] = $idpList; - $t->data['preferredidp'] = $preferredIdP; - $t->data['return'] = $this->returnURL; - $t->data['returnIDParam'] = $this->returnIdParam; - $t->data['entityID'] = $this->spEntityId; - $t->data['urlpattern'] = htmlspecialchars(\SimpleSAML\Utils\HTTP::getSelfURLNoQuery()); - $t->data['rememberenabled'] = $this->config->getBoolean('idpdisco.enableremember', FALSE); - $t->show(); - } +class SimpleSAML_XHTML_IdPDisco +{ + + /** + * An instance of the configuration class. + * + * @var SimpleSAML_Configuration + */ + protected $config; + + /** + * The identifier of this discovery service. + * + * @var string + */ + protected $instance; + + + /** + * An instance of the metadata handler, which will allow us to fetch metadata about IdPs. + * + * @var SimpleSAML_Metadata_MetaDataStorageHandler + */ + protected $metadata; + + + /** + * The users session. + * + * @var SimpleSAML_Session + */ + protected $session; + + + /** + * The metadata sets we find allowed entities in, in prioritized order. + * + * @var array + */ + protected $metadataSets; + + + /** + * The entity id of the SP which accesses this IdP discovery service. + * + * @var string + */ + protected $spEntityId; + + /** + * HTTP parameter from the request, indicating whether the discovery service + * can interact with the user or not. + * + * @var boolean + */ + protected $isPassive; + + /** + * The SP request to set the IdPentityID... + * + * @var string|null + */ + protected $setIdPentityID = null; + + + /** + * The name of the query parameter which should contain the users choice of IdP. + * This option default to 'entityID' for Shibboleth compatibility. + * + * @var string + */ + protected $returnIdParam; + + /** + * The list of scoped idp's. The intersection between the metadata idpList + * and scopedIDPList (given as a $_GET IDPList[] parameter) is presented to + * the user. If the intersection is empty the metadata idpList is used. + * + * @var array + */ + protected $scopedIDPList = array(); + + /** + * The URL the user should be redirected to after choosing an IdP. + * + * @var string + */ + protected $returnURL; + + + /** + * Initializes this discovery service. + * + * The constructor does the parsing of the request. If this is an invalid request, it will throw an exception. + * + * @param array $metadataSets Array with metadata sets we find remote entities in. + * @param string $instance The name of this instance of the discovery service. + * + * @throws Exception If the request is invalid. + */ + public function __construct(array $metadataSets, $instance) + { + assert('is_string($instance)'); + + // initialize standard classes + $this->config = SimpleSAML_Configuration::getInstance(); + $this->metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); + $this->session = SimpleSAML_Session::getSessionFromRequest(); + $this->instance = $instance; + $this->metadataSets = $metadataSets; + + $this->log('Accessing discovery service.'); + + // standard discovery service parameters + if (!array_key_exists('entityID', $_GET)) { + throw new Exception('Missing parameter: entityID'); + } else { + $this->spEntityId = $_GET['entityID']; + } + + if (!array_key_exists('returnIDParam', $_GET)) { + $this->returnIdParam = 'entityID'; + } else { + $this->returnIdParam = $_GET['returnIDParam']; + } + + $this->log('returnIdParam initially set to ['.$this->returnIdParam.']'); + + if (!array_key_exists('return', $_GET)) { + throw new Exception('Missing parameter: return'); + } else { + $this->returnURL = \SimpleSAML\Utils\HTTP::checkURLAllowed($_GET['return']); + } + + $this->isPassive = false; + if (array_key_exists('isPassive', $_GET)) { + if ($_GET['isPassive'] === 'true') { + $this->isPassive = true; + } + } + $this->log('isPassive initially set to ['.($this->isPassive ? 'TRUE' : 'FALSE').']'); + + if (array_key_exists('IdPentityID', $_GET)) { + $this->setIdPentityID = $_GET['IdPentityID']; + } else { + $this->setIdPentityID = null; + } + + if (array_key_exists('IDPList', $_REQUEST)) { + $this->scopedIDPList = $_REQUEST['IDPList']; + } + } + + + /** + * Log a message. + * + * This is an helper function for logging messages. It will prefix the messages with our + * discovery service type. + * + * @param string $message The message which should be logged. + */ + protected function log($message) + { + SimpleSAML_Logger::info('idpDisco.'.$this->instance.': '.$message); + } + + + /** + * Retrieve cookie with the given name. + * + * This function will retrieve a cookie with the given name for the current discovery + * service type. + * + * @param string $name The name of the cookie. + * + * @return string The value of the cookie with the given name, or null if no cookie with that name exists. + */ + protected function getCookie($name) + { + $prefixedName = 'idpdisco_'.$this->instance.'_'.$name; + if (array_key_exists($prefixedName, $_COOKIE)) { + return $_COOKIE[$prefixedName]; + } else { + return null; + } + } + + + /** + * Save cookie with the given name and value. + * + * This function will save a cookie with the given name and value for the current discovery + * service type. + * + * @param string $name The name of the cookie. + * @param string $value The value of the cookie. + */ + protected function setCookie($name, $value) + { + $prefixedName = 'idpdisco_'.$this->instance.'_'.$name; + + $params = array( + // we save the cookies for 90 days + 'lifetime' => (60 * 60 * 24 * 90), + // the base path for cookies. This should be the installation directory for SimpleSAMLphp + 'path' => ('/'.$this->config->getBaseUrl()), + 'httponly' => false, + ); + + \SimpleSAML\Utils\HTTP::setCookie($prefixedName, $value, $params, false); + } + + + /** + * Validates the given IdP entity id. + * + * Takes a string with the IdP entity id, and returns the entity id if it is valid, or + * null if not. + * + * @param string|null $idp The entity id we want to validate. This can be null, in which case we will return null. + * + * @return string|null The entity id if it is valid, null if not. + */ + protected function validateIdP($idp) + { + if ($idp === null) { + return null; + } + + if (!$this->config->getBoolean('idpdisco.validate', true)) { + return $idp; + } + + foreach ($this->metadataSets as $metadataSet) { + try { + $this->metadata->getMetaData($idp, $metadataSet); + return $idp; + } catch (Exception $e) { + } + } + + $this->log('Unable to validate IdP entity id ['.$idp.'].'); + + // the entity id wasn't valid + return null; + } + + + /** + * Retrieve the users choice of IdP. + * + * This function finds out which IdP the user has manually chosen, if any. + * + * @return string The entity id of the IdP the user has chosen, or null if the user has made no choice. + */ + protected function getSelectedIdP() + { + /* Parameter set from the Extended IdP Metadata Discovery Service Protocol, indicating that the user prefers + * this IdP. + */ + if ($this->setIdPentityID) { + return $this->validateIdP($this->setIdPentityID); + } + + // user has clicked on a link, or selected the IdP from a drop-down list + if (array_key_exists('idpentityid', $_GET)) { + return $this->validateIdP($_GET['idpentityid']); + } + + /* Search for the IdP selection from the form used by the links view. This form uses a name which equals + * idp_<entityid>, so we search for that. + * + * Unfortunately, php replaces periods in the name with underscores, and there is no reliable way to get them + * back. Therefore we do some quick and dirty parsing of the query string. + */ + $qstr = $_SERVER['QUERY_STRING']; + $matches = array(); + if (preg_match('/(?:^|&)idp_([^=]+)=/', $qstr, $matches)) { + return $this->validateIdP(urldecode($matches[1])); + } + + // no IdP chosen + return null; + } + + + /** + * Retrieve the users saved choice of IdP. + * + * @return string The entity id of the IdP the user has saved, or null if the user hasn't saved any choice. + */ + protected function getSavedIdP() + { + if (!$this->config->getBoolean('idpdisco.enableremember', false)) { + // saving of IdP choices is disabled + return null; + } + + if ($this->getCookie('remember') === '1') { + $this->log('Return previously saved IdP because of remember cookie set to 1'); + return $this->getPreviousIdP(); + } + + if ($this->isPassive) { + $this->log('Return previously saved IdP because of isPassive'); + return $this->getPreviousIdP(); + } + + return null; + } + + + /** + * Retrieve the previous IdP the user used. + * + * @return string The entity id of the previous IdP the user used, or null if this is the first time. + */ + protected function getPreviousIdP() + { + return $this->validateIdP($this->getCookie('lastidp')); + } + + + /** + * Retrieve a recommended IdP based on the IP address of the client. + * + * @return string|null The entity ID of the IdP if one is found, or null if not. + */ + protected function getFromCIDRhint() + { + foreach ($this->metadataSets as $metadataSet) { + $idp = $this->metadata->getPreferredEntityIdFromCIDRhint($metadataSet, $_SERVER['REMOTE_ADDR']); + if (!empty($idp)) { + return $idp; + } + } + + return null; + } + + + /** + * Try to determine which IdP the user should most likely use. + * + * This function will first look at the previous IdP the user has chosen. If the user + * hasn't chosen an IdP before, it will look at the IP address. + * + * @return string The entity id of the IdP the user should most likely use. + */ + protected function getRecommendedIdP() + { + $idp = $this->getPreviousIdP(); + if ($idp !== null) { + $this->log('Preferred IdP from previous use ['.$idp.'].'); + return $idp; + } + + $idp = $this->getFromCIDRhint(); + + if (!empty($idp)) { + $this->log('Preferred IdP from CIDR hint ['.$idp.'].'); + return $idp; + } + + return null; + } + + + /** + * Save the current IdP choice to a cookie. + * + * @param string $idp The entityID of the IdP. + */ + protected function setPreviousIdP($idp) + { + assert('is_string($idp)'); + + $this->log('Choice made ['.$idp.'] Setting cookie.'); + $this->setCookie('lastidp', $idp); + } + + + /** + * Determine whether the choice of IdP should be saved. + * + * @return boolean True if the choice should be saved, false otherwise. + */ + protected function saveIdP() + { + if (!$this->config->getBoolean('idpdisco.enableremember', false)) { + // saving of IdP choices is disabled + return false; + } + + if (array_key_exists('remember', $_GET)) { + return true; + } + + return false; + } + + + /** + * Determine which IdP the user should go to, if any. + * + * @return string The entity id of the IdP the user should be sent to, or null if the user should choose. + */ + protected function getTargetIdP() + { + // first, check if the user has chosen an IdP + $idp = $this->getSelectedIdP(); + if ($idp !== null) { + // the user selected this IdP. Save the choice in a cookie + $this->setPreviousIdP($idp); + + if ($this->saveIdP()) { + $this->setCookie('remember', '1'); + } else { + $this->setCookie('remember', '0'); + } + + return $idp; + } + + $this->log('getSelectedIdP() returned null'); + + // check if the user has saved an choice earlier + $idp = $this->getSavedIdP(); + if ($idp !== null) { + $this->log('Using saved choice ['.$idp.'].'); + return $idp; + } + + // the user has made no choice + return null; + } + + + /** + * Retrieve the list of IdPs which are stored in the metadata. + * + * @return array An array with entityid => metadata mappings. + */ + protected function getIdPList() + { + $idpList = array(); + foreach ($this->metadataSets as $metadataSet) { + $newList = $this->metadata->getList($metadataSet); + /* + * Note that we merge the entities in reverse order. This ensures that it is the entity in the first + * metadata set that "wins" if two metadata sets have the same entity. + */ + $idpList = array_merge($newList, $idpList); + } + + return $idpList; + } + + + /** + * Return the list of scoped idp + * + * @return array An array of IdP entities + */ + protected function getScopedIDPList() + { + return $this->scopedIDPList; + } + + + /** + * Filter the list of IdPs. + * + * This method returns the IdPs that comply with the following conditions: + * - The IdP does not have the 'hide.from.discovery' configuration option. + * + * @param array $list An associative array containing metadata for the IdPs to apply the filtering to. + * + * @return array An associative array containing metadata for the IdPs that were not filtered out. + */ + protected function filter($list) + { + foreach ($list as $entity => $metadata) { + if (array_key_exists('hide.from.discovery', $metadata) && $metadata['hide.from.discovery'] === true) { + unset($list[$entity]); + } + } + return $list; + } + + + /** + * Handles a request to this discovery service. + * + * The IdP disco parameters should be set before calling this function. + */ + public function handleRequest() + { + $idp = $this->getTargetIdp(); + if ($idp !== null) { + + $extDiscoveryStorage = $this->config->getString('idpdisco.extDiscoveryStorage', null); + if ($extDiscoveryStorage !== null) { + $this->log('Choice made ['.$idp.'] (Forwarding to external discovery storage)'); + \SimpleSAML\Utils\HTTP::redirectTrustedURL($extDiscoveryStorage, array( + 'entityID' => $this->spEntityId, + 'IdPentityID' => $idp, + 'returnIDParam' => $this->returnIdParam, + 'isPassive' => 'true', + 'return' => $this->returnURL + )); + } else { + $this->log( + 'Choice made ['.$idp.'] (Redirecting the user back. returnIDParam='.$this->returnIdParam.')' + ); + \SimpleSAML\Utils\HTTP::redirectTrustedURL($this->returnURL, array($this->returnIdParam => $idp)); + } + + return; + } + + if ($this->isPassive) { + $this->log('Choice not made. (Redirecting the user back without answer)'); + \SimpleSAML\Utils\HTTP::redirectTrustedURL($this->returnURL); + return; + } + + // no choice made. Show discovery service page + $idpList = $this->getIdPList(); + $idpList = $this->filter($idpList); + $preferredIdP = $this->getRecommendedIdP(); + + $idpintersection = array_intersect(array_keys($idpList), $this->getScopedIDPList()); + if (sizeof($idpintersection) > 0) { + $idpList = array_intersect_key($idpList, array_fill_keys($idpintersection, null)); + } + + $idpintersection = array_values($idpintersection); + + if (sizeof($idpintersection) == 1) { + $this->log( + 'Choice made ['.$idpintersection[0].'] (Redirecting the user back. returnIDParam='. + $this->returnIdParam.')' + ); + \SimpleSAML\Utils\HTTP::redirectTrustedURL( + $this->returnURL, + array($this->returnIdParam => $idpintersection[0]) + ); + } + + /* + * Make use of an XHTML template to present the select IdP choice to the user. Currently the supported options + * is either a drop down menu or a list view. + */ + switch ($this->config->getString('idpdisco.layout', 'links')) { + case 'dropdown': + $templateFile = 'selectidp-dropdown.php'; + break; + case 'links': + $templateFile = 'selectidp-links.php'; + break; + default: + throw new Exception('Invalid value for the \'idpdisco.layout\' option.'); + } + + $t = new SimpleSAML_XHTML_Template($this->config, $templateFile, 'disco'); + $t->data['idplist'] = $idpList; + $t->data['preferredidp'] = $preferredIdP; + $t->data['return'] = $this->returnURL; + $t->data['returnIDParam'] = $this->returnIdParam; + $t->data['entityID'] = $this->spEntityId; + $t->data['urlpattern'] = htmlspecialchars(\SimpleSAML\Utils\HTTP::getSelfURLNoQuery()); + $t->data['rememberenabled'] = $this->config->getBoolean('idpdisco.enableremember', false); + $t->show(); + } }