diff --git a/src/pretix/control/templates/pretixcontrol/user/reauth.html b/src/pretix/control/templates/pretixcontrol/user/reauth.html
new file mode 100644
index 0000000000..d5cf90a355
--- /dev/null
+++ b/src/pretix/control/templates/pretixcontrol/user/reauth.html
@@ -0,0 +1,26 @@
+{% extends "pretixcontrol/base.html" %}
+{% load i18n %}
+{% load bootstrap3 %}
+{% block content %}
+
+{% endblock %}
diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py
index 039bc33fcd..989ff70c98 100644
--- a/src/pretix/control/urls.py
+++ b/src/pretix/control/urls.py
@@ -13,6 +13,7 @@ urlpatterns = [
url(r'^forgot$', auth.Forgot.as_view(), name='auth.forgot'),
url(r'^forgot/recover$', auth.Recover.as_view(), name='auth.forgot.recover'),
url(r'^$', dashboards.user_index, name='index'),
+ url(r'^reauth/$', user.ReauthView.as_view(), name='user.reauth'),
url(r'^settings$', user.UserSettings.as_view(), name='user.settings'),
url(r'^settings/2fa/$', user.User2FAMainView.as_view(), name='user.settings.2fa'),
url(r'^settings/history/$', user.UserHistoryView.as_view(), name='user.settings.history'),
diff --git a/src/pretix/control/views/auth.py b/src/pretix/control/views/auth.py
index 273cb2d1d3..a698ad580f 100644
--- a/src/pretix/control/views/auth.py
+++ b/src/pretix/control/views/auth.py
@@ -50,6 +50,7 @@ def login(request):
return redirect(twofa_url)
else:
auth_login(request, form.user_cache)
+ request.session['pretix_auth_login_time'] = int(time.time())
if "next" in request.GET and is_safe_url(request.GET.get("next")):
return redirect(request.GET.get("next"))
return redirect(reverse('control:index'))
@@ -66,6 +67,7 @@ def logout(request):
Log the user out of the current session, then redirect to login page.
"""
auth_logout(request)
+ request.session['pretix_auth_login_time'] = 0
return redirect('control:auth.login')
@@ -89,6 +91,7 @@ def register(request):
user = authenticate(email=user.email, password=form.cleaned_data['password'])
user.log_action('pretix.control.auth.user.created', user=user)
auth_login(request, user)
+ request.session['pretix_auth_login_time'] = int(time.time())
return redirect('control:index')
else:
form = RegistrationForm()
@@ -256,6 +259,7 @@ class Login2FAView(TemplateView):
if valid:
auth_login(request, self.user)
+ request.session['pretix_auth_login_time'] = int(time.time())
del request.session['pretix_auth_2fa_user']
del request.session['pretix_auth_2fa_time']
if "next" in request.GET and is_safe_url(request.GET.get("next")):
diff --git a/src/pretix/control/views/user.py b/src/pretix/control/views/user.py
index 1cc31fac08..16d6de8c88 100644
--- a/src/pretix/control/views/user.py
+++ b/src/pretix/control/views/user.py
@@ -1,16 +1,16 @@
import base64
-import copy
import logging
+import time
from urllib.parse import quote
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import update_session_auth_hash
-from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from django.shortcuts import get_object_or_404, redirect
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
+from django.utils.http import is_safe_url
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
@@ -19,13 +19,38 @@ from u2flib_server import u2f
from u2flib_server.jsapi import DeviceRegistration
from pretix.base.forms.user import User2FADeviceAddForm, UserSettingsForm
-from pretix.base.models import LogEntry, U2FDevice, User
+from pretix.base.models import U2FDevice, User
from pretix.control.views.auth import get_u2f_appid
REAL_DEVICE_TYPES = (TOTPDevice, U2FDevice)
logger = logging.getLogger(__name__)
+class RecentAuthenticationRequiredMixin:
+ max_time = 3600
+
+ def dispatch(self, request, *args, **kwargs):
+ tdelta = time.time() - request.session.get('pretix_auth_login_time', 0)
+ if tdelta > self.max_time:
+ return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
+ return super().dispatch(request, *args, **kwargs)
+
+
+class ReauthView(TemplateView):
+ template_name = 'pretixcontrol/user/reauth.html'
+
+ def post(self, request, *args, **kwargs):
+ password = request.POST.get("password", "")
+ if request.user.check_password(password):
+ request.session['pretix_auth_login_time'] = int(time.time())
+ if "next" in request.GET and is_safe_url(request.GET.get("next")):
+ return redirect(request.GET.get("next"))
+ return redirect(reverse('control:index'))
+ else:
+ messages.error(request, _('The password you entered was invalid, please try again.'))
+ return self.get(request, *args, **kwargs)
+
+
class UserSettings(UpdateView):
model = User
form_class = UserSettingsForm
@@ -87,7 +112,7 @@ class UserHistoryView(TemplateView):
return ctx
-class User2FAMainView(TemplateView):
+class User2FAMainView(RecentAuthenticationRequiredMixin, TemplateView):
template_name = 'pretixcontrol/user/2fa_main.html'
def get_context_data(self, **kwargs):
@@ -114,7 +139,7 @@ class User2FAMainView(TemplateView):
return ctx
-class User2FADeviceAddView(FormView):
+class User2FADeviceAddView(RecentAuthenticationRequiredMixin, FormView):
form_class = User2FADeviceAddForm
template_name = 'pretixcontrol/user/2fa_add.html'
@@ -131,7 +156,7 @@ class User2FADeviceAddView(FormView):
}))
-class User2FADeviceDeleteView(TemplateView):
+class User2FADeviceDeleteView(RecentAuthenticationRequiredMixin, TemplateView):
template_name = 'pretixcontrol/user/2fa_delete.html'
@cached_property
@@ -167,7 +192,7 @@ class User2FADeviceDeleteView(TemplateView):
return redirect(reverse('control:user.settings.2fa'))
-class User2FADeviceConfirmU2FView(TemplateView):
+class User2FADeviceConfirmU2FView(RecentAuthenticationRequiredMixin, TemplateView):
template_name = 'pretixcontrol/user/2fa_confirm_u2f.html'
@property
@@ -217,7 +242,7 @@ class User2FADeviceConfirmU2FView(TemplateView):
}))
-class User2FADeviceConfirmTOTPView(TemplateView):
+class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateView):
template_name = 'pretixcontrol/user/2fa_confirm_totp.html'
@cached_property
@@ -260,7 +285,7 @@ class User2FADeviceConfirmTOTPView(TemplateView):
}))
-class User2FAEnableView(TemplateView):
+class User2FAEnableView(RecentAuthenticationRequiredMixin, TemplateView):
template_name = 'pretixcontrol/user/2fa_enable.html'
def dispatch(self, request, *args, **kwargs):
@@ -281,7 +306,7 @@ class User2FAEnableView(TemplateView):
return redirect(reverse('control:user.settings.2fa'))
-class User2FADisableView(TemplateView):
+class User2FADisableView(RecentAuthenticationRequiredMixin, TemplateView):
template_name = 'pretixcontrol/user/2fa_disable.html'
def post(self, request, *args, **kwargs):
@@ -295,7 +320,7 @@ class User2FADisableView(TemplateView):
return redirect(reverse('control:user.settings.2fa'))
-class User2FARegenerateEmergencyView(TemplateView):
+class User2FARegenerateEmergencyView(RecentAuthenticationRequiredMixin, TemplateView):
template_name = 'pretixcontrol/user/2fa_regenemergency.html'
def post(self, request, *args, **kwargs):
diff --git a/src/tests/control/test_user.py b/src/tests/control/test_user.py
index 41c3cacbe6..8eec35eff2 100644
--- a/src/tests/control/test_user.py
+++ b/src/tests/control/test_user.py
@@ -120,6 +120,24 @@ class UserSettings2FATest(SoupTest):
super().setUp()
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
self.client.login(email='dummy@dummy.dummy', password='dummy')
+ session = self.client.session
+ session['pretix_auth_login_time'] = int(time.time())
+ session.save()
+
+ def test_require_reauth(self):
+ session = self.client.session
+ session['pretix_auth_login_time'] = int(time.time()) - 3600 * 2
+ session.save()
+
+ response = self.client.get('/control/settings/2fa/')
+ self.assertIn('/control/reauth', response['Location'])
+ self.assertEqual(response.status_code, 302)
+
+ response = self.client.post('/control/reauth/?next=/control/settings/2fa/', {
+ 'password': 'dummy'
+ })
+ self.assertIn('/control/settings/2fa/', response['Location'])
+ self.assertEqual(response.status_code, 302)
def test_enable_require_device(self):
r = self.client.post('/control/settings/2fa/enable', follow=True)