diff --git a/src/pretix/base/migrations/0040_u2fdevice.py b/src/pretix/base/migrations/0040_u2fdevice.py new file mode 100644 index 000000000..f4746d1a8 --- /dev/null +++ b/src/pretix/base/migrations/0040_u2fdevice.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-10-08 15:38 +from __future__ import unicode_literals + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0039_user_require_2fa'), + ] + + operations = [ + migrations.CreateModel( + name='U2FDevice', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='The human-readable name of this device.', max_length=64)), + ('confirmed', models.BooleanField(default=True, help_text='Is this device ready for use?')), + ('json_data', models.TextField()), + ('user', models.ForeignKey(help_text='The user that this device belongs to.', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/src/pretix/base/models/__init__.py b/src/pretix/base/models/__init__.py index ec9b5c7da..a6cd2605f 100644 --- a/src/pretix/base/models/__init__.py +++ b/src/pretix/base/models/__init__.py @@ -1,4 +1,4 @@ -from .auth import User +from .auth import U2FDevice, User from .base import CachedFile, LoggedModel, cachedfile_name from .event import Event, EventLock, EventPermission, EventSetting from .invoices import Invoice, InvoiceLine, invoice_filename diff --git a/src/pretix/base/models/auth.py b/src/pretix/base/models/auth.py index 2ef8ccc00..ee487057f 100644 --- a/src/pretix/base/models/auth.py +++ b/src/pretix/base/models/auth.py @@ -4,6 +4,7 @@ from django.contrib.auth.models import ( ) from django.db import models from django.utils.translation import ugettext_lazy as _ +from django_otp.models import Device from .base import LoggingMixin @@ -126,3 +127,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin): } else: return self.email + + +class U2FDevice(Device): + json_data = models.TextField() diff --git a/src/pretix/control/templates/pretixcontrol/auth/login_2fa.html b/src/pretix/control/templates/pretixcontrol/auth/login_2fa.html index ddee5850d..5f4821d25 100644 --- a/src/pretix/control/templates/pretixcontrol/auth/login_2fa.html +++ b/src/pretix/control/templates/pretixcontrol/auth/login_2fa.html @@ -2,8 +2,9 @@ {% load bootstrap3 %} {% load i18n %} {% load staticfiles %} +{% load compress %} {% block content %} -
+ {% csrf_token %}

{% trans "Welcome back!" %}

@@ -11,12 +12,31 @@

+ type="text" required="required" autofocus="autofocus" id="u2f-response">
+
+ {% trans "U2F failed. Check that the correct authentication device is correctly plugged in." %} +
+ {% if jsondata %} +

+ {% trans "Alternatively, connect your U2F device. If it has a button, touch it now. You might have to unplug the device and plug it back in again." %} +

