diff --git a/src/pretix/base/forms/user.py b/src/pretix/base/forms/user.py index 1fbb00d88..983716e9a 100644 --- a/src/pretix/base/forms/user.py +++ b/src/pretix/base/forms/user.py @@ -115,5 +115,5 @@ class User2FADeviceAddForm(forms.Form): name = forms.CharField(label=_('Device name'), max_length=64) devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=( ('totp', _('Smartphone with the Authenticator application')), - ('u2f', _('U2F-compatible hardware token (e.g. Yubikey)')), + ('webauthn', _('WebAuthn-compatible hardware token (e.g. Yubikey)')), )) diff --git a/src/pretix/base/migrations/0134_auto_20190909_1042.py b/src/pretix/base/migrations/0134_auto_20190909_1042.py new file mode 100644 index 000000000..dbeacf639 --- /dev/null +++ b/src/pretix/base/migrations/0134_auto_20190909_1042.py @@ -0,0 +1,35 @@ +# Generated by Django 2.2.4 on 2019-09-09 10:42 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + +import pretix.base.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0133_auto_20190830_1513'), + ] + + operations = [ + migrations.CreateModel( + name='WebAuthnDevice', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=64)), + ('confirmed', models.BooleanField(default=True)), + ('credential_id', models.CharField(max_length=255, null=True)), + ('rp_id', models.CharField(max_length=255, null=True)), + ('icon_url', models.CharField(max_length=255, null=True)), + ('ukey', models.TextField(null=True)), + ('pub_key', models.TextField(null=True)), + ('sign_count', models.IntegerField(default=0)), + ('user', models.ForeignKey(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 82263b51c..22175ed9f 100644 --- a/src/pretix/base/models/__init__.py +++ b/src/pretix/base/models/__init__.py @@ -1,5 +1,5 @@ from ..settings import GlobalSettingsObject_SettingsStore -from .auth import U2FDevice, User +from .auth import U2FDevice, User, WebAuthnDevice from .base import CachedFile, LoggedModel, cachedfile_name from .checkin import Checkin, CheckinList from .devices import Device diff --git a/src/pretix/base/models/auth.py b/src/pretix/base/models/auth.py index d14e5038a..5d87306b0 100644 --- a/src/pretix/base/models/auth.py +++ b/src/pretix/base/models/auth.py @@ -1,5 +1,9 @@ +import binascii +import json from datetime import timedelta +from urllib.parse import urlparse +import webauthn from django.conf import settings from django.contrib.auth.models import ( AbstractBaseUser, BaseUserManager, PermissionsMixin, @@ -13,6 +17,9 @@ from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ from django_otp.models import Device from django_scopes import scopes_disabled +from u2flib_server.utils import ( + pub_key_from_der, websafe_decode, websafe_encode, +) from pretix.base.i18n import language from pretix.helpers.urls import build_absolute_uri @@ -380,3 +387,49 @@ class StaffSessionAuditLog(models.Model): class U2FDevice(Device): json_data = models.TextField() + + @property + def webauthnuser(self): + d = json.loads(self.json_data) + # We manually need to convert the pubkey from DER format (used in our + # former U2F implementation) to the format required by webauthn. This + # is based on the following example: + # https://www.w3.org/TR/webauthn/#sctn-encoded-credPubKey-examples + pub_key = pub_key_from_der(websafe_decode(d['publicKey'].replace('+', '-').replace('/', '_'))) + pub_key = binascii.unhexlify( + 'A5010203262001215820{:064x}225820{:064x}'.format( + pub_key.public_numbers().x, pub_key.public_numbers().y + ) + ) + return webauthn.WebAuthnUser( + d['keyHandle'], + self.user.email, + str(self.user), + settings.SITE_URL, + d['keyHandle'], + websafe_encode(pub_key), + 1, + urlparse(settings.SITE_URL).netloc + ) + + +class WebAuthnDevice(Device): + credential_id = models.CharField(max_length=255, null=True, blank=True) + rp_id = models.CharField(max_length=255, null=True, blank=True) + icon_url = models.CharField(max_length=255, null=True, blank=True) + ukey = models.TextField(null=True) + pub_key = models.TextField(null=True) + sign_count = models.IntegerField(default=0) + + @property + def webauthnuser(self): + return webauthn.WebAuthnUser( + self.ukey, + self.user.email, + str(self.user), + settings.SITE_URL, + self.credential_id, + self.pub_key, + self.sign_count, + urlparse(settings.SITE_URL).netloc + ) diff --git a/src/pretix/control/templates/pretixcontrol/auth/login_2fa.html b/src/pretix/control/templates/pretixcontrol/auth/login_2fa.html index 6c5c73aa8..f5996d536 100644 --- a/src/pretix/control/templates/pretixcontrol/auth/login_2fa.html +++ b/src/pretix/control/templates/pretixcontrol/auth/login_2fa.html @@ -4,7 +4,7 @@ {% load static %} {% load compress %} {% block content %} -