From 7c68cc8ea5140546fef30db980264c468d28796c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sim=C3=A3o=20Martins?= <Lasering@users.noreply.github.com>
Date: Tue, 15 Dec 2020 21:52:50 +0000
Subject: [PATCH] Filter multiauth authentication sources from SP using
 AuthnContextClassRef (#1362)

Co-authored-by: Tim van Dijen <tvdijen@gmail.com>
---
 modules/multiauth/docs/multiauth.md           | 13 ++++-
 .../multiauth/lib/Auth/Source/MultiAuth.php   | 48 +++++++++++++++++--
 2 files changed, 54 insertions(+), 7 deletions(-)

diff --git a/modules/multiauth/docs/multiauth.md b/modules/multiauth/docs/multiauth.md
index 97ae265fe..5b474513a 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 &lt;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 be06955f2..fd6d9dce7 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);
-- 
GitLab