diff --git a/modules/multiauth/docs/multiauth.md b/modules/multiauth/docs/multiauth.md index 97ae265feb2724b63d0c4b154b748d75cb6ca979..5b474513a0ed6088e2c0c7a49cf6a361cab7c0de 100644 --- a/modules/multiauth/docs/multiauth.md +++ b/modules/multiauth/docs/multiauth.md @@ -36,12 +36,14 @@ authentication source: 'es' => 'Entrar usando un SP SAML', ), 'css-class' => 'SAML', + 'AuthnContextClassRef' => array('urn:oasis:names:tc:SAML:2.0:ac:classes:SmartcardPKI', 'urn:oasis:names:tc:SAML:2.0:ac:classes:MobileTwoFactorContract'), ), 'example-admin' => array( 'text' => array( 'en' => 'Log in using the admin password', 'es' => 'Entrar usando la contraseña de administrador', ), + 'AuthnContextClassRef' => 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport', ), ), ), @@ -77,7 +79,7 @@ compatible fashion so both cases should work. Each source in the sources array has a key and a value. As mentioned above the key is the authsource identifier and the value -is another array with two optional keys: 'text' and 'css-class'. +is another array with optional keys: 'text', 'css-class', 'help', and 'AuthnContextClassRef'. The text element is another array with localized strings for one or more languages. These texts will be shown in the selectsource.php view. Note that you should at least enter the text in the default @@ -87,7 +89,14 @@ the <li> element in the selectsource.php view. By default the authtype of the authsource is used as the css class with colons replaced by dashes. So in the previous example, the css class used in the 'example-admin' authentication source would be -'core-AdminPassword'. +'core-AdminPassword'. The help element is another array with localized +strings for one or more languages. These texts will be shown in the +selectsource.php view. The AuthnContextClassRef is either a string or +an array of strings containing [context class ref names](https://docs.oasis-open.org/security/saml/v2.0/saml-authn-context-2.0-os.pdf). +If an SP sets AuthnContextClassRef the list of authsources will be +filtered to only those containing context class refs that are part of the list set by the SP. +If a single authsource results from this filtering the user will be taken directly to the +authentication page for that source, and will never be shown the multiauth select page. It is possible to add the parameter `source` to the calling URL, when accessing a service, to allow the user to preselect the diff --git a/modules/multiauth/lib/Auth/Source/MultiAuth.php b/modules/multiauth/lib/Auth/Source/MultiAuth.php index be06955f2a2bd8e5740e0da92943dd7527685488..fd6d9dce7a94fa35455960b3ce7759c62af7707b 100644 --- a/modules/multiauth/lib/Auth/Source/MultiAuth.php +++ b/modules/multiauth/lib/Auth/Source/MultiAuth.php @@ -4,12 +4,15 @@ declare(strict_types=1); namespace SimpleSAML\Module\multiauth\Auth\Source; +use Exception; +use SAML2\Constants; use SimpleSAML\Auth; use SimpleSAML\Configuration; use SimpleSAML\Error; use SimpleSAML\Module; use SimpleSAML\Session; use SimpleSAML\Utils; +use SimpleSAML\Module\saml\Error\NoAuthnContext; /** * Authentication source which let the user chooses among a list of @@ -67,12 +70,12 @@ class MultiAuth extends \SimpleSAML\Auth\Source parent::__construct($info, $config); if (!array_key_exists('sources', $config)) { - throw new \Exception('The required "sources" config option was not found'); + throw new Exception('The required "sources" config option was not found'); } if (array_key_exists('preselect', $config) && is_string($config['preselect'])) { if (!array_key_exists($config['preselect'], $config['sources'])) { - throw new \Exception('The optional "preselect" config option must be present in "sources"'); + throw new Exception('The optional "preselect" config option must be present in "sources"'); } $this->preselect = $config['preselect']; @@ -115,11 +118,22 @@ class MultiAuth extends \SimpleSAML\Auth\Source } } + $class_ref = []; + if (array_key_exists('AuthnContextClassRef', $info)) { + $ref = $info['AuthnContextClassRef']; + if (is_string($ref)) { + $class_ref = [$ref]; + } else { + $class_ref = $ref; + } + } + $this->sources[] = [ 'source' => $source, 'text' => $text, 'help' => $help, 'css_class' => $css_class, + 'AuthnContextClassRef' => $class_ref, ]; } } @@ -149,6 +163,30 @@ class MultiAuth extends \SimpleSAML\Auth\Source $state['multiauth:preselect'] = $this->preselect; } + if ( + !is_null($state['saml:RequestedAuthnContext']) + && array_key_exists('AuthnContextClassRef', $state['saml:RequestedAuthnContext']) + ) { + $refs = array_values($state['saml:RequestedAuthnContext']['AuthnContextClassRef']); + $new_sources = []; + foreach ($this->sources as $source) { + if (count(array_intersect($source['AuthnContextClassRef'], $refs)) >= 1) { + $new_sources[] = $source; + } + } + $state[self::SOURCESID] = $new_sources; + + $number_of_sources = count($new_sources); + if ($number_of_sources === 0) { + throw new NoAuthnContext( + Constants::STATUS_RESPONDER, + 'No authentication sources exist for the requested AuthnContextClassRefs: ' . implode(', ', $refs) + ); + } else if ($number_of_sources === 1) { + MultiAuth::delegateAuthentication($new_sources[0]['source'], $state); + } + } + // Save the $state array, so that we can restore if after a redirect $id = Auth\State::saveState($state, self::STAGEID); @@ -200,7 +238,7 @@ class MultiAuth extends \SimpleSAML\Auth\Source $state[self::SOURCESID] ); if ($as === null || !in_array($authId, $valid_sources, true)) { - throw new \Exception('Invalid authentication source: ' . $authId); + throw new Exception('Invalid authentication source: ' . $authId); } // Save the selected authentication source for the logout process. @@ -216,7 +254,7 @@ class MultiAuth extends \SimpleSAML\Auth\Source $as->authenticate($state); } catch (Error\Exception $e) { Auth\State::throwException($state, $e); - } catch (\Exception $e) { + } catch (Exception $e) { $e = new Error\UnserializableException($e); Auth\State::throwException($state, $e); } @@ -243,7 +281,7 @@ class MultiAuth extends \SimpleSAML\Auth\Source $source = Auth\Source::getById($authId); if ($source === null) { - throw new \Exception('Invalid authentication source during logout: ' . $authId); + throw new Exception('Invalid authentication source during logout: ' . $authId); } // Then, do the logout on it $source->logout($state);