From dfdc1034ca38348162fb7ac0a8700d8b47655a3e Mon Sep 17 00:00:00 2001
From: Serghei Mihai <smihai@entrouvert.com>
Date: Thu, 22 Nov 2018 12:04:41 +0100
Subject: [PATCH] misc: allow authenticator autorun if only one available
 (#28216)

---
 src/authentic2/views.py                    |  7 +++++++
 src/authentic2_auth_fc/authenticators.py   |  4 ++++
 src/authentic2_auth_oidc/authenticators.py | 12 ++++++++++++
 src/authentic2_auth_saml/authenticators.py | 13 ++++++++++++-
 tests/auth_fc/test_auth_fc.py              |  7 +++++++
 tests/test_auth_oidc.py                    | 11 +++++++++++
 tests/test_auth_saml.py                    | 13 +++++++++++++
 7 files changed, 66 insertions(+), 1 deletion(-)

diff --git a/src/authentic2/views.py b/src/authentic2/views.py
index 13881ce4..c1caf1d0 100644
--- a/src/authentic2/views.py
+++ b/src/authentic2/views.py
@@ -341,6 +341,13 @@ def login(request, template_name='authentic2/login.html',
                         return block['response']
                     blocks.append(block)
 
+    # run the only available authenticator if able to autorun
+    if len(blocks) == 1:
+        block = blocks[0]
+        authenticator = block['authenticator']
+        if hasattr(authenticator, 'autorun'):
+            return authenticator.autorun(request, block['id'])
+
     # Old frontends API
     for block in blocks:
         fid = block['id']
diff --git a/src/authentic2_auth_fc/authenticators.py b/src/authentic2_auth_fc/authenticators.py
index 00c974e7..a7829b7b 100644
--- a/src/authentic2_auth_fc/authenticators.py
+++ b/src/authentic2_auth_fc/authenticators.py
@@ -20,6 +20,7 @@ from django.template.response import TemplateResponse
 
 from authentic2 import app_settings as a2_app_settings, utils as a2_utils
 from authentic2.authenticators import BaseAuthenticator
+from authentic2.utils import redirect_to_login
 
 from . import app_settings
 
@@ -39,6 +40,9 @@ class FcAuthenticator(BaseAuthenticator):
     def id(self):
         return 'fc'
 
+    def autorun(self, request, block_id):
+        return redirect_to_login(request, login_url='fc-login-or-link')
+
     def login(self, request, *args, **kwargs):
         if 'nofc' in request.GET:
             return
diff --git a/src/authentic2_auth_oidc/authenticators.py b/src/authentic2_auth_oidc/authenticators.py
index 46a78313..0c12ec3d 100644
--- a/src/authentic2_auth_oidc/authenticators.py
+++ b/src/authentic2_auth_oidc/authenticators.py
@@ -18,7 +18,9 @@ from django.utils.translation import gettext_noop
 from django.shortcuts import render
 
 from . import app_settings, utils
+from .models import OIDCProvider
 from authentic2.utils import make_url
+from authentic2.utils import redirect_to_login
 from authentic2.authenticators import BaseAuthenticator
 
 
@@ -36,6 +38,16 @@ class OIDCAuthenticator(BaseAuthenticator):
         for p in utils.get_providers(shown=True):
             yield (p.slug, p)
 
+    def autorun(self, request, block_id):
+        auth_id, instance_slug = block_id.split('_')
+        assert auth_id == self.id
+
+        try:
+            provider = OIDCProvider.objects.get(slug=instance_slug)
+        except OIDCProvider.DoesNotExist():
+            return redirect_to_login(request)
+        return redirect_to_login(request, login_url='oidc-login', kwargs={'pk': provider.pk})
+
     def login(self, request, *args, **kwargs):
         context = kwargs.get('context', {})
         if kwargs.get('instance'):
diff --git a/src/authentic2_auth_saml/authenticators.py b/src/authentic2_auth_saml/authenticators.py
index 5131a80b..b47c408c 100644
--- a/src/authentic2_auth_saml/authenticators.py
+++ b/src/authentic2_auth_saml/authenticators.py
@@ -19,8 +19,8 @@ from django.template.loader import render_to_string
 from django.shortcuts import render
 from mellon.utils import get_idp, get_idps
 
-from authentic2.utils import redirect_to_login
 from authentic2.authenticators import BaseAuthenticator
+from authentic2.utils import redirect_to_login
 
 from . import app_settings
 
@@ -38,6 +38,17 @@ class SAMLAuthenticator(BaseAuthenticator):
         for idx, idp in enumerate(get_idps()):
             yield(idp.get('SLUG') or str(idx), idp)
 
+    def autorun(self, request, block_id):
+        auth_id, instance_slug = block_id.split('_')
+        assert auth_id == self.id
+
+        for slug, instance in self.instances(request):
+            if slug == instance_slug:
+                return redirect_to_login(request, login_url='mellon_login',
+                                         params={'entityID': instance['ENTITY_ID']})
+        return redirect_to_login(request)
+
+
     def login(self, request, *args, **kwargs):
         context = kwargs.pop('context', {})
         instance_id = kwargs.get('instance_id')
diff --git a/tests/auth_fc/test_auth_fc.py b/tests/auth_fc/test_auth_fc.py
index a30f073d..dd048f58 100644
--- a/tests/auth_fc/test_auth_fc.py
+++ b/tests/auth_fc/test_auth_fc.py
@@ -98,6 +98,13 @@ def test_login_with_condition(app, fc_settings, settings):
     assert 'fc-button' not in response
 
 
+def test_login_autorun(app, fc_settings, settings):
+    # hide password block
+    settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': 'remote_addr==\'0.0.0.0\''}}
+    response = app.get('/login/')
+    assert response['Location'] == reverse('fc-login-or-link')
+
+
 @pytest.mark.parametrize('exp', [timestamp_from_datetime(now() + datetime.timedelta(seconds=1000)),
                                  timestamp_from_datetime(now() - datetime.timedelta(seconds=1000))])
 def test_login_simple(app, fc_settings, caplog, hooks, exp):