+ {% endif %}
+ {% if jsondata %} + + {% endif %} + {% compress js %} + + + + {% endcompress %} {% endblock %} diff --git a/src/pretix/control/views/auth.py b/src/pretix/control/views/auth.py index baeea7ce8..8f87fbb33 100644 --- a/src/pretix/control/views/auth.py +++ b/src/pretix/control/views/auth.py @@ -1,3 +1,4 @@ +import logging import time from urllib.parse import quote @@ -15,14 +16,19 @@ 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 u2flib_server.jsapi import DeviceRegistration +from u2flib_server.u2f import start_authenticate, verify_authenticate +from u2flib_server.utils import rand_bytes from pretix.base.forms.auth import ( LoginForm, PasswordForgotForm, PasswordRecoverForm, RegistrationForm, ) -from pretix.base.models import User +from pretix.base.models import U2FDevice, User from pretix.base.services.mail import SendMailException, mail from pretix.helpers.urls import build_absolute_uri +logger = logging.getLogger(__name__) + def login(request): """ @@ -204,9 +210,17 @@ class Recover(TemplateView): return context +def get_u2f_appid(request): + return '%s://%s' % ('https' if request.is_secure() else 'http', request.get_host()) + + class Login2FAView(TemplateView): template_name = 'pretixcontrol/auth/login_2fa.html' + @property + def app_id(self): + return get_u2f_appid(self.request) + def dispatch(self, request, *args, **kwargs): fail = False if 'pretix_auth_2fa_user' not in request.session: @@ -226,7 +240,21 @@ class Login2FAView(TemplateView): def post(self, request, *args, **kwargs): token = request.POST.get('token', '').strip().replace(' ', '') - if match_token(self.user, token): + + valid = False + if '_u2f_challenge' in self.request.session and token.startswith('{'): + devices = [DeviceRegistration.wrap(device.json_data) + for device in U2FDevice.objects.filter(confirmed=True, user=self.user)] + challenge = self.request.session.pop('_u2f_challenge') + try: + verify_authenticate(devices, challenge, token, [self.app_id]) + valid = True + except Exception: + logger.exception('U2F login failed') + else: + valid = match_token(self.user, token) + + if valid: auth_login(request, self.user) del request.session['pretix_auth_2fa_user'] del request.session['pretix_auth_2fa_time'] @@ -236,3 +264,21 @@ class Login2FAView(TemplateView): else: messages.error(request, _('Invalid code, please try again.')) return redirect('control:auth.login.2fa') + + def get_context_data(self, **kwargs): + ctx = super().get_context_data() + + devices = [DeviceRegistration.wrap(device.json_data) + for device in U2FDevice.objects.filter(confirmed=True, user=self.user)] + if devices: + challenge = start_authenticate(devices, challenge=rand_bytes(32)) + self.request.session['_u2f_challenge'] = challenge.json + ctx['jsondata'] = challenge.json + else: + del self.request.session['_u2f_challenge'] + ctx['jsondata'] = None + + return ctx + + def get(self, request, *args, **kwargs): + return super().get(request, *args, **kwargs) diff --git a/src/pretix/control/views/user.py b/src/pretix/control/views/user.py index 2db89cad0..8fe7b71d2 100644 --- a/src/pretix/control/views/user.py +++ b/src/pretix/control/views/user.py @@ -13,13 +13,12 @@ from django.utils.translation import ugettext_lazy as _ from django.views.generic import FormView, TemplateView, UpdateView from django_otp.plugins.otp_static.models import StaticDevice from django_otp.plugins.otp_totp.models import TOTPDevice -from django_otp_u2f_stub.models import U2FDevice -from django_otp_u2f_stub.utils import get_origin from u2flib_server.jsapi import DeviceRegistration from u2flib_server.u2f import complete_register, start_register from pretix.base.forms.user import User2FADeviceAddForm, UserSettingsForm -from pretix.base.models import User +from pretix.base.models import U2FDevice, User +from pretix.control.views.auth import get_u2f_appid REAL_DEVICE_TYPES = (TOTPDevice, U2FDevice) logger = logging.getLogger(__name__) @@ -128,7 +127,7 @@ class User2FADeviceConfirmU2FView(TemplateView): @property def app_id(self): - return get_origin(self.request) + return get_u2f_appid(self.request) @cached_property def device(self): @@ -139,7 +138,7 @@ class User2FADeviceConfirmU2FView(TemplateView): ctx['device'] = self.device devices = [DeviceRegistration.wrap(device.json_data) - for device in U2FDevice.objects.filter(confirmed=True)] + for device in U2FDevice.objects.filter(confirmed=True, user=self.request.user)] enroll = start_register(self.app_id, devices) self.request.session['_u2f_enroll'] = enroll.json ctx['jsondata'] = enroll.json @@ -156,7 +155,7 @@ class User2FADeviceConfirmU2FView(TemplateView): self.device.save() messages.success(request, _('The device has been verified and can now be used.')) return redirect(reverse('control:user.settings.2fa')) - except ValueError: + except Exception: messages.error(request, _('The registration could not be completed. Please try again.')) logger.exception('U2F registration failed') return redirect(reverse('control:user.settings.2fa.confirm.2fa', kwargs={ diff --git a/src/pretix/settings.py b/src/pretix/settings.py index df9232342..675c73395 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -179,7 +179,6 @@ INSTALLED_APPS = [ 'django_otp', 'django_otp.plugins.otp_totp', 'django_otp.plugins.otp_static', - 'django_otp_u2f_stub' ] try: diff --git a/src/requirements/production.txt b/src/requirements/production.txt index 839539b30..b68963e6f 100644 --- a/src/requirements/production.txt +++ b/src/requirements/production.txt @@ -11,7 +11,6 @@ easy-thumbnails>=2.2,<3 django-libsass libsass django-otp==0.3.* -django-otp-u2f-stub python-u2flib-server==4.* # celery>=3.1,<3.2 # until the following issue is fixed, we need our own celery version diff --git a/src/static/pretixcontrol/js/ui/u2f.js b/src/static/pretixcontrol/js/ui/u2f.js index abbb685cf..ae62c8649 100644 --- a/src/static/pretixcontrol/js/ui/u2f.js +++ b/src/static/pretixcontrol/js/ui/u2f.js @@ -1,22 +1,40 @@ /*global $,u2f */ $(function () { + $("#u2f-progress").hide(); if ($("#u2f-enroll").length) { var request = JSON.parse($.trim($("#u2f-enroll").html())); + $("#u2f-progress").show(); setTimeout(function () { var appId = request.registerRequests[0].appId; - $('#promptModal').modal('show'); - console.log(appId, request.registerRequests); u2f.register(appId, request.registerRequests, [], function (data) { - console.log("callback", data); if (data.errorCode) { $("#u2f-error").removeClass("sr-only"); $("#u2f-progress").remove(); } else { - console.log("Register callback", data); $('#u2f-response').val(JSON.stringify(data)); $('#u2f-form').submit(); } - }); - }, 500); + }, 300); + }, 100); + } else if ($("#u2f-login").length) { + var request = JSON.parse($.trim($("#u2f-login").html())); + $("#u2f-progress").show(); + setTimeout(function () { + var firstr = request.authenticateRequests[0]; + var appId = firstr.appId; + var registeredKeys = []; + var reqs = request.authenticateRequests; + for (var i = 0; i < reqs.length; i++) { + registeredKeys.push({version: reqs[i].version, keyHandle: reqs[i].keyHandle}); + } + u2f.sign(appId, firstr.challenge, registeredKeys, function (data) { + if (data.errorCode && data.errorCode != 5) { + $("#u2f-error").removeClass("sr-only"); + } else { + $('#u2f-response').val(JSON.stringify(data)); + $('#u2f-form').submit(); + } + }, 300); + }, 100); } });