diff --git a/hypha/apply/users/middleware.py b/hypha/apply/users/middleware.py index 4ebdf2d0e55453a10ffbe6b5069c04ec03f1d53a..53ae4fc4f3bc1363d4a7d528459805271e69e43d 100644 --- a/hypha/apply/users/middleware.py +++ b/hypha/apply/users/middleware.py @@ -1,8 +1,16 @@ +from django.conf import settings +from django.shortcuts import redirect from social_core.exceptions import AuthForbidden from social_django.middleware import ( SocialAuthExceptionMiddleware as _SocialAuthExceptionMiddleware, ) +ALLOWED_SUBPATH_FOR_UNVERIFIED_USERS = [ + 'login/', + 'logout/', + 'account/', +] + class SocialAuthExceptionMiddleware(_SocialAuthExceptionMiddleware): """ @@ -13,3 +21,36 @@ class SocialAuthExceptionMiddleware(_SocialAuthExceptionMiddleware): return 'Your credentials are not recognised.' super().get_message(request, exception) + + +class TwoFactorAuthenticationMiddleware: + """ + Middleware to enforce 2FA activation for unverified users + + To activate this middleware set env variable ENFORCE_TWO_FACTOR as True. + + This will redirect all request from unverified users to enable 2FA first. + Except the request made on the url paths listed in ALLOWED_SUBPATH_FOR_UNVERIFIED_USERS. + """ + def __init__(self, get_response): + self.get_response = get_response + + def is_path_allowed(self, path): + + for sub_path in ALLOWED_SUBPATH_FOR_UNVERIFIED_USERS: + if sub_path in path: + return True + return False + + def __call__(self, request): + # code to execute before the view + user = request.user + if settings.ENFORCE_TWO_FACTOR: + if user.is_authenticated and not user.is_verified() and not user.social_auth.exists(): + if not self.is_path_allowed(request.path): + return redirect('/account/two_factor/required/') + + response = self.get_response(request) + + # code to execute after view + return response diff --git a/hypha/apply/users/templates/two_factor/core/two_factor_required.html b/hypha/apply/users/templates/two_factor/core/two_factor_required.html new file mode 100644 index 0000000000000000000000000000000000000000..64ed802ae57c9688282b1cf70b24b9669efe7264 --- /dev/null +++ b/hypha/apply/users/templates/two_factor/core/two_factor_required.html @@ -0,0 +1,22 @@ +<!--Custom template to enforce 2FA and Copied from two_factor/core/otp_required.html--> + +{% extends "two_factor/_base_focus.html" %} +{% load i18n %} + +{% block content %} + <h1>{% block title %}{% trans "Permission Denied" %}{% endblock %}</h1> + + <p>{% blocktrans trimmed %}The page you requested, enforces users to verify using + two-factor authentication for security reasons. You need to enable these + security features in order to access this page. Without enabling these security features, + You can only access the account(Profile section) or can logout from the system.{% endblocktrans %}</p> + + <p>{% blocktrans trimmed %}Two-factor authentication is not enabled for your + account. Enable two-factor authentication for enhanced account + security.{% endblocktrans %}</p> + + <p> + <a href="{% url 'two_factor:setup' %}" class="btn btn-primary"> + {% trans "Enable Two-Factor Authentication" %}</a> + </p> +{% endblock %} diff --git a/hypha/apply/users/tests/test_middleware.py b/hypha/apply/users/tests/test_middleware.py new file mode 100644 index 0000000000000000000000000000000000000000..6800bb67ba2247e3dad6b7b57787fe25fffa9989 --- /dev/null +++ b/hypha/apply/users/tests/test_middleware.py @@ -0,0 +1,41 @@ +from django.conf import settings +from django.test import TestCase, override_settings +from django.urls import reverse + +from hypha.apply.users.tests.factories import UserFactory + +from ..middleware import ALLOWED_SUBPATH_FOR_UNVERIFIED_USERS + + +@override_settings(ROOT_URLCONF='hypha.apply.urls', ENFORCE_TWO_FACTOR=True) +class TestTwoFactorAuthenticationMiddleware(TestCase): + def enable_otp(self, user): + return user.totpdevice_set.create(name='default') + + def test_unverified_user_redirect(self): + user = UserFactory() + self.client.force_login(user) + + response = self.client.get(settings.LOGIN_REDIRECT_URL, follow=True) + self.assertRedirects(response, reverse('users:two_factor_required'), status_code=301) + + response = self.client.get(reverse('funds:submissions:list'), follow=True) + self.assertRedirects(response, reverse('users:two_factor_required'), status_code=301) + + def test_verified_user_redirect(self): + user = UserFactory() + self.client.force_login(user) + self.enable_otp(user=user) + response = self.client.get(settings.LOGIN_REDIRECT_URL, follow=True) + self.assertEqual(response.status_code, 200) + + response = self.client.get(reverse('funds:submissions:list'), follow=True) + self.assertEqual(response.status_code, 200) + + def test_unverified_user_can_access_allowed_urls(self): + user = UserFactory() + self.client.force_login(user) + + for path in ALLOWED_SUBPATH_FOR_UNVERIFIED_USERS: + response = self.client.get(path, follow=True) + self.assertEqual(response.status_code, 200) diff --git a/hypha/apply/users/urls.py b/hypha/apply/users/urls.py index eaea2ed6784a6c2e84ee5de7e3e11450539fcfe4..94bc9bb2ff835b4cdd36f589eafe7aeab3d1983b 100644 --- a/hypha/apply/users/urls.py +++ b/hypha/apply/users/urls.py @@ -10,6 +10,7 @@ from .views import ( LoginView, TWOFABackupTokensPasswordView, TWOFADisableView, + TWOFARequiredMessageView, become, create_password, oauth, @@ -83,6 +84,8 @@ urlpatterns = [ ActivationView.as_view(), name='activate' ), + # Two factor redirect + path('two_factor/required/', TWOFARequiredMessageView.as_view(), name='two_factor_required'), path('two_factor/backup_tokens/password/', TWOFABackupTokensPasswordView.as_view(), name='backup_tokens_password'), path('two_factor/disable/', TWOFADisableView.as_view(), name='disable'), path('confirmation/done/', EmailChangeDoneView.as_view(), name="confirm_link_sent"), diff --git a/hypha/apply/users/views.py b/hypha/apply/users/views.py index b5266400ff86d7c0d5244cc89ac27b047a18bd10..6bf3241b353e8be114120cc3ad00fb8d7cccfa76 100644 --- a/hypha/apply/users/views.py +++ b/hypha/apply/users/views.py @@ -304,3 +304,7 @@ class TWOFADisableView(TwoFactorDisableView): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs + + +class TWOFARequiredMessageView(TemplateView): + template_name = 'two_factor/core/two_factor_required.html' diff --git a/hypha/settings/base.py b/hypha/settings/base.py index d0379bd6a8321fd4be66fea9973cf87ff64d4839..8eb1f01fade095472701ffc1c85bd721e15fb794 100644 --- a/hypha/settings/base.py +++ b/hypha/settings/base.py @@ -146,6 +146,7 @@ MIDDLEWARE = [ 'hijack.middleware.HijackUserMiddleware', + 'hypha.apply.users.middleware.TwoFactorAuthenticationMiddleware', 'hypha.apply.users.middleware.SocialAuthExceptionMiddleware', 'wagtail.contrib.redirects.middleware.RedirectMiddleware', @@ -361,6 +362,9 @@ WAGTAIL_PASSWORD_MANAGEMENT_ENABLED = False WAGTAILUSERS_PASSWORD_ENABLED = False WAGTAILUSERS_PASSWORD_REQUIRED = False +# Enforce Two factor setting +ENFORCE_TWO_FACTOR = env.bool('ENFORCE_TWO_FACTOR', False) + LOGIN_URL = 'users_public:login' LOGIN_REDIRECT_URL = 'dashboard:dashboard'