mirror of
https://github.com/pretix/pretix.git
synced 2026-08-21 12:36:26 +00:00
2FA: Added tests
This commit is contained in:
@@ -16,8 +16,8 @@ from django.utils.http import is_safe_url
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import TemplateView
|
||||
from django_otp import match_token
|
||||
from u2flib_server import u2f
|
||||
from u2flib_server.jsapi import DeviceRegistration
|
||||
from u2flib_server.u2f import start_authenticate, verify_authenticate
|
||||
from u2flib_server.utils import rand_bytes
|
||||
|
||||
from pretix.base.forms.auth import (
|
||||
@@ -247,7 +247,7 @@ class Login2FAView(TemplateView):
|
||||
for device in U2FDevice.objects.filter(confirmed=True, user=self.user)]
|
||||
challenge = self.request.session.pop('_u2f_challenge')
|
||||
try:
|
||||
verify_authenticate(devices, challenge, token, [self.app_id])
|
||||
u2f.verify_authenticate(devices, challenge, token, [self.app_id])
|
||||
valid = True
|
||||
except Exception:
|
||||
logger.exception('U2F login failed')
|
||||
@@ -271,11 +271,12 @@ class Login2FAView(TemplateView):
|
||||
devices = [DeviceRegistration.wrap(device.json_data)
|
||||
for device in U2FDevice.objects.filter(confirmed=True, user=self.user)]
|
||||
if devices:
|
||||
challenge = start_authenticate(devices, challenge=rand_bytes(32))
|
||||
challenge = u2f.start_authenticate(devices, challenge=rand_bytes(32))
|
||||
self.request.session['_u2f_challenge'] = challenge.json
|
||||
ctx['jsondata'] = challenge.json
|
||||
else:
|
||||
del self.request.session['_u2f_challenge']
|
||||
if '_u2f_challenge' in self.request.session:
|
||||
del self.request.session['_u2f_challenge']
|
||||
ctx['jsondata'] = None
|
||||
|
||||
return ctx
|
||||
|
||||
@@ -13,8 +13,8 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import FormView, TemplateView, UpdateView
|
||||
from django_otp.plugins.otp_static.models import StaticDevice
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from u2flib_server import u2f
|
||||
from u2flib_server.jsapi import DeviceRegistration
|
||||
from u2flib_server.u2f import complete_register, start_register
|
||||
|
||||
from pretix.base.forms.user import User2FADeviceAddForm, UserSettingsForm
|
||||
from pretix.base.models import U2FDevice, User
|
||||
@@ -90,9 +90,6 @@ class User2FADeviceAddView(FormView):
|
||||
messages.error(self.request, _('U2F devices are only available if pretix is served via HTTPS.'))
|
||||
return self.get(self.request, self.args, self.kwargs)
|
||||
dev = U2FDevice.objects.create(user=self.request.user, confirmed=False, name=form.cleaned_data['name'])
|
||||
else:
|
||||
messages.error(self.request, _('Unknown device type'))
|
||||
return self.get(self.request, self.args, self.kwargs)
|
||||
return redirect(reverse('control:user.settings.2fa.confirm.' + form.cleaned_data['devicetype'], kwargs={
|
||||
'device': dev.pk
|
||||
}))
|
||||
@@ -139,7 +136,7 @@ class User2FADeviceConfirmU2FView(TemplateView):
|
||||
|
||||
devices = [DeviceRegistration.wrap(device.json_data)
|
||||
for device in U2FDevice.objects.filter(confirmed=True, user=self.request.user)]
|
||||
enroll = start_register(self.app_id, devices)
|
||||
enroll = u2f.start_register(self.app_id, devices)
|
||||
self.request.session['_u2f_enroll'] = enroll.json
|
||||
ctx['jsondata'] = enroll.json
|
||||
|
||||
@@ -147,9 +144,9 @@ class User2FADeviceConfirmU2FView(TemplateView):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
try:
|
||||
binding, cert = complete_register(self.request.session.pop('_u2f_enroll'),
|
||||
request.POST.get('token'),
|
||||
[self.app_id])
|
||||
binding, cert = u2f.complete_register(self.request.session.pop('_u2f_enroll'),
|
||||
request.POST.get('token'),
|
||||
[self.app_id])
|
||||
self.device.json_data = binding.json
|
||||
self.device.confirmed = True
|
||||
self.device.save()
|
||||
@@ -158,7 +155,7 @@ class User2FADeviceConfirmU2FView(TemplateView):
|
||||
except Exception:
|
||||
messages.error(request, _('The registration could not be completed. Please try again.'))
|
||||
logger.exception('U2F registration failed')
|
||||
return redirect(reverse('control:user.settings.2fa.confirm.2fa', kwargs={
|
||||
return redirect(reverse('control:user.settings.2fa.confirm.u2f', kwargs={
|
||||
'device': self.device.pk
|
||||
}))
|
||||
|
||||
@@ -225,7 +222,7 @@ class User2FADisableView(TemplateView):
|
||||
|
||||
|
||||
class User2FARegenerateEmergencyView(TemplateView):
|
||||
template_name = 'pretixcontrol/user/2fa_regenermergency.html'
|
||||
template_name = 'pretixcontrol/user/2fa_regenemergency.html'
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
d = StaticDevice.objects.get(user=self.request.user, name='emergency')
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import time
|
||||
from datetime import date, timedelta
|
||||
|
||||
from _pytest import monkeypatch
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.tokens import (
|
||||
PasswordResetTokenGenerator, default_token_generator,
|
||||
)
|
||||
from django.core import mail as djmail
|
||||
from django.test import TestCase
|
||||
from django_otp.oath import TOTP
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from u2flib_server.jsapi import JSONDict
|
||||
|
||||
from pretix.base.models import User
|
||||
from pretix.base.models import U2FDevice, User
|
||||
|
||||
|
||||
class LoginFormTest(TestCase):
|
||||
@@ -47,6 +52,18 @@ class LoginFormTest(TestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn('/control/events/', response['Location'])
|
||||
|
||||
def test_redirect_to_2fa(self):
|
||||
self.user.require_2fa = True
|
||||
self.user.save()
|
||||
response = self.client.post('/control/login?next=/control/events/', {
|
||||
'email': 'dummy@dummy.dummy',
|
||||
'password': 'dummy',
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn('/control/login/2fa?next=/control/events/', response['Location'])
|
||||
assert self.client.session['pretix_auth_2fa_user'] == self.user.pk
|
||||
assert 'pretix_auth_2fa_time' in self.client.session
|
||||
|
||||
def test_logged_in(self):
|
||||
response = self.client.post('/control/login?next=/control/events/', {
|
||||
'email': 'dummy@dummy.dummy',
|
||||
@@ -172,6 +189,95 @@ class RegistrationFormTest(TestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
|
||||
class Login2FAFormTest(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy', require_2fa=True)
|
||||
session = self.client.session
|
||||
session['pretix_auth_2fa_user'] = self.user.pk
|
||||
session['pretix_auth_2fa_time'] = str(int(time.time()))
|
||||
session.save()
|
||||
|
||||
def test_invalid_session(self):
|
||||
session = self.client.session
|
||||
session['pretix_auth_2fa_user'] = self.user.pk + 12
|
||||
session['pretix_auth_2fa_time'] = str(int(time.time()))
|
||||
session.save()
|
||||
response = self.client.get('/control/login/2fa')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn('/control/login', response['Location'])
|
||||
|
||||
def test_expired_session(self):
|
||||
session = self.client.session
|
||||
session['pretix_auth_2fa_user'] = self.user.pk + 12
|
||||
session['pretix_auth_2fa_time'] = str(int(time.time()) - 3600)
|
||||
session.save()
|
||||
response = self.client.get('/control/login/2fa')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn('/control/login', response['Location'])
|
||||
|
||||
def test_totp_invalid(self):
|
||||
response = self.client.get('/control/login/2fa')
|
||||
assert 'token' in response.rendered_content
|
||||
d = TOTPDevice.objects.create(user=self.user, name='test')
|
||||
totp = TOTP(d.bin_key, d.step, d.t0, d.digits, d.drift)
|
||||
totp.time = time.time()
|
||||
response = self.client.post('/control/login/2fa'.format(d.pk), {
|
||||
'token': str(totp.token() + 2)
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn('/control/login/2fa', response['Location'])
|
||||
|
||||
def test_totp_valid(self):
|
||||
response = self.client.get('/control/login/2fa')
|
||||
assert 'token' in response.rendered_content
|
||||
d = TOTPDevice.objects.create(user=self.user, name='test')
|
||||
totp = TOTP(d.bin_key, d.step, d.t0, d.digits, d.drift)
|
||||
totp.time = time.time()
|
||||
response = self.client.post('/control/login/2fa?next=/control/events/'.format(d.pk), {
|
||||
'token': str(totp.token())
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn('/control/events/', response['Location'])
|
||||
|
||||
def test_u2f_invalid(self):
|
||||
def fail(*args, **kwargs):
|
||||
raise Exception("Failed")
|
||||
|
||||
m = monkeypatch.monkeypatch()
|
||||
m.setattr("u2flib_server.u2f.verify_authenticate", fail)
|
||||
m.setattr("u2flib_server.u2f.start_authenticate",
|
||||
lambda *args, **kwargs: JSONDict({'authenticateRequests': []}))
|
||||
d = U2FDevice.objects.create(user=self.user, name='test', json_data="{}")
|
||||
|
||||
response = self.client.get('/control/login/2fa')
|
||||
assert 'token' in response.rendered_content
|
||||
response = self.client.post('/control/login/2fa'.format(d.pk), {
|
||||
'token': '{"response": "true"}'
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn('/control/login/2fa', response['Location'])
|
||||
|
||||
m.undo()
|
||||
|
||||
def test_u2f_valid(self):
|
||||
m = monkeypatch.monkeypatch()
|
||||
m.setattr("u2flib_server.u2f.verify_authenticate", lambda *args, **kwargs: True)
|
||||
m.setattr("u2flib_server.u2f.start_authenticate",
|
||||
lambda *args, **kwargs: JSONDict({'authenticateRequests': []}))
|
||||
d = U2FDevice.objects.create(user=self.user, name='test', json_data="{}")
|
||||
|
||||
response = self.client.get('/control/login/2fa')
|
||||
assert 'token' in response.rendered_content
|
||||
response = self.client.post('/control/login/2fa'.format(d.pk), {
|
||||
'token': '{"response": "true"}'
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn('/control/', response['Location'])
|
||||
|
||||
m.undo()
|
||||
|
||||
|
||||
class PasswordRecoveryFormTest(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
from tests.base import SoupTest, extract_form_fields
|
||||
import time
|
||||
|
||||
from pretix.base.models import User
|
||||
from _pytest import monkeypatch
|
||||
from django_otp.oath import TOTP
|
||||
from django_otp.plugins.otp_static.models import StaticDevice
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from tests.base import SoupTest, extract_form_fields
|
||||
from u2flib_server.jsapi import JSONDict
|
||||
|
||||
from pretix.base.models import U2FDevice, User
|
||||
from pretix.testutils.mock import mocker_context
|
||||
|
||||
|
||||
class UserSettingsTest(SoupTest):
|
||||
@@ -14,7 +22,6 @@ class UserSettingsTest(SoupTest):
|
||||
def save(self, data):
|
||||
form_data = self.form_data.copy()
|
||||
form_data.update(data)
|
||||
print(form_data)
|
||||
return self.post_doc('/control/settings', form_data)
|
||||
|
||||
def test_set_name(self):
|
||||
@@ -106,3 +113,145 @@ class UserSettingsTest(SoupTest):
|
||||
pw = self.user.password
|
||||
self.user = User.objects.get(pk=self.user.pk)
|
||||
assert self.user.password == pw
|
||||
|
||||
|
||||
class UserSettings2FATest(SoupTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
||||
self.client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
|
||||
def test_enable_require_device(self):
|
||||
r = self.client.post('/control/settings/2fa/enable', follow=True)
|
||||
assert 'alert-danger' in r.rendered_content
|
||||
self.user.refresh_from_db()
|
||||
assert not self.user.require_2fa
|
||||
|
||||
def test_enable(self):
|
||||
U2FDevice.objects.create(user=self.user, name='Test')
|
||||
r = self.client.post('/control/settings/2fa/enable', follow=True)
|
||||
assert 'alert-success' in r.rendered_content
|
||||
self.user.refresh_from_db()
|
||||
assert self.user.require_2fa
|
||||
|
||||
def test_disable(self):
|
||||
self.user.require_2fa = True
|
||||
self.user.save()
|
||||
r = self.client.post('/control/settings/2fa/disable', follow=True)
|
||||
assert 'alert-success' in r.rendered_content
|
||||
self.user.refresh_from_db()
|
||||
assert not self.user.require_2fa
|
||||
|
||||
def test_gen_emergency(self):
|
||||
self.client.get('/control/settings/2fa/')
|
||||
d = StaticDevice.objects.get(user=self.user, name='emergency')
|
||||
assert d.token_set.count() == 10
|
||||
old_tokens = set(t.token for t in d.token_set.all())
|
||||
self.client.post('/control/settings/2fa/regenemergency')
|
||||
new_tokens = set(t.token for t in d.token_set.all())
|
||||
assert d.token_set.count() == 10
|
||||
assert old_tokens != new_tokens
|
||||
|
||||
def test_delete_u2f(self):
|
||||
d = U2FDevice.objects.create(user=self.user, name='Test')
|
||||
self.client.get('/control/settings/2fa/u2f/{}/delete'.format(d.pk))
|
||||
self.client.post('/control/settings/2fa/u2f/{}/delete'.format(d.pk))
|
||||
assert not U2FDevice.objects.exists()
|
||||
|
||||
def test_delete_totp(self):
|
||||
d = TOTPDevice.objects.create(user=self.user, name='Test')
|
||||
self.client.get('/control/settings/2fa/totp/{}/delete'.format(d.pk))
|
||||
self.client.post('/control/settings/2fa/totp/{}/delete'.format(d.pk))
|
||||
assert not TOTPDevice.objects.exists()
|
||||
|
||||
def test_create_u2f_require_https(self):
|
||||
r = self.client.post('/control/settings/2fa/add', {
|
||||
'devicetype': 'u2f',
|
||||
'name': 'Foo'
|
||||
})
|
||||
assert 'alert-danger' in r.rendered_content
|
||||
|
||||
def test_create_u2f(self):
|
||||
with mocker_context() as mocker:
|
||||
mocker.patch('django.http.request.HttpRequest.is_secure')
|
||||
self.client.post('/control/settings/2fa/add', {
|
||||
'devicetype': 'u2f',
|
||||
'name': 'Foo'
|
||||
})
|
||||
d = U2FDevice.objects.first()
|
||||
assert d.name == 'Foo'
|
||||
assert not d.confirmed
|
||||
|
||||
def test_create_totp(self):
|
||||
self.client.post('/control/settings/2fa/add', {
|
||||
'devicetype': 'totp',
|
||||
'name': 'Foo'
|
||||
})
|
||||
d = TOTPDevice.objects.first()
|
||||
assert d.name == 'Foo'
|
||||
|
||||
def test_confirm_totp(self):
|
||||
self.client.post('/control/settings/2fa/add', {
|
||||
'devicetype': 'totp',
|
||||
'name': 'Foo'
|
||||
}, follow=True)
|
||||
d = TOTPDevice.objects.first()
|
||||
totp = TOTP(d.bin_key, d.step, d.t0, d.digits, d.drift)
|
||||
totp.time = time.time()
|
||||
r = self.client.post('/control/settings/2fa/totp/{}/confirm'.format(d.pk), {
|
||||
'token': str(totp.token())
|
||||
}, follow=True)
|
||||
d.refresh_from_db()
|
||||
assert d.confirmed
|
||||
assert 'alert-success' in r.rendered_content
|
||||
|
||||
def test_confirm_totp_failed(self):
|
||||
self.client.post('/control/settings/2fa/add', {
|
||||
'devicetype': 'totp',
|
||||
'name': 'Foo'
|
||||
}, follow=True)
|
||||
d = TOTPDevice.objects.first()
|
||||
totp = TOTP(d.bin_key, d.step, d.t0, d.digits, d.drift)
|
||||
totp.time = time.time()
|
||||
r = self.client.post('/control/settings/2fa/totp/{}/confirm'.format(d.pk), {
|
||||
'token': str(totp.token() - 2)
|
||||
}, follow=True)
|
||||
assert 'alert-danger' in r.rendered_content
|
||||
d.refresh_from_db()
|
||||
assert not d.confirmed
|
||||
|
||||
def test_confirm_u2f_failed(self):
|
||||
with mocker_context() as mocker:
|
||||
mocker.patch('django.http.request.HttpRequest.is_secure')
|
||||
self.client.post('/control/settings/2fa/add', {
|
||||
'devicetype': 'u2f',
|
||||
'name': 'Foo'
|
||||
}, follow=True)
|
||||
d = U2FDevice.objects.first()
|
||||
r = self.client.post('/control/settings/2fa/u2f/{}/confirm'.format(d.pk), {
|
||||
'token': 'FOO'
|
||||
}, follow=True)
|
||||
assert 'alert-danger' in r.rendered_content
|
||||
d.refresh_from_db()
|
||||
assert not d.confirmed
|
||||
|
||||
def test_confirm_u2f_success(self):
|
||||
with mocker_context() as mocker:
|
||||
mocker.patch('django.http.request.HttpRequest.is_secure')
|
||||
self.client.post('/control/settings/2fa/add', {
|
||||
'devicetype': 'u2f',
|
||||
'name': 'Foo'
|
||||
}, follow=True)
|
||||
|
||||
m = monkeypatch.monkeypatch()
|
||||
m.setattr("u2flib_server.u2f.complete_register", lambda *args, **kwargs: (JSONDict({}), None))
|
||||
|
||||
d = U2FDevice.objects.first()
|
||||
r = self.client.post('/control/settings/2fa/u2f/{}/confirm'.format(d.pk), {
|
||||
'token': 'FOO'
|
||||
}, follow=True)
|
||||
d.refresh_from_db()
|
||||
assert d.confirmed
|
||||
assert 'alert-success' in r.rendered_content
|
||||
|
||||
m.undo()
|
||||
|
||||
Reference in New Issue
Block a user