Replace U2F with WebAuthn (#1392)

* Replace U2F with WebAuthn

* Imports

* Fix backwards compatibility

* Add explanatory comment

* Fix tests
This commit is contained in:
Raphael Michel
2019-09-10 09:58:31 +02:00
committed by GitHub
parent 21451db412
commit 2c4ee3b3c7
20 changed files with 686 additions and 928 deletions

View File

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