2FA: Support for adding TOTP-based devices

This commit is contained in:
Raphael Michel
2016-10-08 14:11:59 +02:00
parent 508a4f8e86
commit 2f24af824e
18 changed files with 290 additions and 17 deletions

View File

@@ -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>

View 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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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">

View File

@@ -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'),

View File

@@ -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
}))