diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b6795be7..a6bb1c3de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ ### Added - Support confirmation for Django 5.1. +- The login view is also decorated with the `login_not_required` decorator for + projects using the new `LoginRequiredMiddleware` available with Django 5.1+. ### Removed - Dropped support for Django <4.2. diff --git a/tests/test_views_login.py b/tests/test_views_login.py index dd5b5840e..f6e6e76e1 100644 --- a/tests/test_views_login.py +++ b/tests/test_views_login.py @@ -1,13 +1,13 @@ import json from importlib import import_module from time import sleep -from unittest import mock +from unittest import mock, skipUnless from django.conf import settings from django.core.signing import BadSignature from django.shortcuts import resolve_url from django.test import RequestFactory, TestCase -from django.test.utils import override_settings +from django.test.utils import modify_settings, override_settings from django.urls import reverse from django_otp import DEVICE_ID_SESSION_KEY from django_otp.oath import totp @@ -18,12 +18,27 @@ from .utils import UserMixin, totp_str +try: + from django.contrib.auth.middleware import LoginRequiredMiddleware # NOQA + has_login_required_middleware = True +except ImportError: + # Django < 5.1 + has_login_required_middleware = False + class LoginTest(UserMixin, TestCase): def _post(self, data=None): return self.client.post(reverse('two_factor:login'), data=data) - def test_form(self): + def test_get_to_login(self): + response = self.client.get(reverse('two_factor:login')) + self.assertContains(response, 'Password:') + + @skipUnless(has_login_required_middleware, 'LoginRequiredMiddleware needs Django 5.1+') + @modify_settings( + MIDDLEWARE={'append': 'django.contrib.auth.middleware.LoginRequiredMiddleware'} + ) + def test_get_to_login_with_loginrequiredmiddleware(self): response = self.client.get(reverse('two_factor:login')) self.assertContains(response, 'Password:') diff --git a/two_factor/views/core.py b/two_factor/views/core.py index fedf42196..0c429c99f 100644 --- a/two_factor/views/core.py +++ b/two_factor/views/core.py @@ -51,12 +51,27 @@ validate_remember_device_cookie, ) +try: + from django.contrib.auth.decorators import login_not_required +except ImportError: + # For Django < 5.1, copy the current Django implementation + def login_not_required(view_func): + """ + Decorator for views that allows access to unauthenticated requests. + """ + view_func.login_required = False + return view_func + + logger = logging.getLogger(__name__) REMEMBER_COOKIE_PREFIX = getattr(settings, 'TWO_FACTOR_REMEMBER_COOKIE_PREFIX', 'remember-cookie_') -@method_decorator([sensitive_post_parameters(), csrf_protect, never_cache], name='dispatch') +@method_decorator( + [login_not_required, sensitive_post_parameters(), csrf_protect, never_cache], + name='dispatch' +) class LoginView(RedirectURLMixin, IdempotentSessionWizardView): """ View for handling the login process, including OTP verification.