From 0c27d59b2c5d95e8860ac41cd63b15866b422f6c Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Tue, 3 Aug 2021 16:38:38 +0200 Subject: [PATCH 2/2] views: allow adapter to force authentication (#55953) --- mellon/utils.py | 8 ++++++++ mellon/views.py | 3 +-- tests/__init__.py | 0 tests/adapters.py | 19 +++++++++++++++++++ tests/test_sso_slo.py | 20 +++++++++++++++++--- tests/test_utils.py | 2 +- tests/test_views.py | 4 ++-- testsettings.py | 2 +- 8 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/adapters.py diff --git a/mellon/utils.py b/mellon/utils.py index 77bb899..2700cb8 100644 --- a/mellon/utils.py +++ b/mellon/utils.py @@ -282,6 +282,14 @@ def login(request, user): auth.login(request, user) +def should_force_authn(request): + result = False + for adapter in get_adapters(): + if hasattr(adapter, 'should_force_authn'): + result |= adapter.should_force_authn(request) + return result + + def get_xml_encoding(content): xml_encoding = 'utf-8' diff --git a/mellon/views.py b/mellon/views.py index f836c74..d3d2ec4 100644 --- a/mellon/views.py +++ b/mellon/views.py @@ -501,8 +501,7 @@ class LoginView(ProfileMixin, LogMixin, View): # link the nonce to the request-id if 'nonce' in request.GET: self.set_nonce(request.GET['nonce'][:128]) - if force_authn: - authn_request.forceAuthn = True + authn_request.forceAuthn = force_authn or utils.should_force_authn(request) if request.GET.get('passive') == '1': authn_request.isPassive = True # configure requested AuthnClassRef diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/adapters.py b/tests/adapters.py new file mode 100644 index 0000000..64389ac --- /dev/null +++ b/tests/adapters.py @@ -0,0 +1,19 @@ +# django-mellon - SAML2 authentication for Django +# Copyright (C) 2014-2021 Entr'ouvert +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +class ShouldForceAuthnAdapter: + def should_force_authn(self, request): + return True diff --git a/tests/test_sso_slo.py b/tests/test_sso_slo.py index ee51076..6bcef28 100644 --- a/tests/test_sso_slo.py +++ b/tests/test_sso_slo.py @@ -94,7 +94,7 @@ class MockIdp(object): self.session_dump = None def process_authn_request_redirect(self, url, auth_result=True, consent=True, msg=None): - login = lasso.Login(self.server) + login = self.login = lasso.Login(self.server) if self.identity_dump: login.setIdentityFromDump(self.identity_dump) if self.session_dump: @@ -445,7 +445,7 @@ def test_sso_request_denied(db, app, idp, caplog, sp_settings): ) -@pytest.mark.urls('urls_tests_template_base') +@pytest.mark.urls('tests.urls_tests_template_base') def test_template_base(db, app, idp, caplog, sp_settings): response = app.get(reverse('mellon_metadata')) response = app.get(reverse('mellon_login')) @@ -462,7 +462,7 @@ def test_template_base(db, app, idp, caplog, sp_settings): assert urlparse.urlparse(response['Location']).path == '/singleLogout' -@pytest.mark.urls('urls_tests_template_hook') +@pytest.mark.urls('tests.urls_tests_template_hook') def test_template_hook(db, app, idp, caplog, sp_settings): response = app.get(reverse('mellon_metadata')) response = app.get(reverse('mellon_login')) @@ -739,3 +739,17 @@ def test_nonce(db, app, idp, caplog, sp_settings): url, body, relay_state = idp.process_authn_request_redirect(response['Location']) response = app.post(reverse('mellon_login'), params={'SAMLResponse': body, 'RelayState': relay_state}) assert app.session['mellon_session']['nonce'] == '1234' + + +def test_adapter_should_force_authn(db, app, idp, caplog, sp_settings): + response = app.get(reverse('mellon_login')) + idp.process_authn_request_redirect(response['Location']) + assert not idp.login.request.forceAuthn + + sp_settings.MELLON_ADAPTER = [ + 'mellon.adapters.DefaultAdapter', + 'tests.adapters.ShouldForceAuthnAdapter', + ] + response = app.get(reverse('mellon_login')) + idp.process_authn_request_redirect(response['Location']) + assert idp.login.request.forceAuthn diff --git a/tests/test_utils.py b/tests/test_utils.py index fb5fa21..fa06d61 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -22,7 +22,7 @@ import lasso from mellon.utils import create_metadata, iso8601_to_datetime, flatten_datetime from mellon.views import check_next_url -from xml_utils import assert_xml_constraints +from .xml_utils import assert_xml_constraints def test_create_metadata(rf, private_settings, caplog): diff --git a/tests/test_views.py b/tests/test_views.py index 451709e..6f68342 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -27,9 +27,9 @@ from django.urls import reverse from django.utils.encoding import force_text from django.utils.http import urlencode -from xml_utils import assert_xml_constraints +from .xml_utils import assert_xml_constraints -from utils import error_500, html_response +from .utils import error_500, html_response pytestmark = pytest.mark.django_db diff --git a/testsettings.py b/testsettings.py index 99e1726..adcff59 100644 --- a/testsettings.py +++ b/testsettings.py @@ -31,7 +31,7 @@ else: ) AUTHENTICATION_BACKENDS = ('mellon.backends.SAMLBackend',) -ROOT_URLCONF = 'urls_tests' +ROOT_URLCONF = 'tests.urls_tests' TEMPLATE_DIRS = [ 'tests/templates/', ] -- 2.32.0.rc0