mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
Replace U2F with WebAuthn (#1392)
* Replace U2F with WebAuthn * Imports * Fix backwards compatibility * Add explanatory comment * Fix tests
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
{% load static %}
|
||||
{% load compress %}
|
||||
{% block content %}
|
||||
<form class="form-signin" action="" method="post" id="u2f-form">
|
||||
<form class="form-signin" action="" method="post" id="webauthn-form">
|
||||
{% csrf_token %}
|
||||
<h3>{% trans "Welcome back!" %}</h3>
|
||||
<p>
|
||||
@@ -12,14 +12,14 @@
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<input class="form-control" name="token" placeholder="{% trans "Token" %}"
|
||||
type="text" required="required" autofocus="autofocus" id="u2f-response">
|
||||
type="text" required="required" autofocus="autofocus" id="webauthn-response">
|
||||
</div>
|
||||
<div class="sr-only alert alert-danger" id="u2f-error">
|
||||
{% trans "U2F failed. Check that the correct authentication device is correctly plugged in." %}
|
||||
<div class="sr-only alert alert-danger" id="webauthn-error">
|
||||
{% trans "WebAuthn failed. Check that the correct authentication device is correctly plugged in." %}
|
||||
</div>
|
||||
{% if jsondata %}
|
||||
<p><small>
|
||||
{% 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." %}
|
||||
{% trans "Alternatively, connect your WebAuthn device. If it has a button, touch it now. You might have to unplug the device and plug it back in again." %}
|
||||
</small></p>
|
||||
{% endif %}
|
||||
<div class="form-group buttons">
|
||||
@@ -29,14 +29,14 @@
|
||||
</div>
|
||||
</form>
|
||||
{% if jsondata %}
|
||||
<script type="text/json" id="u2f-login">
|
||||
<script type="text/json" id="webauthn-login">
|
||||
{{ jsondata|safe }}
|
||||
|
||||
</script>
|
||||
{% endif %}
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/u2f-api.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/u2f.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/base64js.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/webauthn.js" %}"></script>
|
||||
{% endcompress %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
{% block title %}{% trans "Add a two-factor authentication device" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Add a two-factor authentication device" %}</h1>
|
||||
<p id="u2f-progress">
|
||||
<p id="webauthn-progress">
|
||||
<span class="fa fa-cog fa-spin"></span>
|
||||
{% trans "Please 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." %}
|
||||
{% trans "Please connect your WebAuthn device. If it has a button, touch it now. You might have to unplug the device and plug it back in again." %}
|
||||
</p>
|
||||
<form class="form form-inline" method="post" action="" id="u2f-form">
|
||||
<form class="form form-inline" method="post" action="" id="webauthn-form">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" id="u2f-response" name="token" class="form-control" required="required">
|
||||
<input type="hidden" id="webauthn-response" name="token" class="form-control" required="required">
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" name="activate" checked="checked" value="on">
|
||||
@@ -22,16 +22,16 @@
|
||||
<button class="btn btn-primary sr-only" type="submit"></button>
|
||||
</form>
|
||||
|
||||
<div class="sr-only alert alert-danger" id="u2f-error">
|
||||
<div class="sr-only alert alert-danger" id="webauthn-error">
|
||||
{% trans "Device registration failed." %}
|
||||
</div>
|
||||
<script type="text/json" id="u2f-enroll">
|
||||
<script type="text/json" id="webauthn-enroll">
|
||||
{{ jsondata|safe }}
|
||||
|
||||
|
||||
</script>
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/u2f-api.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/u2f.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/base64js.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/webauthn.js" %}"></script>
|
||||
{% endcompress %}
|
||||
{% endblock %}
|
||||
@@ -78,6 +78,8 @@
|
||||
</a>
|
||||
{% if d.devicetype == "totp" %}
|
||||
<span class="fa fa-mobile"></span>
|
||||
{% elif d.devicetype == "webauthn" %}
|
||||
<span class="fa fa-usb"></span>
|
||||
{% elif d.devicetype == "u2f" %}
|
||||
<span class="fa fa-usb"></span>
|
||||
{% endif %}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{% load compress %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
<form class="form-signin" id="u2f-form" action="" method="post">
|
||||
<form class="form-signin" id="webauthn-form" action="" method="post">
|
||||
{% csrf_token %}
|
||||
<h3>{% trans "Welcome back!" %}</h3>
|
||||
<p>
|
||||
@@ -19,12 +19,12 @@
|
||||
title="" type="password" required="" autofocus>
|
||||
</div>
|
||||
{% if jsondata %}
|
||||
<div class="sr-only alert alert-danger" id="u2f-error">
|
||||
{% trans "U2F failed. Check that the correct authentication device is correctly plugged in." %}
|
||||
<div class="sr-only alert alert-danger" id="webauthn-error">
|
||||
{% trans "WebAuthn failed. Check that the correct authentication device is correctly plugged in." %}
|
||||
</div>
|
||||
<p><small>
|
||||
<span class="fa fa-usb"></span>
|
||||
{% trans "Alternatively, you can use your U2F device." %}
|
||||
{% trans "Alternatively, you can use your WebAuthn device." %}
|
||||
</small></p>
|
||||
{% endif %}
|
||||
<div class="form-group text-right">
|
||||
@@ -37,14 +37,14 @@
|
||||
</div>
|
||||
|
||||
{% if jsondata %}
|
||||
<script type="text/json" id="u2f-login">
|
||||
<script type="text/json" id="webauthn-login">
|
||||
{{ jsondata|safe }}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/u2f-api.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/u2f.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/base64js.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/webauthn.js" %}"></script>
|
||||
{% endcompress %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -62,8 +62,8 @@ urlpatterns = [
|
||||
name='user.settings.2fa.regenemergency'),
|
||||
url(r'^settings/2fa/totp/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmTOTPView.as_view(),
|
||||
name='user.settings.2fa.confirm.totp'),
|
||||
url(r'^settings/2fa/u2f/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmU2FView.as_view(),
|
||||
name='user.settings.2fa.confirm.u2f'),
|
||||
url(r'^settings/2fa/webauthn/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmWebAuthnView.as_view(),
|
||||
name='user.settings.2fa.confirm.webauthn'),
|
||||
url(r'^settings/2fa/(?P<devicetype>[^/]+)/(?P<device>[0-9]+)/delete', user.User2FADeviceDeleteView.as_view(),
|
||||
name='user.settings.2fa.delete'),
|
||||
url(r'^organizers/$', organizer.OrganizerList.as_view(), name='organizers'),
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from urllib.parse import quote
|
||||
|
||||
import webauthn
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import (
|
||||
@@ -17,15 +19,13 @@ 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 import u2f
|
||||
from u2flib_server.jsapi import DeviceRegistration
|
||||
from u2flib_server.utils import rand_bytes
|
||||
|
||||
from pretix.base.forms.auth import (
|
||||
LoginForm, PasswordForgotForm, PasswordRecoverForm, RegistrationForm,
|
||||
)
|
||||
from pretix.base.models import TeamInvite, U2FDevice, User
|
||||
from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.helpers.webauthn import generate_challenge
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -302,7 +302,7 @@ class Recover(TemplateView):
|
||||
|
||||
|
||||
def get_u2f_appid(request):
|
||||
return '%s://%s' % ('https' if request.is_secure() else 'http', request.get_host())
|
||||
return settings.SITE_URL
|
||||
|
||||
|
||||
class Login2FAView(TemplateView):
|
||||
@@ -333,15 +333,41 @@ class Login2FAView(TemplateView):
|
||||
token = request.POST.get('token', '').strip().replace(' ', '')
|
||||
|
||||
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')
|
||||
if 'webauthn_challenge' in self.request.session and token.startswith('{'):
|
||||
challenge = self.request.session['webauthn_challenge']
|
||||
|
||||
resp = json.loads(self.request.POST.get("token"))
|
||||
try:
|
||||
u2f.verify_authenticate(devices, challenge, token, [self.app_id])
|
||||
valid = True
|
||||
except Exception:
|
||||
logger.exception('U2F login failed')
|
||||
devices = [WebAuthnDevice.objects.get(user=self.user, credential_id=resp.get("id"))]
|
||||
except WebAuthnDevice.DoesNotExist:
|
||||
devices = U2FDevice.objects.filter(user=self.user)
|
||||
|
||||
for d in devices:
|
||||
try:
|
||||
wu = d.webauthnuser
|
||||
|
||||
if isinstance(d, U2FDevice):
|
||||
# RP_ID needs to be appId for U2F devices, but we can't
|
||||
# set it that way in U2FDevice.webauthnuser, since that
|
||||
# breaks the frontend part.
|
||||
wu.rp_id = settings.SITE_URL
|
||||
|
||||
webauthn_assertion_response = webauthn.WebAuthnAssertionResponse(
|
||||
wu,
|
||||
resp,
|
||||
challenge,
|
||||
settings.SITE_URL,
|
||||
uv_required=False # User Verification
|
||||
)
|
||||
sign_count = webauthn_assertion_response.verify()
|
||||
except Exception:
|
||||
logger.exception('U2F login failed')
|
||||
else:
|
||||
if isinstance(d, WebAuthnDevice):
|
||||
d.sign_count = sign_count
|
||||
d.save()
|
||||
valid = True
|
||||
break
|
||||
else:
|
||||
valid = match_token(self.user, token)
|
||||
|
||||
@@ -359,18 +385,25 @@ class Login2FAView(TemplateView):
|
||||
|
||||
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 'webauthn_challenge' in self.request.session:
|
||||
del self.request.session['webauthn_challenge']
|
||||
challenge = generate_challenge(32)
|
||||
self.request.session['webauthn_challenge'] = challenge
|
||||
devices = [
|
||||
device.webauthnuser for device in WebAuthnDevice.objects.filter(confirmed=True, user=self.user)
|
||||
] + [
|
||||
device.webauthnuser for device in U2FDevice.objects.filter(confirmed=True, user=self.user)
|
||||
]
|
||||
if devices:
|
||||
challenge = u2f.start_authenticate(devices, challenge=rand_bytes(32))
|
||||
self.request.session['_u2f_challenge'] = challenge.json
|
||||
ctx['jsondata'] = challenge.json
|
||||
else:
|
||||
if '_u2f_challenge' in self.request.session:
|
||||
del self.request.session['_u2f_challenge']
|
||||
ctx['jsondata'] = None
|
||||
|
||||
webauthn_assertion_options = webauthn.WebAuthnAssertionOptions(
|
||||
devices,
|
||||
challenge
|
||||
)
|
||||
ad = webauthn_assertion_options.assertion_dict
|
||||
ad['extensions'] = {
|
||||
'appid': get_u2f_appid(self.request)
|
||||
}
|
||||
ctx['jsondata'] = json.dumps(ad)
|
||||
return ctx
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from urllib.parse import quote
|
||||
from urllib.parse import quote, urlparse
|
||||
|
||||
import webauthn
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
@@ -19,13 +22,10 @@ from django.views import View
|
||||
from django.views.generic import FormView, ListView, TemplateView, UpdateView
|
||||
from django_otp.plugins.otp_static.models import StaticDevice
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from u2flib_server import u2f
|
||||
from u2flib_server.jsapi import DeviceRegistration
|
||||
from u2flib_server.utils import rand_bytes
|
||||
|
||||
from pretix.base.forms.user import User2FADeviceAddForm, UserSettingsForm
|
||||
from pretix.base.models import (
|
||||
Event, LogEntry, NotificationSetting, U2FDevice, User,
|
||||
Event, LogEntry, NotificationSetting, U2FDevice, User, WebAuthnDevice,
|
||||
)
|
||||
from pretix.base.models.auth import StaffSession
|
||||
from pretix.base.notifications import get_all_notification_types
|
||||
@@ -34,8 +34,9 @@ from pretix.control.permissions import (
|
||||
AdministratorPermissionRequiredMixin, StaffMemberRequiredMixin,
|
||||
)
|
||||
from pretix.control.views.auth import get_u2f_appid
|
||||
from pretix.helpers.webauthn import generate_challenge, generate_ukey
|
||||
|
||||
REAL_DEVICE_TYPES = (TOTPDevice, U2FDevice)
|
||||
REAL_DEVICE_TYPES = (TOTPDevice, WebAuthnDevice, U2FDevice)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -52,23 +53,45 @@ class RecentAuthenticationRequiredMixin:
|
||||
class ReauthView(TemplateView):
|
||||
template_name = 'pretixcontrol/user/reauth.html'
|
||||
|
||||
@property
|
||||
def app_id(self):
|
||||
return get_u2f_appid(self.request)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
password = request.POST.get("password", "")
|
||||
valid = False
|
||||
|
||||
if '_u2f_challenge' in self.request.session and password.startswith('{'):
|
||||
devices = [DeviceRegistration.wrap(device.json_data)
|
||||
for device in U2FDevice.objects.filter(confirmed=True, user=self.request.user)]
|
||||
challenge = self.request.session.pop('_u2f_challenge')
|
||||
if 'webauthn_challenge' in self.request.session and password.startswith('{'):
|
||||
challenge = self.request.session['webauthn_challenge']
|
||||
|
||||
resp = json.loads(password)
|
||||
try:
|
||||
u2f.verify_authenticate(devices, challenge, password, [self.app_id])
|
||||
valid = True
|
||||
except Exception:
|
||||
logger.exception('U2F login failed')
|
||||
devices = [WebAuthnDevice.objects.get(user=self.request.user, credential_id=resp.get("id"))]
|
||||
except WebAuthnDevice.DoesNotExist:
|
||||
devices = U2FDevice.objects.filter(user=self.request.user)
|
||||
|
||||
for d in devices:
|
||||
try:
|
||||
wu = d.webauthnuser
|
||||
|
||||
if isinstance(d, U2FDevice):
|
||||
# RP_ID needs to be appId for U2F devices, but we can't
|
||||
# set it that way in U2FDevice.webauthnuser, since that
|
||||
# breaks the frontend part.
|
||||
wu.rp_id = settings.SITE_URL
|
||||
|
||||
webauthn_assertion_response = webauthn.WebAuthnAssertionResponse(
|
||||
wu,
|
||||
resp,
|
||||
challenge,
|
||||
settings.SITE_URL,
|
||||
uv_required=False # User Verification
|
||||
)
|
||||
sign_count = webauthn_assertion_response.verify()
|
||||
except Exception:
|
||||
logger.exception('U2F login failed')
|
||||
else:
|
||||
if isinstance(d, WebAuthnDevice):
|
||||
d.sign_count = sign_count
|
||||
d.save()
|
||||
valid = True
|
||||
break
|
||||
|
||||
valid = valid or request.user.check_password(password)
|
||||
|
||||
@@ -85,18 +108,25 @@ class ReauthView(TemplateView):
|
||||
|
||||
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.request.user)]
|
||||
if 'webauthn_challenge' in self.request.session:
|
||||
del self.request.session['webauthn_challenge']
|
||||
challenge = generate_challenge(32)
|
||||
self.request.session['webauthn_challenge'] = challenge
|
||||
devices = [
|
||||
device.webauthnuser for device in WebAuthnDevice.objects.filter(confirmed=True, user=self.request.user)
|
||||
] + [
|
||||
device.webauthnuser for device in U2FDevice.objects.filter(confirmed=True, user=self.request.user)
|
||||
]
|
||||
if devices:
|
||||
challenge = u2f.start_authenticate(devices, challenge=rand_bytes(32))
|
||||
self.request.session['_u2f_challenge'] = challenge.json
|
||||
ctx['jsondata'] = challenge.json
|
||||
else:
|
||||
if '_u2f_challenge' in self.request.session:
|
||||
del self.request.session['_u2f_challenge']
|
||||
ctx['jsondata'] = None
|
||||
|
||||
webauthn_assertion_options = webauthn.WebAuthnAssertionOptions(
|
||||
devices,
|
||||
challenge
|
||||
)
|
||||
ad = webauthn_assertion_options.assertion_dict
|
||||
ad['extensions'] = {
|
||||
'appid': get_u2f_appid(self.request)
|
||||
}
|
||||
ctx['jsondata'] = json.dumps(ad)
|
||||
return ctx
|
||||
|
||||
|
||||
@@ -200,6 +230,8 @@ class User2FAMainView(RecentAuthenticationRequiredMixin, TemplateView):
|
||||
obj.devicetype = 'totp'
|
||||
elif dt == U2FDevice:
|
||||
obj.devicetype = 'u2f'
|
||||
elif dt == WebAuthnDevice:
|
||||
obj.devicetype = 'webauthn'
|
||||
ctx['devices'] += objs
|
||||
|
||||
return ctx
|
||||
@@ -212,11 +244,12 @@ class User2FADeviceAddView(RecentAuthenticationRequiredMixin, FormView):
|
||||
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'])
|
||||
elif form.cleaned_data['devicetype'] == 'u2f':
|
||||
elif form.cleaned_data['devicetype'] == 'webauthn':
|
||||
if not self.request.is_secure():
|
||||
messages.error(self.request, _('U2F devices are only available if pretix is served via HTTPS.'))
|
||||
messages.error(self.request,
|
||||
_('Security devices are only available if pretix is served via HTTPS.'))
|
||||
return self.get(self.request, self.args, self.kwargs)
|
||||
dev = U2FDevice.objects.create(user=self.request.user, confirmed=False, name=form.cleaned_data['name'])
|
||||
dev = WebAuthnDevice.objects.create(user=self.request.user, confirmed=False, name=form.cleaned_data['name'])
|
||||
return redirect(reverse('control:user.settings.2fa.confirm.' + form.cleaned_data['devicetype'], kwargs={
|
||||
'device': dev.pk
|
||||
}))
|
||||
@@ -233,6 +266,8 @@ class User2FADeviceDeleteView(RecentAuthenticationRequiredMixin, TemplateView):
|
||||
def device(self):
|
||||
if self.kwargs['devicetype'] == 'totp':
|
||||
return get_object_or_404(TOTPDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
|
||||
elif self.kwargs['devicetype'] == 'webauthn':
|
||||
return get_object_or_404(WebAuthnDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
|
||||
elif self.kwargs['devicetype'] == 'u2f':
|
||||
return get_object_or_404(U2FDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
|
||||
|
||||
@@ -262,35 +297,94 @@ class User2FADeviceDeleteView(RecentAuthenticationRequiredMixin, TemplateView):
|
||||
return redirect(reverse('control:user.settings.2fa'))
|
||||
|
||||
|
||||
class User2FADeviceConfirmU2FView(RecentAuthenticationRequiredMixin, TemplateView):
|
||||
template_name = 'pretixcontrol/user/2fa_confirm_u2f.html'
|
||||
|
||||
@property
|
||||
def app_id(self):
|
||||
return get_u2f_appid(self.request)
|
||||
class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, TemplateView):
|
||||
template_name = 'pretixcontrol/user/2fa_confirm_webauthn.html'
|
||||
|
||||
@cached_property
|
||||
def device(self):
|
||||
return get_object_or_404(U2FDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=False)
|
||||
return get_object_or_404(WebAuthnDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=False)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
ctx['device'] = self.device
|
||||
|
||||
devices = [DeviceRegistration.wrap(device.json_data)
|
||||
for device in U2FDevice.objects.filter(confirmed=True, user=self.request.user)]
|
||||
enroll = u2f.start_register(self.app_id, devices)
|
||||
self.request.session['_u2f_enroll'] = enroll.json
|
||||
ctx['jsondata'] = enroll.json
|
||||
if 'webauthn_register_ukey' in self.request.session:
|
||||
del self.request.session['webauthn_register_ukey']
|
||||
if 'webauthn_challenge' in self.request.session:
|
||||
del self.request.session['webauthn_challenge']
|
||||
|
||||
challenge = generate_challenge(32)
|
||||
ukey = generate_ukey()
|
||||
|
||||
self.request.session['webauthn_challenge'] = challenge
|
||||
self.request.session['webauthn_register_ukey'] = ukey
|
||||
|
||||
make_credential_options = webauthn.WebAuthnMakeCredentialOptions(
|
||||
challenge,
|
||||
urlparse(settings.SITE_URL).netloc,
|
||||
urlparse(settings.SITE_URL).netloc,
|
||||
ukey,
|
||||
self.request.user.email,
|
||||
str(self.request.user),
|
||||
settings.SITE_URL
|
||||
)
|
||||
ctx['jsondata'] = json.dumps(make_credential_options.registration_dict)
|
||||
|
||||
return ctx
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
try:
|
||||
binding, cert = u2f.complete_register(self.request.session.pop('_u2f_enroll'),
|
||||
request.POST.get('token'),
|
||||
[self.app_id])
|
||||
self.device.json_data = binding.json
|
||||
challenge = self.request.session['webauthn_challenge']
|
||||
ukey = self.request.session['webauthn_register_ukey']
|
||||
resp = json.loads(self.request.POST.get("token"))
|
||||
trust_anchor_dir = os.path.normpath(os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
'../../static/webauthn_trusted_attestation_roots' # currently does not exist
|
||||
))
|
||||
# We currently do not check attestation certificates, since there's no real risk
|
||||
# and we do not have any policies specifying what devices can be used. (Also, we
|
||||
# didn't get it to work.)
|
||||
# Read more: https://fidoalliance.org/fido-technotes-the-truth-about-attestation/
|
||||
trusted_attestation_cert_required = False
|
||||
self_attestation_permitted = True
|
||||
none_attestation_permitted = True
|
||||
|
||||
webauthn_registration_response = webauthn.WebAuthnRegistrationResponse(
|
||||
urlparse(settings.SITE_URL).netloc,
|
||||
settings.SITE_URL,
|
||||
resp,
|
||||
challenge,
|
||||
trust_anchor_dir,
|
||||
trusted_attestation_cert_required,
|
||||
self_attestation_permitted,
|
||||
none_attestation_permitted,
|
||||
uv_required=False
|
||||
)
|
||||
webauthn_credential = webauthn_registration_response.verify()
|
||||
|
||||
# Check that the credentialId is not yet registered to any other user.
|
||||
# If registration is requested for a credential that is already registered
|
||||
# to a different user, the Relying Party SHOULD fail this registration
|
||||
# ceremony, or it MAY decide to accept the registration, e.g. while deleting
|
||||
# the older registration.
|
||||
credential_id_exists = WebAuthnDevice.objects.filter(
|
||||
credential_id=webauthn_credential.credential_id
|
||||
).first()
|
||||
if credential_id_exists:
|
||||
messages.error(request, _('This security device is already registered.'))
|
||||
return redirect(reverse('control:user.settings.2fa.confirm.webauthn', kwargs={
|
||||
'device': self.device.pk
|
||||
}))
|
||||
|
||||
webauthn_credential.credential_id = str(webauthn_credential.credential_id, "utf-8")
|
||||
webauthn_credential.public_key = str(webauthn_credential.public_key, "utf-8")
|
||||
|
||||
self.device.credential_id = webauthn_credential.credential_id
|
||||
self.device.ukey = ukey
|
||||
self.device.pub_key = webauthn_credential.public_key
|
||||
self.device.sign_count = webauthn_credential.sign_count
|
||||
self.device.rp_id = urlparse(settings.SITE_URL).netloc
|
||||
self.device.icon_url = settings.SITE_URL
|
||||
self.device.confirmed = True
|
||||
self.device.save()
|
||||
self.request.user.log_action('pretix.user.settings.2fa.device.added', user=self.request.user, data={
|
||||
@@ -320,8 +414,8 @@ class User2FADeviceConfirmU2FView(RecentAuthenticationRequiredMixin, TemplateVie
|
||||
return redirect(reverse('control:user.settings.2fa'))
|
||||
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.u2f', kwargs={
|
||||
logger.exception('WebAuthn registration failed')
|
||||
return redirect(reverse('control:user.settings.2fa.confirm.webauthn', kwargs={
|
||||
'device': self.device.pk
|
||||
}))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user