diff --git a/CHANGELOG b/CHANGELOG index 88dca169f76a71f2a612d3afaacf89b134e9dd01..deb5ceede55b54c2c9d43e2e69bebf2074972d97 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,7 @@ smasch (1.0.0~alpha.1-0) unstable; urgency=low (#285) * small improvement: all configuration options that are not obligatory are moved to configuration panel (#343) + * small improvement: 2FA can be configured to be required option (#358) -- Piotr Gawron <piotr.gawron@uni.lu> Tue, 10 Nov 2020 14:00:00 +0200 diff --git a/smash/smash/local_settings.py.template b/smash/smash/local_settings.py.template index 79c90e5220130d1e6ea9467b312d3edf30beee9b..fb44fbe9517d7b10898b1f5ae955843bed55ef06 100644 --- a/smash/smash/local_settings.py.template +++ b/smash/smash/local_settings.py.template @@ -76,3 +76,7 @@ LOGGING = { } TWO_FACTOR_SMS_GATEWAY = "web.nexmo_gateway.Nexmo" + +# whether 2 steps authentication is mandatory to access the system + +FORCE_2FA = True diff --git a/smash/smash/middleware/__init__.py b/smash/smash/middleware/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..eea436a3790f7fd77b1cf18c6f5c5ae198e7dd6f --- /dev/null +++ b/smash/smash/middleware/__init__.py @@ -0,0 +1,3 @@ +import logging + +logger = logging.getLogger(__name__) diff --git a/smash/smash/middleware/force_2fa_middleware.py b/smash/smash/middleware/force_2fa_middleware.py new file mode 100644 index 0000000000000000000000000000000000000000..21de43603e166d84d043fa403840357b0a45922b --- /dev/null +++ b/smash/smash/middleware/force_2fa_middleware.py @@ -0,0 +1,27 @@ +import logging + +from django.contrib import messages +from django.shortcuts import redirect + +logger = logging.getLogger(__name__) + + +class Force2FAMiddleware: + """ + Middleware restricting access to users with 2 factors authentication enabled + Redirects to 2fa section if not enabled + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + """ + If user is authenticated and user is not verified (2fa not enabled) and we are in one of the 2fa setting pages + we redirect to the 2fa profile page + """ + if request.user.is_authenticated and not request.user.is_verified() and 'two_factor' not in request.path: + messages.add_message(request, messages.WARNING, + '2 factors authentication must be enabled to use this system') + return redirect('two_factor:profile') + return self.get_response(request) diff --git a/smash/smash/settings.py b/smash/smash/settings.py index 15be4ea9c4cd68270d4226892658989016d80985..0b20f9916893b6e782d9aed6fe15fe898a3ae558 100644 --- a/smash/smash/settings.py +++ b/smash/smash/settings.py @@ -24,6 +24,9 @@ WSGI_APPLICATION = 'smash.wsgi.application' SERVE_STATIC = False +# whether 2 steps authentication is mandatory to access the system +FORCE_2FA = False + # Application definition INSTALLED_APPS = [ @@ -145,3 +148,5 @@ from .local_settings import * if not SERVE_STATIC: MIDDLEWARE.remove('whitenoise.middleware.WhiteNoiseMiddleware') +if FORCE_2FA: + MIDDLEWARE.append('smash.middleware.force_2fa_middleware.Force2FAMiddleware') diff --git a/smash/web/tests/test_enforce_2fa.py b/smash/web/tests/test_enforce_2fa.py new file mode 100644 index 0000000000000000000000000000000000000000..0affc9b1708b360feb079b12e3e0c09718260e01 --- /dev/null +++ b/smash/web/tests/test_enforce_2fa.py @@ -0,0 +1,55 @@ +# coding=utf-8 +from unittest.mock import Mock + +from django.http import HttpResponseRedirect +from django.test import TestCase + +from smash.middleware.force_2fa_middleware import Force2FAMiddleware + +NOT_REDIRECTED = "Not redirected" + + +class TestForce2FAMiddleware(TestCase): + + def setUp(self): + self.middleware = Force2FAMiddleware(lambda x: NOT_REDIRECTED) + + def test_verified(self): + """ + if user verified and we don't redirect + """ + request = Mock() + request.session = {} + request.user = Mock() + request.path = 'subjects' + request.user.is_authenticated = True + request.user.is_verified = lambda: True + result = self.middleware(request) + self.assertEqual(result, NOT_REDIRECTED) + + def test_not_verified(self): + """ + if user not verified and not in 2fa section, we redirect to 2fa section + """ + request = Mock() + request.session = {} + request.user = Mock() + request.path = 'subjects' + request.user.is_authenticated = True + request.user.is_verified = lambda: False + result = self.middleware(request) + self.assertTrue(isinstance(result, HttpResponseRedirect)) + self.assertEqual(result.url, '/account/two_factor/') + + def test_not_verified_2fa_section(self): + """ + if user not verified but in 2fa section, we don't redirect + """ + request = Mock() + request.session = {} + request.user = Mock() + request.path = 'two_factor' + request.user.is_authenticated = True + request.user.is_verified = lambda: False + result = self.middleware(request) + self.assertEqual(result, NOT_REDIRECTED)