mirror of
https://github.com/pretix/pretix.git
synced 2026-08-11 10:57:00 +00:00
2FA: Support for adding TOTP-based devices
This commit is contained in:
@@ -102,3 +102,10 @@ class UserSettingsForm(forms.ModelForm):
|
||||
self.instance.set_password(password1)
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
|
||||
class User2FADeviceAddForm(forms.Form):
|
||||
name = forms.CharField(label=_('Device name'))
|
||||
devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=(
|
||||
('totp', _('Smartphone with the Authenticator application')),
|
||||
))
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.1 on 2016-10-08 10:47
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0038_auto_20160924_1448'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='require_2fa',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -76,6 +76,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
timezone = models.CharField(max_length=100,
|
||||
default=settings.TIME_ZONE,
|
||||
verbose_name=_('Timezone'))
|
||||
require_2fa = models.BooleanField(default=False)
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}))
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from django.core.urlresolvers import resolve, reverse
|
||||
from django.dispatch import receiver
|
||||
from django.template.loader import get_template
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.control.signals import html_head, nav_event
|
||||
from pretix.control.signals import nav_event
|
||||
|
||||
|
||||
@receiver(nav_event, dispatch_uid="pretixdroid_nav")
|
||||
@@ -22,13 +21,3 @@ def control_nav_import(sender, request=None, **kwargs):
|
||||
'icon': 'android',
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@receiver(html_head, dispatch_uid="pretixdroid_html_head")
|
||||
def html_head_presale(sender, request=None, **kwargs):
|
||||
url = resolve(request.path_info)
|
||||
if url.namespace == 'plugins:pretixdroid':
|
||||
template = get_template('pretixplugins/pretixdroid/control_head.html')
|
||||
return template.render({})
|
||||
else:
|
||||
return ""
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{% load staticfiles %}
|
||||
|
||||
<script type="text/javascript" src="{% static "pretixplugins/pretixdroid/jquery.qrcode.min.js" %}"></script>
|
||||
@@ -176,6 +176,9 @@ INSTALLED_APPS = [
|
||||
'pretix.plugins.pretixdroid',
|
||||
'easy_thumbnails',
|
||||
'django_markup',
|
||||
'django_otp',
|
||||
'django_otp.plugins.otp_totp',
|
||||
'django_otp.plugins.otp_static',
|
||||
]
|
||||
|
||||
try:
|
||||
|
||||
@@ -10,6 +10,7 @@ git+https://github.com/pretix/PyPDF2.git@pretix#egg=PyPDF2
|
||||
easy-thumbnails>=2.2,<3
|
||||
django-libsass
|
||||
libsass
|
||||
django-otp==0.3.*
|
||||
# celery>=3.1,<3.2
|
||||
# until the following issue is fixed, we need our own celery version
|
||||
# https://github.com/celery/celery/pull/3199
|
||||
|
||||
@@ -123,4 +123,12 @@ $(function () {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(".qrcode-canvas").each(function() {
|
||||
$(this).qrcode(
|
||||
{
|
||||
text: $.trim($($(this).attr("data-qrdata")).html())
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,10 @@ td > .form-group > .checkbox {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.static-form-row {
|
||||
padding-top: 7px;
|
||||
}
|
||||
|
||||
.has-success .form-control {
|
||||
border-color: #cccccc;
|
||||
}
|
||||
|
||||
@@ -133,6 +133,12 @@ h1 .btn-sm {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.multi-step-tutorial {
|
||||
&> li {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
#loadingmodal, #ajaxerr {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
||||
Reference in New Issue
Block a user