Skip to content

Commit 8f02f48

Browse files
authored
Merge pull request #757 from shawnz/shawnz/check-admin-site-otp-required
Fix infinite login redirect on admin site with AdminSiteOTPRequiredMixin
2 parents 2ecaff6 + e9da182 commit 8f02f48

File tree

2 files changed

+49
-2
lines changed

2 files changed

+49
-2
lines changed

tests/test_admin.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
from binascii import unhexlify
2+
13
from django.conf import settings
24
from django.shortcuts import resolve_url
35
from django.test import TestCase
46
from django.test.utils import override_settings
7+
from django.urls import reverse
8+
from django_otp.oath import totp
59

610
from two_factor.admin import patch_admin, unpatch_admin
711

8-
from .utils import UserMixin
12+
from .utils import UserMixin, method_registry
913

1014

1115
@override_settings(ROOT_URLCONF='tests.urls_admin')
@@ -48,15 +52,16 @@ class OTPAdminSiteTest(UserMixin, TestCase):
4852
def setUp(self):
4953
super().setUp()
5054
self.user = self.create_superuser()
51-
self.login_user()
5255

5356
def test_otp_admin_without_otp(self):
57+
self.login_user()
5458
response = self.client.get('/otp_admin/', follow=True)
5559
redirect_to = '%s?next=/otp_admin/' % resolve_url(settings.LOGIN_URL)
5660
self.assertRedirects(response, redirect_to)
5761

5862
@override_settings(LOGIN_URL='two_factor:login')
5963
def test_otp_admin_without_otp_named_url(self):
64+
self.login_user()
6065
response = self.client.get('/otp_admin/', follow=True)
6166
redirect_to = '%s?next=/otp_admin/' % resolve_url(settings.LOGIN_URL)
6267
self.assertRedirects(response, redirect_to)
@@ -66,3 +71,41 @@ def test_otp_admin_with_otp(self):
6671
self.login_user()
6772
response = self.client.get('/otp_admin/')
6873
self.assertEqual(response.status_code, 200)
74+
75+
@method_registry(['generator'])
76+
def test_otp_admin_login_redirect_without_otp(self):
77+
# Open URL that is protected
78+
# assert go to login page
79+
# assert the next param not in the session (but still in url)
80+
response = self.client.get('/otp_admin/', follow=True)
81+
redirect_to = '%s?next=/otp_admin/' % resolve_url(settings.LOGIN_URL)
82+
self.assertRedirects(response, redirect_to)
83+
self.assertEqual(self.client.session.get('next'), None)
84+
85+
# Log in given the last redirect
86+
# assert redirect to setup
87+
response = self.client.post(
88+
redirect_to,
89+
{'auth-username': 'bouke@example.com', 'auth-password': 'secret', 'login_view-current_step': 'auth'},
90+
)
91+
self.assertRedirects(response, reverse('two_factor:setup'))
92+
self.assertEqual(self.client.session.get('next'), '/otp_admin/')
93+
94+
# Setup the device accordingly
95+
# assert redirect to setup completed
96+
# assert button for redirection to the original page
97+
response = self.client.post(reverse('two_factor:setup'), data={'setup_view-current_step': 'welcome'})
98+
self.assertContains(response, 'Token:')
99+
self.assertContains(response, 'autofocus="autofocus"')
100+
self.assertContains(response, 'inputmode="numeric"')
101+
self.assertContains(response, 'autocomplete="one-time-code"')
102+
103+
key = response.context_data['keys'].get('generator')
104+
bin_key = unhexlify(key.encode())
105+
response = self.client.post(
106+
reverse('two_factor:setup'),
107+
data={'setup_view-current_step': 'generator', 'generator-token': totp(bin_key)},
108+
follow=True,
109+
)
110+
111+
self.assertRedirects(response, '/otp_admin/')

two_factor/views/mixins.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django.template.response import TemplateResponse
55
from django.urls import Resolver404, resolve, reverse
66

7+
from ..admin import AdminSiteOTPRequiredMixin
78
from ..utils import default_device
89

910

@@ -90,4 +91,7 @@ def is_otp_view(cls, view_path):
9091
return (
9192
hasattr(next_resolver_match.func, 'view_class') and
9293
issubclass(next_resolver_match.func.view_class, OTPRequiredMixin)
94+
) or (
95+
hasattr(next_resolver_match.func, 'admin_site') and
96+
isinstance(next_resolver_match.func.admin_site, AdminSiteOTPRequiredMixin)
9397
)

0 commit comments

Comments
 (0)