mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
2FA: Login using a TOTP token
This commit is contained in:
@@ -21,6 +21,7 @@ class PermissionMiddleware(MiddlewareMixin):
|
||||
|
||||
EXCEPTIONS = (
|
||||
"auth.login",
|
||||
"auth.login.2fa",
|
||||
"auth.register",
|
||||
"auth.forgot",
|
||||
"auth.forgot.recover"
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{% extends "pretixcontrol/auth/base.html" %}
|
||||
{% load bootstrap3 %}
|
||||
{% load i18n %}
|
||||
{% load staticfiles %}
|
||||
{% block content %}
|
||||
<form class="form-signin" action="" method="post">
|
||||
{% csrf_token %}
|
||||
<h3>{% trans "Welcome back!" %}</h3>
|
||||
<p>
|
||||
{% trans "You configured your account two require authentification with a second medium, e.g. your phone. Please enter your verification code here:" %}
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<input class="form-control" name="token" placeholder="{% trans "Token" %}"
|
||||
type="number" required="required" autofocus="autofocus">
|
||||
</div>
|
||||
<div class="form-group buttons">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{% trans "Continue" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -25,7 +25,7 @@
|
||||
{% if user.require_2fa %}
|
||||
<span class="label label-success">{% trans "Enabled" %}</span>
|
||||
<a href="{% url "control:user.settings.2fa" %}">
|
||||
{% trans "Change settings" %}
|
||||
{% trans "Change two-factor settings" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="label label-default">{% trans "Disabled" %}</span>
|
||||
|
||||
@@ -8,6 +8,7 @@ from pretix.control.views import (
|
||||
urlpatterns = [
|
||||
url(r'^logout$', auth.logout, name='auth.logout'),
|
||||
url(r'^login$', auth.login, name='auth.login'),
|
||||
url(r'^login/2fa$', auth.Login2FAView.as_view(), name='auth.login.2fa'),
|
||||
url(r'^register$', auth.register, name='auth.register'),
|
||||
url(r'^forgot$', auth.Forgot.as_view(), name='auth.forgot'),
|
||||
url(r'^forgot/recover$', auth.Recover.as_view(), name='auth.forgot.recover'),
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import time
|
||||
from urllib.parse import quote
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import (
|
||||
@@ -6,9 +9,12 @@ from django.contrib.auth import (
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
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 TemplateView
|
||||
from django_otp import match_token
|
||||
|
||||
from pretix.base.forms.auth import (
|
||||
LoginForm, PasswordForgotForm, PasswordRecoverForm, RegistrationForm,
|
||||
@@ -29,10 +35,18 @@ def login(request):
|
||||
if request.method == 'POST':
|
||||
form = LoginForm(data=request.POST)
|
||||
if form.is_valid() and form.user_cache:
|
||||
auth_login(request, form.user_cache)
|
||||
if "next" in request.GET:
|
||||
return redirect(request.GET.get("next", 'control:index'))
|
||||
return redirect('control:index')
|
||||
if form.user_cache.require_2fa:
|
||||
request.session['pretix_auth_2fa_user'] = form.user_cache.pk
|
||||
request.session['pretix_auth_2fa_time'] = str(int(time.time()))
|
||||
twofa_url = reverse('control:auth.login.2fa')
|
||||
if 'next' in request.GET:
|
||||
twofa_url += '?next=' + quote(request.GET.get('next'))
|
||||
return redirect(twofa_url)
|
||||
else:
|
||||
auth_login(request, form.user_cache)
|
||||
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:
|
||||
form = LoginForm()
|
||||
ctx['form'] = form
|
||||
@@ -188,3 +202,36 @@ class Recover(TemplateView):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['form'] = self.form
|
||||
return context
|
||||
|
||||
|
||||
class Login2FAView(TemplateView):
|
||||
template_name = 'pretixcontrol/auth/login_2fa.html'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
fail = False
|
||||
if 'pretix_auth_2fa_user' not in request.session:
|
||||
fail = True
|
||||
else:
|
||||
try:
|
||||
self.user = User.objects.get(pk=request.session['pretix_auth_2fa_user'], is_active=True)
|
||||
except User.DoesNotExist:
|
||||
fail = True
|
||||
logintime = int(request.session.get('pretix_auth_2fa_time', '1'))
|
||||
if time.time() - logintime > 300:
|
||||
fail = True
|
||||
if fail:
|
||||
messages.error(request, _('Please try again.'))
|
||||
return redirect('control:auth.login')
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if match_token(self.user, request.POST.get('token', '')):
|
||||
auth_login(request, self.user)
|
||||
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")):
|
||||
return redirect(request.GET.get("next"))
|
||||
return redirect(reverse('control:index'))
|
||||
else:
|
||||
messages.error(request, _('Invalid code, please try again.'))
|
||||
return redirect('control:auth.login.2fa')
|
||||
|
||||
Reference in New Issue
Block a user