diff --git a/mellon/app_settings.py b/mellon/app_settings.py index fdbf949..acf99ae 100644 --- a/mellon/app_settings.py +++ b/mellon/app_settings.py @@ -31,7 +31,6 @@ class AppSettings(object): 'DEFAULT_ASSERTION_CONSUMER_BINDING': 'post', # or artifact 'VERIFY_SSL_CERTIFICATE': True, 'OPENED_SESSION_COOKIE_NAME': None, - 'OPENED_SESSION_COOKIE_DOMAIN': None, 'ORGANIZATION': None, 'CONTACT_PERSONS': [], 'TRANSIENT_FEDERATION_ATTRIBUTE': None, diff --git a/mellon/compat.py b/mellon/compat.py new file mode 100644 index 0000000..3ec9e6e --- /dev/null +++ b/mellon/compat.py @@ -0,0 +1,27 @@ +# django-mellon - SAML2 authentication for Django +# Copyright (C) 2014-2019 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 . + +import django +if django.VERSION < (1, 11, 0): + from django.core.urlresolvers import reverse + MiddlewareClass = object + + is_authenticated = lambda user: user.is_authenticated() +else: + from django.urls import reverse + from django.utils.deprecation import MiddlewareMixin + MiddlewareClass = MiddlewareMixin + + is_authenticated = lambda user: user.is_authenticated diff --git a/mellon/middleware.py b/mellon/middleware.py index d834b60..2a31a21 100644 --- a/mellon/middleware.py +++ b/mellon/middleware.py @@ -19,12 +19,12 @@ from django.utils.http import urlencode from django.http import HttpResponseRedirect from . import app_settings, utils -from .compat import reverse +from .compat import reverse, MiddlewareClass, is_authenticated PASSIVE_TRIED_COOKIE = 'MELLON_PASSIVE_TRIED' -class PassiveAuthenticationMiddleware(object): +class PassiveAuthenticationMiddleware(MiddlewareClass): def process_response(self, request, response): # When unlogged remove the PASSIVE_TRIED cookie if app_settings.OPENED_SESSION_COOKIE_NAME \ @@ -47,26 +47,19 @@ class PassiveAuthenticationMiddleware(object): return if not app_settings.OPENED_SESSION_COOKIE_NAME: return - if hasattr(request, 'user') and request.user.is_authenticated(): + if hasattr(request, 'user') and is_authenticated(request.user): return if PASSIVE_TRIED_COOKIE in request.COOKIES: return - if app_settings.OPENED_SESSION_COOKIE_NAME in request.COOKIES: - # get the common domain or guess - common_domain = app_settings.OPENED_SESSION_COOKIE_DOMAIN - if not common_domain: - host = request.get_host() - # accept automatic common domain selection if domain has at least three components - # and is not an IP address - if not host.count('.') > 1 or host.replace('.', '').isdigit(): - return - common_domain = request.get_host().split('.', 1)[1] - params = { - 'next': request.build_absolute_uri(), - 'passive': '', - } - url = reverse('mellon_login') + '?%s' % urlencode(params) - response = HttpResponseRedirect(url) - # prevent loops - response.set_cookie(PASSIVE_TRIED_COOKIE, value='1', max_age=None) - return response + if app_settings.OPENED_SESSION_COOKIE_NAME not in request.COOKIES: + return + # all is good, try passive login + params = { + 'next': request.build_absolute_uri(), + 'passive': '', + } + url = reverse('mellon_login') + '?%s' % urlencode(params) + response = HttpResponseRedirect(url) + # prevent loops + response.set_cookie(PASSIVE_TRIED_COOKIE, value='1', max_age=None) + return response diff --git a/tests/test_sso_slo.py b/tests/test_sso_slo.py index 2e68b37..bffbe22 100644 --- a/tests/test_sso_slo.py +++ b/tests/test_sso_slo.py @@ -373,3 +373,31 @@ def test_sso_slo_pass_login_hints_backoffice(db, app, idp, caplog, sp_settings): login_hints = root.findall('.//{https://www.entrouvert.com/}login-hint') assert len(login_hints) == 1, 'missing login hint' assert login_hints[0].text == 'backoffice', 'login hint is not backoffice' + + +def test_middleware_mixin_first_time(db, app, idp, caplog, settings): + settings.MELLON_OPENED_SESSION_COOKIE_NAME = 'IDP_SESSION' + assert 'MELLON_PASSIVE_TRIED' not in app.cookies + # webtest-lint is against unicode + app.set_cookie(str('IDP_SESSION'), str('1')) + response = app.get('/', status=302) + assert urlparse.urlparse(response.location).path == '/login/' + assert (urlparse.parse_qs(urlparse.urlparse(response.location).query, keep_blank_values=True) + == {'next': ['http://testserver/'], 'passive': ['']}) + + # simulate closing of session at IdP + app.cookiejar.clear('testserver.local', '/', 'IDP_SESSION') + assert 'IDP_SESSION' not in app.cookies + + # verify MELLON_PASSIVE_TRIED is removed + assert 'MELLON_PASSIVE_TRIED' in app.cookies + response = app.get('/', status=200) + assert 'MELLON_PASSIVE_TRIED' not in app.cookies + + # check passive authentication is tried again + app.set_cookie(str('IDP_SESSION'), str('1')) + response = app.get('/', status=302) + assert urlparse.urlparse(response.location).path == '/login/' + assert (urlparse.parse_qs(urlparse.urlparse(response.location).query, keep_blank_values=True) + == {'next': ['http://testserver/'], 'passive': ['']}) + assert 'MELLON_PASSIVE_TRIED' in app.cookies diff --git a/tests/urls_tests.py b/tests/urls_tests.py index 52500a4..651f131 100644 --- a/tests/urls_tests.py +++ b/tests/urls_tests.py @@ -13,8 +13,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import django - from django.conf.urls import url, include from django.http import HttpResponse diff --git a/testsettings.py b/testsettings.py index 0ccac2b..fc24f09 100644 --- a/testsettings.py +++ b/testsettings.py @@ -21,12 +21,14 @@ if hasattr(global_settings, 'MIDDLEWARE_CLASSES'): MIDDLEWARE_CLASSES += ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'mellon.middleware.PassiveAuthenticationMiddleware', ) else: MIDDLEWARE = global_settings.MIDDLEWARE MIDDLEWARE += ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'mellon.middleware.PassiveAuthenticationMiddleware', ) AUTHENTICATION_BACKENDS = (