diff --git a/modules/saml/dictionaries/proxy.definition.json b/modules/saml/dictionaries/proxy.definition.json new file mode 100644 index 0000000000000000000000000000000000000000..938c73fc55516bb5f92777b65345849b5e4a680f --- /dev/null +++ b/modules/saml/dictionaries/proxy.definition.json @@ -0,0 +1,8 @@ +{ + "invalid_idp": { + "en": "Invalid Identity Provider" + }, + "invalid_idp_description": { + "en": "You already have a valid session with an identity provider (<em>%IDP%</em>) that is not accepted by <em>%SP%</em>. Would you like to log out from your existing session and log in again with another identity provider?" + } +} diff --git a/modules/saml/dictionaries/proxy.translation.json b/modules/saml/dictionaries/proxy.translation.json new file mode 100644 index 0000000000000000000000000000000000000000..c78ea653fbf62f96fc955879dbc0928e9a4f2ed9 --- /dev/null +++ b/modules/saml/dictionaries/proxy.translation.json @@ -0,0 +1,8 @@ +{ + "invalid_idp": { + "es": "Proveedor de Identidad inválido" + }, + "invalid_idp_description": { + "es": "Ya existe una sesión válida con un proveedor de identidad (<em>%IDP%</em>) que <em>%SP%</em> no acepta. ¿Desea cerrar su sesión actual e iniciar una nueva con otro proveedor de identidad?" + } +} \ No newline at end of file diff --git a/modules/saml/lib/Auth/Source/SP.php b/modules/saml/lib/Auth/Source/SP.php index 40076712e2c0383808ee5d287dfae1a021107ee2..4a091fdb56d0b39c3d28b830f8321ca7fe4c030f 100644 --- a/modules/saml/lib/Auth/Source/SP.php +++ b/modules/saml/lib/Auth/Source/SP.php @@ -418,24 +418,84 @@ class sspmod_saml_Auth_Source_SP extends SimpleSAML_Auth_Source { // check if we have an IDPList specified in the request if (isset($state['saml:IDPList']) && sizeof($state['saml:IDPList']) > 0 && - !in_array($state['saml:sp:IdP'], $state['saml:IDPList'], TRUE)) { + !in_array($state['saml:sp:IdP'], $state['saml:IDPList'], true)) + { /* - * This is essentially wrong. The IdP used to authenticate the current session is not in the IDPList - * that we just received, so we are triggering authentication again against an IdP in the IDPList. This - * is fine if the user wants to, but we SHOULD offer the user to logout before proceeding. - * - * After successful authentication in a different IdP, the reauthPostLogin callback will be invoked, - * overriding the current session with a new one, associated with the new IdP. This will leave us in an - * inconsistent state, with several service providers with valid sessions they got from different IdPs. - * - * TODO: we need to offer the user the possibility to logout before blindly authenticating him again. + * The user has an existing, valid session. However, the SP provided a list of IdPs it accepts for + * authentication, and the IdP the existing session is related to is not in that list. We need to + * inform the user, and ask whether we should logout before starting the authentication process again + * with a different IdP, or cancel the current SSO attempt. */ - $state['LoginCompletedHandler'] = array('sspmod_saml_Auth_Source_SP', 'reauthPostLogin'); - $this->authenticate($state); + SimpleSAML\Logger::warning( + "Reauthentication after logout is needed. The IdP '${state['saml:sp:IdP']}' is not in the IDPList ". + "provided by the Service Provider '${state['core:SP']}'." + ); + + $state['saml:sp:IdPMetadata'] = $this->getIdPMetadata($state['saml:sp:IdP']); + $state['saml:sp:AuthId'] = $this->authId; + self::askForIdPChange($state); } } + /** + * Ask the user to log out before being able to log in again with a different identity provider. Note that this + * method is intended for instances of SimpleSAMLphp running as a SAML proxy, and therefore acting both as an SP + * and an IdP at the same time. + * + * This method will never return. + * + * @param array $state The state array. The following keys must be defined in the array: + * - 'saml:sp:IdPMetadata': a SimpleSAML_Configuration object containing the metadata of the IdP that authenticated + * the user in the current session. + * - 'saml:sp:AuthId': the identifier of the current authentication source. + * - 'core:IdP': the identifier of the local IdP. + * - 'SPMetadata': an array with the metadata of this local SP. + * + * @throws SimpleSAML_Error_NoPassive In case the authentication request was passive. + */ + public static function askForIdPChange(array &$state) + { + assert('array_key_exists("saml:sp:IdPMetadata", $state)'); + assert('array_key_exists("saml:sp:AuthId", $state)'); + assert('array_key_exists("core:IdP", $state)'); + assert('array_key_exists("SPMetadata", $state)'); + + if (isset($state['isPassive']) && (bool)$state['isPassive']) { + // passive request, we cannot authenticate the user + throw new SimpleSAML_Error_NoPassive('Reauthentication required'); + } + + // save the state WITHOUT a restart URL, so that we don't try an IdP-initiated login if something goes wrong + $id = SimpleSAML_Auth_State::saveState($state, 'saml:proxy:invalid_idp', true); + $url = SimpleSAML\Module::getModuleURL('saml/proxy/invalid_session.php'); + SimpleSAML\Utils\HTTP::redirectTrustedURL($url, array('AuthState' => $id)); + assert('false'); + } + + + /** + * Log the user out before logging in again. + * + * This method will never return. + * + * @param array $state The state array. + */ + public static function reauthLogout(array $state) + { + SimpleSAML\Logger::debug('Proxy: logging the user out before re-authentication.'); + + if (isset($state['Responder'])) { + $state['saml:proxy:reauthLogout:PrevResponder'] = $state['Responder']; + } + $state['Responder'] = array('sspmod_saml_Auth_Source_SP', 'reauthPostLogout'); + + $idp = SimpleSAML_IdP::getByState($state); + $idp->handleLogoutRequest($state, null); + assert('false'); + } + + /** * Complete login operation after re-authenticating the user on another IdP. * @@ -455,6 +515,31 @@ class sspmod_saml_Auth_Source_SP extends SimpleSAML_Auth_Source { } + /** + * Post-logout handler for re-authentication. + * + * This method will never return. + * + * @param SimpleSAML_IdP $idp The IdP we are logging out from. + * @param array &$state The state array with the state during logout. + */ + public static function reauthPostLogout(SimpleSAML_IdP $idp, array $state) { + assert('isset($state["saml:sp:AuthId"])'); + + SimpleSAML\Logger::debug('Proxy: logout completed.'); + + if (isset($state['saml:proxy:reauthLogout:PrevResponder'])) { + $state['Responder'] = $state['saml:proxy:reauthLogout:PrevResponder']; + } + + $sp = SimpleSAML_Auth_Source::getById($state['saml:sp:AuthId'], 'sspmod_saml_Auth_Source_SP'); + /** @var sspmod_saml_Auth_Source_SP $authSource */ + SimpleSAML\Logger::debug('Proxy: logging in again.'); + $sp->authenticate($state); + assert('false'); + } + + /** * Start a SAML 2 logout operation. * diff --git a/modules/saml/templates/proxy/invalid_session.php b/modules/saml/templates/proxy/invalid_session.php new file mode 100644 index 0000000000000000000000000000000000000000..1131379988f3a9954594674d5d57a72d9fb52ef6 --- /dev/null +++ b/modules/saml/templates/proxy/invalid_session.php @@ -0,0 +1,32 @@ +<?php +/** + * Template to ask a user whether to logout because of a reauthentication or not. + * + * @var SimpleSAML_XHTML_Template $this + * + * @author Jaime Pérez Crespo, UNINETT AS <jaime.perez@uninett.no> + * + * @package SimpleSAMLphp + */ + +if (!isset($this->data['head'])) { + $this->data['head'] = ''; +} +$this->includeAtTemplateBase('includes/header.php'); + +$translator = $this->getTranslator(); + +$params = array( + '%IDP%' => $this->data['idp_name'], + '%SP%' => $this->data['sp_name'], +); +?> + <h2><?php echo $translator->t('{saml:proxy:invalid_idp}'); ?></h2> + <p><?php echo $translator->t('{saml:proxy:invalid_idp_description}', $params); ?></p> + <form method="post" action="?"> + <input type="hidden" name="AuthState" value="<?php echo htmlspecialchars($this->data['AuthState']); ?>" /> + <input type="submit" name="continue" value="<?php echo $translator->t('{general:yes_continue}'); ?>" /> + <input type="submit" name="cancel" value="<?php echo $translator->t('{general:no_cancel}'); ?>" /> + </form> +<?php +$this->includeAtTemplateBase('includes/footer.php'); diff --git a/modules/saml/www/proxy/invalid_session.php b/modules/saml/www/proxy/invalid_session.php new file mode 100644 index 0000000000000000000000000000000000000000..ac38a65d7aa83e783b9d0e571e400daefcd486d0 --- /dev/null +++ b/modules/saml/www/proxy/invalid_session.php @@ -0,0 +1,69 @@ +<?php + +/** + * This file will handle the case of a user with an existing session that's not valid for a specific Service Provider, + * since the authenticating IdP is not in the list of IdPs allowed by the SP. + * + * @author Jaime Pérez Crespo, UNINETT AS <jaime.perez@uninett.no> + * + * @package SimpleSAMLphp + */ + +// retrieve the authentication state +if (!array_key_exists('AuthState', $_REQUEST)) { + throw new SimpleSAML_Error_BadRequest('Missing mandatory parameter: AuthState'); +} + +try { + // try to get the state + $state = SimpleSAML_Auth_State::loadState($_REQUEST['AuthState'], 'saml:proxy:invalid_idp'); +} catch (Exception $e) { + // the user probably hit the back button after starting the logout, try to recover the state with another stage + $state = SimpleSAML_Auth_State::loadState($_REQUEST['AuthState'], 'core:Logout:afterbridge'); + + // success! Try to continue with reauthentication, since we no longer have a valid session here + $idp = SimpleSAML_IdP::getById($state['core:IdP']); + sspmod_saml_Auth_Source_SP::reauthPostLogout($idp, $state); +} + +if (isset($_POST['cancel'])) { + // the user does not want to logout, cancel login + $e = new SimpleSAML_Error_Exception('User refused to reauthenticate with any of the IdPs requested.'); + sspmod_saml_IdP_SAML2::handleAuthError($e, $state); +} + +if (isset($_POST['continue'])) { + // log the user out before being able to login again + $as = SimpleSAML_Auth_Source::getById($state['saml:sp:AuthId'], 'sspmod_saml_Auth_Source_SP'); + /** @var sspmod_saml_Auth_Source_SP $as */ + $as->reauthLogout($state); +} + +$cfg = SimpleSAML_Configuration::getInstance(); +$template = new SimpleSAML_XHTML_Template($cfg, 'saml:proxy/invalid_session.php'); +$translator = $template->getTranslator(); +$template->data['AuthState'] = (string)$_REQUEST['AuthState']; + +// get the name of the IdP +$idpmdcfg = $state['saml:sp:IdPMetadata']; +/** @var SimpleSAML_Configuration $idpmdcfg */ +$idpmd = $idpmdcfg->toArray(); +if (array_key_exists('name', $idpmd)) { + $template->data['idp_name'] = $translator->getPreferredTranslation($idpmd['name']); +} elseif (array_key_exists('OrganizationDisplayName', $idpmd)) { + $template->data['idp_name'] = $translator->getPreferredTranslation($idpmd['OrganizationDisplayName']); +} else { + $template->data['idp_name'] = $idpmd['entityid']; +} + +// get the name of the SP +$spmd = $state['SPMetadata']; +if (array_key_exists('name', $spmd)) { + $template->data['sp_name'] = $translator->getPreferredTranslation($spmd['name']); +} elseif (array_key_exists('OrganizationDisplayName', $spmd)) { + $template->data['sp_name'] = $translator->getPreferredTranslation($spmd['OrganizationDisplayName']); +} else { + $template->data['sp_name'] = $spmd['entityid']; +} + +$template->show();