diff --git a/lib/SimpleSAML/XHTML/IdPDisco.php b/lib/SimpleSAML/XHTML/IdPDisco.php new file mode 100644 index 0000000000000000000000000000000000000000..bd9e13484e2279709131f43646c623ef2642bac9 --- /dev/null +++ b/lib/SimpleSAML/XHTML/IdPDisco.php @@ -0,0 +1,383 @@ +<?php + +/** + * This class implements a generic IdP discovery service, for use in various IdP + * discovery service pages. This should reduce code duplication. + * + * @author Olav Morken, UNINETT AS. + * @package simpleSAMLphp + * @version $Id$ + */ +class SimpleSAML_XHTML_IdPDisco { + + /** + * The various discovery services we can host. + */ + private static $discoTypes = array( + 'saml20' => array( + 'metadata' => 'saml20-idp-remote', + ), + 'shib13' => array( + 'metadata' => 'shib13-idp-remote', + ), + ); + + + /** + * An instance of the configuration class. + */ + private $config; + + + /** + * An instance of the metadata handler, which will allow us to fetch metadata about IdPs. + */ + private $metadata; + + + /** + * The users session. + */ + private $session; + + + /** + * Our discovery service type. + */ + private $discoType; + + + /** + * The entity id of the SP which accesses this IdP discovery service. + */ + private $spEntityId; + + + /** + * The name of the query parameter which should contain the users choice of IdP. + * This option default to 'entityID' for Shibboleth compatibility. + */ + private $returnIdParam; + + + /** + * The URL the user should be redirected to after choosing an IdP. + */ + private $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 $discoType String which identifies the type of discovery service. + */ + public function __construct($discoType) { + + /* Initialize standard classes. */ + $this->config = SimpleSAML_Configuration::getInstance(); + $this->metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); + $this->session = SimpleSAML_Session::getInstance(); + + + if(!array_key_exists($discoType, self::$discoTypes)) { + throw new Exception('Unknown discovery service type: ' . $discoType); + } + + $this->discoType = self::$discoTypes[$discoType]; + $this->discoType['type'] = $discoType; + + $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']; + } + + if(!array_key_exists('return', $_GET)) { + throw new Exception('Missing parameter: return'); + } else { + $this->returnURL = $_GET['return']; + } + + } + + + /** + * Log a message. + * + * This is an helper function for logging messages. It will prefix the messages with our + * discovery service type. + * + * @param $message The message which should be logged. + */ + private function log($message) { + SimpleSAML_Logger::info('idpDisco.' . $this->discoType['type'] . ': ' . $message); + } + + + /** + * Retrieve cookie with the given name. + * + * This function will retrieve a cookie with the given name for the current discovery + * service type. + * + * @param $name The name of the cookie. + * @return The value of the cookie with the given name, or NULL if no cookie with that name exists. + */ + private function getCookie($name) { + $prefixedName = 'idpdisco_' . $this->discoType['type'] . '_' . $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 $name The name of the cookie. + * @param $value The value of the cookie. + */ + private function setCookie($name, $value) { + $prefixedName = 'idpdisco_' . $this->discoType['type'] . '_' . $name; + + /* We save the cookies for 90 days. */ + $saveUntil = time() + 60*60*24*90; + + /* The base path for cookies. This should be the installation directory for simpleSAMLphp. */ + $cookiePath = '/' . $this->config->getBaseUrl(); + + setcookie($prefixedName, $value, $saveUntil, $cookiePath); + } + + + /** + * 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 $idp The entity id we want to validate. This can be NULL, in which case we will return NULL. + * @return The entity id if it is valid, NULL if not. + */ + private function validateIdP($idp) { + if($idp === NULL) { + return NULL; + } + + try { + $this->metadata->getMetaData($idp, $this->discoType['metadata']); + 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 The entity id of the IdP the user has chosen, or NULL if the user has made no choice. + */ + private function getSelectedIdP() { + + 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 The entity id of the IdP the user has saved, or NULL if the user hasn't saved any choice. + */ + private function getSavedIdP() { + if(!$this->config->getBoolean('idpdisco.enableremember', FALSE)) { + /* Saving of IdP choices is disabled. */ + return NULL; + } + + if($this->getCookie('remember') === '1') { + return $this->getPreviousIdP(); + } + } + + + /** + * Retrieve the previous IdP the user used. + * + * @return The entity id of the previous IdP the user used, or NULL if this is the first time. + */ + private function getPreviousIdP() { + return $this->validateIdP($this->getCookie('lastidp')); + } + + + /** + * 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 The entity id of the IdP the user should most likely use. + */ + private function getRecommendedIdP() { + + $idp = $this->getPreviousIdP(); + if($idp !== NULL) { + $this->log('Preferred IdP from previous use [' . $idp . '].'); + return $idp; + } + + $idp = $this->metadata->getPreferredEntityIdFromCIDRhint( + $this->discoType['metadata'], $_SERVER['REMOTE_ADDR']); + + if(!empty($idp)) { + $this->log('Preferred IdP from CIDR hint [' . $idp . '].'); + return $idp; + } + + return NULL; + } + + + /** + * Determine whether the choice of IdP should be saved. + * + * @return TRUE if the choice should be saved, FALSE if not. + */ + private 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 The entity id of the IdP the user should be sent to, or NULL if the user + * should choose. + */ + private 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->log('Choice made [' . $idp . '] Setting cookie.'); + $this->setCookie('lastidp', $idp); + + if($this->saveIdP()) { + $this->setCookie('remember', 1); + } else { + $this->setCookie('remember', 0); + } + + return $idp; + } + + /* 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; + } + + + /** + * 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) { + $this->log('Choice made [' . $idp . '] (Redirecting the user back)'); + SimpleSAML_Utilities::redirect($this->returnURL, array($this->returnIdParam => $idp)); + return; + } + + /* No choice made. Show discovery service page. */ + + $idpList = $this->metadata->getList($this->discoType['metadata']); + $preferredIdP = $this->getRecommendedIdP(); + + /* + * 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.php'); + $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_Utilities::selfURLNoQuery()); + $t->data['rememberenabled'] = $this->config->getBoolean('idpdisco.enableremember', FALSE); + $t->show(); + } +} + +?> \ No newline at end of file diff --git a/www/saml2/sp/idpdisco.php b/www/saml2/sp/idpdisco.php index 67b8022e76e1d2cf55903316a7b1a1701c4dff81..0566025b27e26db99142551fecae7ac620500a03 100644 --- a/www/saml2/sp/idpdisco.php +++ b/www/saml2/sp/idpdisco.php @@ -2,168 +2,20 @@ require_once('../../_include.php'); -$config = SimpleSAML_Configuration::getInstance(); -$metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); $session = SimpleSAML_Session::getInstance(); - -SimpleSAML_Logger::info('SAML2.0 - SP.idpDisco: Accessing SAML 2.0 discovery service'); - -if (!$config->getValue('enable.saml20-sp', false)) { - SimpleSAML_Utilities::fatalError($session->getTrackID(), 'NOACCESS'); -} - -/* The base path for cookies. This should be the installation directory for simpleSAMLphp. */ -$cookiePath = '/' . $config->getBaseUrl(); - -/* Has the admin enabled the remember choice option in the config? */ -$rememberEnabled = $config->getBoolean('idpdisco.enableremember', FALSE); - -/* Check script parameters. */ try { - - if (!isset($_GET['entityID'])) throw new Exception('Missing parameter: entityID'); - if (!isset($_GET['return'])) throw new Exception('Missing parameter: return'); - - - - $spentityid = $_GET['entityID']; - $return = $_GET['return']; - - // Default value for "returnIDParam". Added to support Shibboleth 2.0 SP which does not - // send this parameter. - // if (!isset($_GET['returnIDParam'])) throw new Exception('Missing parameter: returnIDParam'); - $returnidparam = 'entityID'; - - // - if (isset($_GET['returnIDParam'])) { - $returnidparam = $_GET['returnIDParam']; - } - + $discoHandler = new SimpleSAML_XHTML_IdPDisco('saml20'); } catch (Exception $exception) { + /* An error here should be caused by invalid query parameters. */ SimpleSAML_Utilities::fatalError($session->getTrackID(), 'DISCOPARAMS', $exception); } - -$selectedIdP = NULL; -$userSelectedIdP = FALSE; - -/* Check for dropdown-style of IdP selection. */ -if(array_key_exists('idpentityid', $_GET)) { - $selectedIdP = $_GET['idpentityid']; - $userSelectedIdP = TRUE; - -} - -if($selectedIdP === NULL) { - /* 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)) { - $selectedIdP = urldecode($matches[1]); - $userSelectedIdP = TRUE; - } -} - - -if($selectedIdP === NULL && $rememberEnabled) { - /* No choice made by the user. Check if there is a remembered IdP for the user. */ - - if(array_key_exists('idpdisco_saml20_rememberchoice', $_COOKIE) && - array_key_exists('idpdisco_saml20_lastidp', $_COOKIE)) { - - $selectedIdP = $_COOKIE['idpdisco_saml20_lastidp']; - $userSelectedIdP = FALSE; - } -} - -/* Check that the selected IdP is a valid IdP. */ -if($selectedIdP !== NULL) { - try { - $idpMetadata = $metadata->getMetaData($selectedIdP, 'saml20-idp-remote'); - } catch(Exception $e) { - /* The entity id wasn't valid. */ - $selectedIdP = NULL; - $userSelectedIdP = FALSE; - } -} - -if($selectedIdP !== NULL) { - /* We have an IdP selection. */ - - if($userSelectedIdP) { - /* We save the users choice for 90 days. */ - $saveUntil = time() + 60*60*24*90; - - SimpleSAML_Logger::info('SAML2.0 - SP.idpDisco: Choice made [ ' . $selectedIdP . ']' . - ' Setting idpdisco_saml20_lastidp cookie.'); - setcookie('idpdisco_saml20_lastidp', $selectedIdP, $saveUntil, $cookiePath); - - if($rememberEnabled) { - if(array_key_exists('remember', $_GET)) { - /* The remember choice option is enabled, and the user has selected - * "remember choice" in the IdP list. Save this choice. - */ - setcookie('idpdisco_saml20_rememberchoice', 1, $saveUntil, $cookiePath); - } - } - } - - SimpleSAML_Logger::info('SAML2.0 - SP.idpDisco: Choice made [ ' . $selectedIdP . '] (Redirecting the user back)'); - SimpleSAML_Utilities::redirect($return, array($returnidparam => $selectedIdP)); -} - - -/* Load list of entities. */ try { - $idplist = $metadata->getList('saml20-idp-remote'); - $preferredidp = $metadata->getPreferredEntityIdFromCIDRhint('saml20-idp-remote', $_SERVER['REMOTE_ADDR']); - - if (!empty($preferredidp)) { - SimpleSAML_Logger::info('SAML2.0 - SP.idpDisco: Preferred IdP from CIDR hint [ ' . $preferredidp . '].'); - } + $discoHandler->handleRequest(); } catch(Exception $exception) { + /* An error here should be caused by metadata. */ SimpleSAML_Utilities::fatalError($session->getTrackID(), 'METADATA', $exception); } -if(array_key_exists('idpdisco_saml20_lastidp', $_COOKIE)) { - $preferredidp = $_COOKIE['idpdisco_saml20_lastidp']; - SimpleSAML_Logger::info('SAML2.0 - SP.idpDisco: Preferred IdP overridden from cookie [ ' . $preferredidp . '].'); -} - - -/* - * 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($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($config, $templatefile, 'disco.php'); -$t->data['idplist'] = $idplist; -$t->data['preferredidp'] = $preferredidp; - -$t->data['return']= $return; -$t->data['returnIDParam'] = $returnidparam; -$t->data['entityID'] = $spentityid; -$t->data['urlpattern'] = htmlspecialchars(SimpleSAML_Utilities::selfURLNoQuery()); - -$t->data['rememberenabled'] = $rememberEnabled; - -$t->show(); - ?> \ No newline at end of file diff --git a/www/shib13/sp/idpdisco.php b/www/shib13/sp/idpdisco.php index 3044a6c9735bd795f749df4cdc7c293b6163f411..ce4c7233b62cf0c2dbfc9c03f2a7b000b47c4a78 100644 --- a/www/shib13/sp/idpdisco.php +++ b/www/shib13/sp/idpdisco.php @@ -2,157 +2,20 @@ require_once('../../_include.php'); -$config = SimpleSAML_Configuration::getInstance(); -$metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); $session = SimpleSAML_Session::getInstance(); - -SimpleSAML_Logger::info('Shib1.3 - SP.idpDisco: Accessing Shib 1.3 discovery service'); - -if (!$config->getValue('enable.shib13-sp', false)) { - SimpleSAML_Utilities::fatalError($session->getTrackID(), 'NOACCESS'); -} - -/* The base path for cookies. This should be the installation directory for simpleSAMLphp. */ -$cookiePath = '/' . $config->getBaseUrl(); - -/* Has the admin enabled the remember choice option in the config? */ -$rememberEnabled = $config->getBoolean('idpdisco.enableremember', FALSE); - -/* Check script parameters. */ try { - - if (!isset($_GET['entityID'])) throw new Exception('Missing parameter: entityID'); - if (!isset($_GET['return'])) throw new Exception('Missing parameter: return'); - if (!isset($_GET['returnIDParam'])) throw new Exception('Missing parameter: returnIDParam'); - - $spentityid = $_GET['entityID']; - $return = $_GET['return']; - $returnidparam = $_GET['returnIDParam']; - + $discoHandler = new SimpleSAML_XHTML_IdPDisco('shib13'); } catch (Exception $exception) { + /* An error here should be caused by invalid query parameters. */ SimpleSAML_Utilities::fatalError($session->getTrackID(), 'DISCOPARAMS', $exception); } - -$selectedIdP = NULL; -$userSelectedIdP = FALSE; - -/* Check for dropdown-style of IdP selection. */ -if(array_key_exists('idpentityid', $_GET)) { - $selectedIdP = $_GET['idpentityid']; - $userSelectedIdP = TRUE; - -} - -if($selectedIdP === NULL) { - /* 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)) { - $selectedIdP = urldecode($matches[1]); - $userSelectedIdP = TRUE; - } -} - - -if($selectedIdP === NULL && $rememberEnabled) { - /* No choice made by the user. Check if there is a remembered IdP for the user. */ - - if(array_key_exists('idpdisco_shib13_rememberchoice', $_COOKIE) && - array_key_exists('idpdisco_shib13_lastidp', $_COOKIE)) { - - $selectedIdP = $_COOKIE['idpdisco_shib13_lastidp']; - $userSelectedIdP = FALSE; - } -} - -/* Check that the selected IdP is a valid IdP. */ -if($selectedIdP !== NULL) { - try { - $idpMetadata = $metadata->getMetaData($selectedIdP, 'shib13-idp-remote'); - } catch(Exception $e) { - /* The entity id wasn't valid. */ - $selectedIdP = NULL; - $userSelectedIdP = FALSE; - } -} - -if($selectedIdP !== NULL) { - /* We have an IdP selection. */ - - if($userSelectedIdP) { - /* We save the users choice for 90 days. */ - $saveUntil = time() + 60*60*24*90; - - SimpleSAML_Logger::info('Shib1.3 - SP.idpDisco: Choice made [ ' . $selectedIdP . ']' . - ' Setting idpdisco_shib13_lastidp cookie.'); - setcookie('idpdisco_shib13_lastidp', $selectedIdP, $saveUntil, $cookiePath); - - if($rememberEnabled) { - if(array_key_exists('remember', $_GET)) { - /* The remember choice option is enabled, and the user has selected - * "remember choice" in the IdP list. Save this choice. - */ - setcookie('idpdisco_shib13_rememberchoice', 1, $saveUntil, $cookiePath); - } - } - } - - SimpleSAML_Utilities::redirect($return, array($returnidparam => $selectedIdP)); -} - - -/* Load list of entities. */ try { - $idplist = $metadata->getList('shib13-idp-remote'); - $preferredidp = $metadata->getPreferredEntityIdFromCIDRhint('shib13-idp-remote', $_SERVER['REMOTE_ADDR']); - - if (!empty($preferredidp)) { - SimpleSAML_Logger::info('Shib1.3 - SP.idpDisco: Preferred IdP from CIDR hint [ ' . $preferredidp . '].'); - } + $discoHandler->handleRequest(); } catch(Exception $exception) { + /* An error here should be caused by metadata. */ SimpleSAML_Utilities::fatalError($session->getTrackID(), 'METADATA', $exception); } -if(array_key_exists('idpdisco_shib13_lastidp', $_COOKIE)) { - $preferredidp = $_COOKIE['idpdisco_shib13_lastidp']; - SimpleSAML_Logger::info('Shib1.3 - SP.idpDisco: Preferred IdP overridden from cookie [ ' . $preferredidp . '].'); -} - - -/* - * 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($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($config, $templatefile, 'disco.php'); -$t->data['idplist'] = $idplist; -$t->data['preferredidp'] = $preferredidp; - -$t->data['return']= $return; -$t->data['returnIDParam'] = $returnidparam; -$t->data['entityID'] = $spentityid; -$t->data['urlpattern'] = htmlspecialchars(SimpleSAML_Utilities::selfURLNoQuery()); - -$t->data['rememberenabled'] = $rememberEnabled; - -$t->show(); - ?> \ No newline at end of file