diff --git a/tests/test_auth_oidc.py b/tests/test_auth_oidc.py
index 6fdc2ff8..94a04bce 100644
--- a/tests/test_auth_oidc.py
+++ b/tests/test_auth_oidc.py
@@ -471,6 +471,17 @@ def test_login_with_conditional_authenticators(oidc_provider, app, settings, cap
     assert 'My IDP' not in response
 
 
+def test_login_autorun(oidc_provider, app, settings):
+    response = app.get('/login/')
+    assert 'OIDIDP' in response
+
+    # hide password block
+    settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': 'remote_addr==\'0.0.0.0\''}}
+    response = app.get('/login/', status=302)
+    assert response['Location'] == '/accounts/oidc/login/%s/' % oidc_provider.pk
+
+
+
 def test_sso(app, caplog, code, oidc_provider, oidc_provider_jwkset, hooks):
     OU = get_ou_model()
     cassis = OU.objects.create(name='Cassis', slug='cassis')
diff --git a/tests/test_auth_saml.py b/tests/test_auth_saml.py
index e1704491..f5ca36c3 100644
--- a/tests/test_auth_saml.py
+++ b/tests/test_auth_saml.py
@@ -177,3 +177,16 @@ def test_login_with_conditionnal_authenticators(db, app, settings, caplog):
     response = app.get('/login/')
     assert 'login-saml-0' not in response
     assert 'login-saml-1' not in response
+
+
+def test_login_autorun(db, app, settings):
+    response = app.get('/login/')
+
+    settings.A2_AUTH_SAML_ENABLE = True
+    settings.MELLON_IDENTITY_PROVIDERS = [
+        {"METADATA": os.path.join(os.path.dirname(__file__), 'metadata.xml')}
+    ]
+    # hide password block
+    settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': 'remote_addr==\'0.0.0.0\''}}
+    response = app.get('/login/', status=302)
+    assert '/accounts/saml/login/?entityID=' in response['Location']
-- 
2.27.0

