mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
2FA: Support for adding TOTP-based devices
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
<script type="text/javascript" src="{% static "datetimepicker/bootstrap-datetimepicker.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "charts/raphael-min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "charts/morris.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/jquery.qrcode.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/menu.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/sb-admin-2.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/main.js" %}"></script>
|
||||
|
||||
18
src/pretix/control/templates/pretixcontrol/user/2fa_add.html
Normal file
18
src/pretix/control/templates/pretixcontrol/user/2fa_add.html
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Add a two-factor authentication device" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Add a two-factor authentication device" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.name layout='horizontal' %}
|
||||
{% bootstrap_field form.devicetype layout='horizontal' %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Continue" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,59 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Add a two-factor authentication device" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Add a two-factor authentication device" %}</h1>
|
||||
<p>
|
||||
{% trans "To set up this device, please follow the following steps:" %}
|
||||
</p>
|
||||
<ol class="multi-step-tutorial">
|
||||
<li>
|
||||
{% trans "Download the Google Authenticator application to your phone:" %}
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&"
|
||||
target="_blank">
|
||||
{% trans "Android (Google Play)" %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://f-droid.org/repository/browse/?fdfilter=authenticator&fdid=com.google.android.apps.authenticator2"
|
||||
target="_blank">
|
||||
{% trans "Android (F-Droid)" %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://itunes.apple.com/en/app/google-authenticator/id388497605?mt=8">
|
||||
{% trans "iOS (iTunes)" %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://m.google.com/authenticator">
|
||||
{% trans "Blackberry (Link via Google)" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
{% trans "Add a new account to the app by scanning the following barcode:" %}
|
||||
<div class="qrcode-canvas" data-qrdata="#qrdata"></div>
|
||||
</li>
|
||||
<li>
|
||||
{% trans "Enter the displayed code here:" %}
|
||||
<form class="form form-inline" method="post" action="">
|
||||
{% csrf_token %}
|
||||
<input type="number" name="token" class="form-control" required="required">
|
||||
<button class="btn btn-primary" type="submit">
|
||||
{% trans "Continue" %}
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<script type="text/json" id="qrdata">
|
||||
{{ qrdata|safe }}
|
||||
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,66 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Two-factor authentication" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Two-factor authentication" %}</h1>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Two-factor authentication is a way to add additional security to your account. If you enable it, you will
|
||||
not only need your password to log in, but also an additional token that is generated e.g. by an app on your
|
||||
smartphone or a hardware token generator and that changes on a regular basis.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% if user.require_2fa %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Two-factor status" %}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<a href="" class="btn btn-primary pull-right">Disable</a>
|
||||
<p>
|
||||
<strong>{% trans "Two-factor authentication is currently enabled." %}</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="panel panel-warning">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Two-factor status" %}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if devices|length %}
|
||||
<a href="" class="btn btn-primary pull-right">Enable</a>
|
||||
{% endif %}
|
||||
<p>
|
||||
<strong>{% trans "Two-factor authentication is currently disabled." %}</strong>
|
||||
</p>
|
||||
{% if not devices|length %}
|
||||
<p>{% trans "To enable it, you need to configure at least one device below." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Enabled devices" %}</h3>
|
||||
</div>
|
||||
<ul class="list-group">
|
||||
{% for d in devices %}
|
||||
<li class="list-group-item">
|
||||
{% if d.devicetype == "totp" %}
|
||||
<span class="fa fa-mobile"></span>
|
||||
{% endif %}
|
||||
{{ d.name }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
<li class="list-group-item">
|
||||
<a href="{% url "control:user.settings.2fa.add" %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus"></span>
|
||||
{% trans "Add a new device" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -19,6 +19,22 @@
|
||||
{% bootstrap_field form.email layout='horizontal' %}
|
||||
{% bootstrap_field form.new_pw layout='horizontal' %}
|
||||
{% bootstrap_field form.new_pw_repeat layout='horizontal' %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="id_new_pw_repeat">{% trans "Two-factor authentication" %}</label>
|
||||
<div class="col-md-9 static-form-row">
|
||||
{% if user.require_2fa %}
|
||||
<span class="label label-success">{% trans "Enabled" %}</span>
|
||||
<a href="{% url "control:user.settings.2fa" %}">
|
||||
{% trans "Change settings" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="label label-default">{% trans "Disabled" %}</span>
|
||||
<a href="{% url "control:user.settings.2fa" %}">
|
||||
{% trans "Enable" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
|
||||
@@ -13,6 +13,10 @@ urlpatterns = [
|
||||
url(r'^forgot/recover$', auth.Recover.as_view(), name='auth.forgot.recover'),
|
||||
url(r'^$', dashboards.user_index, name='index'),
|
||||
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/2fa/add$', user.User2FADeviceAddView.as_view(), name='user.settings.2fa.add'),
|
||||
url(r'^settings/2fa/totp/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmTOTPView.as_view(),
|
||||
name='user.settings.2fa.confirm.totp'),
|
||||
url(r'^organizers/$', organizer.OrganizerList.as_view(), name='organizers'),
|
||||
url(r'^organizers/add$', organizer.OrganizerCreate.as_view(), name='organizers.add'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/edit$', organizer.OrganizerUpdate.as_view(), name='organizer.edit'),
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import base64
|
||||
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.core.urlresolvers import reverse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import UpdateView
|
||||
from django.views.generic import FormView, TemplateView, UpdateView
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
|
||||
from pretix.base.forms.user import UserSettingsForm
|
||||
from pretix.base.forms.user import User2FADeviceAddForm, UserSettingsForm
|
||||
from pretix.base.models import User
|
||||
|
||||
|
||||
@@ -33,3 +40,69 @@ class UserSettings(UpdateView):
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:user.settings')
|
||||
|
||||
|
||||
class User2FAMainView(TemplateView):
|
||||
template_name = 'pretixcontrol/user/2fa_main.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
|
||||
ctx['devices'] = []
|
||||
for dt in (TOTPDevice,):
|
||||
objs = list(dt.objects.filter(user=self.request.user, confirmed=True))
|
||||
for obj in objs:
|
||||
if dt == TOTPDevice:
|
||||
obj.devicetype = 'totp'
|
||||
ctx['devices'] += objs
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
class User2FADeviceAddView(FormView):
|
||||
form_class = User2FADeviceAddForm
|
||||
template_name = 'pretixcontrol/user/2fa_add.html'
|
||||
|
||||
def form_valid(self, form):
|
||||
if form.cleaned_data['devicetype'] == 'totp':
|
||||
dev = TOTPDevice.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
|
||||
}))
|
||||
|
||||
|
||||
class User2FADeviceConfirmTOTPView(TemplateView):
|
||||
template_name = 'pretixcontrol/user/2fa_confirm_totp.html'
|
||||
|
||||
@cached_property
|
||||
def device(self):
|
||||
return get_object_or_404(TOTPDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=False)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
|
||||
ctx['secret'] = base64.b32encode(self.device.bin_key).decode('utf-8')
|
||||
ctx['qrdata'] = 'otpauth://totp/{label}%3A%20{user}?issuer={label}&secret={secret}&digits={digits}'.format(
|
||||
label=quote(settings.PRETIX_INSTANCE_NAME), user=quote(self.request.user.email),
|
||||
secret=ctx['secret'],
|
||||
digits=self.device.digits
|
||||
)
|
||||
ctx['device'] = self.device
|
||||
return ctx
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
token = request.POST.get('token', '')
|
||||
if self.device.verify_token(token):
|
||||
self.device.confirmed = True
|
||||
self.device.save()
|
||||
messages.success(request, _('The device has been verified and can now be used.'))
|
||||
return redirect(reverse('control:user.settings.2fa'))
|
||||
else:
|
||||
messages.error(request, _('The code you entered was not valid. If this problem persists, please check '
|
||||
'that the date and time of your phone are configured correctly.'))
|
||||
return redirect(reverse('control:user.settings.2fa.confirm.totp', kwargs={
|
||||
'device': self.device.pk
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user