forked from CGM_Public/pretix_original
2FA: Login using a TOTP token
This commit is contained in:
@@ -21,6 +21,7 @@ class PermissionMiddleware(MiddlewareMixin):
|
|||||||
|
|
||||||
EXCEPTIONS = (
|
EXCEPTIONS = (
|
||||||
"auth.login",
|
"auth.login",
|
||||||
|
"auth.login.2fa",
|
||||||
"auth.register",
|
"auth.register",
|
||||||
"auth.forgot",
|
"auth.forgot",
|
||||||
"auth.forgot.recover"
|
"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 %}
|
{% if user.require_2fa %}
|
||||||
<span class="label label-success">{% trans "Enabled" %}</span>
|
<span class="label label-success">{% trans "Enabled" %}</span>
|
||||||
<a href="{% url "control:user.settings.2fa" %}">
|
<a href="{% url "control:user.settings.2fa" %}">
|
||||||
{% trans "Change settings" %}
|
{% trans "Change two-factor settings" %}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="label label-default">{% trans "Disabled" %}</span>
|
<span class="label label-default">{% trans "Disabled" %}</span>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from pretix.control.views import (
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^logout$', auth.logout, name='auth.logout'),
|
url(r'^logout$', auth.logout, name='auth.logout'),
|
||||||
url(r'^login$', auth.login, name='auth.login'),
|
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'^register$', auth.register, name='auth.register'),
|
||||||
url(r'^forgot$', auth.Forgot.as_view(), name='auth.forgot'),
|
url(r'^forgot$', auth.Forgot.as_view(), name='auth.forgot'),
|
||||||
url(r'^forgot/recover$', auth.Recover.as_view(), name='auth.forgot.recover'),
|
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.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth import (
|
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.contrib.auth.tokens import default_token_generator
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.functional import cached_property
|
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.utils.translation import ugettext_lazy as _
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
from django_otp import match_token
|
||||||
|
|
||||||
from pretix.base.forms.auth import (
|
from pretix.base.forms.auth import (
|
||||||
LoginForm, PasswordForgotForm, PasswordRecoverForm, RegistrationForm,
|
LoginForm, PasswordForgotForm, PasswordRecoverForm, RegistrationForm,
|
||||||
@@ -29,10 +35,18 @@ def login(request):
|
|||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = LoginForm(data=request.POST)
|
form = LoginForm(data=request.POST)
|
||||||
if form.is_valid() and form.user_cache:
|
if form.is_valid() and form.user_cache:
|
||||||
auth_login(request, form.user_cache)
|
if form.user_cache.require_2fa:
|
||||||
if "next" in request.GET:
|
request.session['pretix_auth_2fa_user'] = form.user_cache.pk
|
||||||
return redirect(request.GET.get("next", 'control:index'))
|
request.session['pretix_auth_2fa_time'] = str(int(time.time()))
|
||||||
return redirect('control:index')
|
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:
|
else:
|
||||||
form = LoginForm()
|
form = LoginForm()
|
||||||
ctx['form'] = form
|
ctx['form'] = form
|
||||||
@@ -188,3 +202,36 @@ class Recover(TemplateView):
|
|||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['form'] = self.form
|
context['form'] = self.form
|
||||||
return context
|
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