diff --git a/pyproject.toml b/pyproject.toml index cdebc1562..b6fd3002b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,7 +102,7 @@ dependencies = [ "tqdm==4.*", "vat_moss_forked==2020.3.20.0.11.0", "vobject==0.9.*", - "webauthn==0.4.*", + "webauthn==2.0.*", "zeep==4.2.*" ] diff --git a/src/pretix/base/models/auth.py b/src/pretix/base/models/auth.py index c7c63b1d5..07ff51e0e 100644 --- a/src/pretix/base/models/auth.py +++ b/src/pretix/base/models/auth.py @@ -37,9 +37,7 @@ import json import operator from datetime import timedelta from functools import reduce -from urllib.parse import urlparse -import webauthn from django.conf import settings from django.contrib.auth.models import ( AbstractBaseUser, BaseUserManager, PermissionsMixin, @@ -53,11 +51,12 @@ from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ from django_otp.models import Device from django_scopes import scopes_disabled +from webauthn.helpers.structs import PublicKeyCredentialDescriptor from pretix.base.i18n import language from pretix.helpers.urls import build_absolute_uri -from ...helpers.u2f import pub_key_from_der, websafe_decode, websafe_encode +from ...helpers.u2f import pub_key_from_der, websafe_decode from .base import LoggingMixin @@ -606,7 +605,12 @@ class U2FDevice(Device): json_data = models.TextField() @property - def webauthnuser(self): + def webauthndevice(self): + d = json.loads(self.json_data) + return PublicKeyCredentialDescriptor(websafe_decode(d['keyHandle'])) + + @property + def webauthnpubkey(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 @@ -618,16 +622,7 @@ class U2FDevice(Device): 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 - ) + return pub_key class WebAuthnDevice(Device): @@ -639,14 +634,9 @@ class WebAuthnDevice(Device): 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 - ) + def webauthndevice(self): + return PublicKeyCredentialDescriptor(websafe_decode(self.credential_id)) + + @property + def webauthnpubkey(self): + return websafe_decode(self.pub_key) diff --git a/src/pretix/control/views/auth.py b/src/pretix/control/views/auth.py index 0c33e93bc..ac4a45cf9 100644 --- a/src/pretix/control/views/auth.py +++ b/src/pretix/control/views/auth.py @@ -32,11 +32,11 @@ # Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under the License. - +import base64 import json import logging import time -from urllib.parse import quote +from urllib.parse import quote, urlparse import webauthn from django.conf import settings @@ -54,6 +54,7 @@ from django.utils.http import url_has_allowed_host_and_scheme from django.utils.translation import gettext_lazy as _ from django.views.generic import TemplateView from django_otp import match_token +from webauthn.helpers import generate_challenge from pretix.base.auth import get_auth_backends from pretix.base.forms.auth import ( @@ -62,7 +63,6 @@ from pretix.base.forms.auth import ( from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice from pretix.base.services.mail import SendMailException from pretix.helpers.http import redirect_to_url -from pretix.helpers.webauthn import generate_challenge logger = logging.getLogger(__name__) @@ -389,6 +389,10 @@ def get_u2f_appid(request): return settings.SITE_URL +def get_webauthn_rp_id(request): + return urlparse(settings.SITE_URL).hostname + + class Login2FAView(TemplateView): template_name = 'pretixcontrol/auth/login_2fa.html' @@ -427,25 +431,41 @@ class Login2FAView(TemplateView): devices = U2FDevice.objects.filter(user=self.user) for d in devices: + credential_current_sign_count = d.sign_count if isinstance(d, WebAuthnDevice) else 0 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 + webauthn_assertion_response = webauthn.verify_authentication_response( + credential=resp, + expected_challenge=base64.b64decode(challenge), + expected_rp_id=get_webauthn_rp_id(self.request), + expected_origin=settings.SITE_URL, + credential_public_key=d.webauthnpubkey, + credential_current_sign_count=credential_current_sign_count, ) - sign_count = webauthn_assertion_response.verify() + sign_count = webauthn_assertion_response.new_sign_count + if sign_count < credential_current_sign_count: + raise Exception("Possible replay attack, sign count not higher") except Exception: - logger.exception('U2F login failed') + if isinstance(d, U2FDevice): + # https://www.w3.org/TR/webauthn/#sctn-appid-extension says + # "When verifying the assertion, expect that the rpIdHash MAY be the hash of the AppID instead of the RP ID." + try: + webauthn_assertion_response = webauthn.verify_authentication_response( + credential=resp, + expected_challenge=base64.b64decode(challenge), + expected_rp_id=get_u2f_appid(self.request), + expected_origin=settings.SITE_URL, + credential_public_key=d.webauthnpubkey, + credential_current_sign_count=credential_current_sign_count, + ) + if webauthn_assertion_response.new_sign_count < 1: + raise Exception("Possible replay attack, sign count set") + except Exception: + logger.exception('U2F login failed') + else: + valid = True + break + else: + logger.exception('Webauthn login failed') else: if isinstance(d, WebAuthnDevice): d.sign_count = sign_count @@ -471,23 +491,24 @@ class Login2FAView(TemplateView): ctx = super().get_context_data() if 'webauthn_challenge' in self.request.session: del self.request.session['webauthn_challenge'] - challenge = generate_challenge(32) - self.request.session['webauthn_challenge'] = challenge + challenge = generate_challenge() + self.request.session['webauthn_challenge'] = base64.b64encode(challenge).decode() devices = [ - device.webauthnuser for device in WebAuthnDevice.objects.filter(confirmed=True, user=self.user) + device.webauthndevice for device in WebAuthnDevice.objects.filter(confirmed=True, user=self.user) ] + [ - device.webauthnuser for device in U2FDevice.objects.filter(confirmed=True, user=self.user) + device.webauthndevice for device in U2FDevice.objects.filter(confirmed=True, user=self.user) ] if devices: - webauthn_assertion_options = webauthn.WebAuthnAssertionOptions( - devices, - challenge + auth_options = webauthn.generate_authentication_options( + rp_id=get_webauthn_rp_id(self.request), + challenge=challenge, + allow_credentials=devices, ) - ad = webauthn_assertion_options.assertion_dict - ad['extensions'] = { - 'appid': get_u2f_appid(self.request) - } - ctx['jsondata'] = json.dumps(ad) + + # Backwards compatibility to U2F + j = json.loads(webauthn.options_to_json(auth_options)) + j["extensions"] = {"appid": get_u2f_appid(self.request)} + ctx['jsondata'] = json.dumps(j) return ctx def get(self, request, *args, **kwargs): diff --git a/src/pretix/control/views/user.py b/src/pretix/control/views/user.py index 575afc420..a879fbe92 100644 --- a/src/pretix/control/views/user.py +++ b/src/pretix/control/views/user.py @@ -35,10 +35,9 @@ import base64 import json import logging -import os import time from collections import defaultdict -from urllib.parse import quote, urlparse +from urllib.parse import quote import webauthn from django.conf import settings @@ -57,6 +56,7 @@ 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 django_scopes import scopes_disabled +from webauthn.helpers import generate_challenge, generate_user_handle from pretix.base.auth import get_auth_backends from pretix.base.forms.auth import ReauthForm @@ -70,9 +70,9 @@ from pretix.control.forms.users import StaffSessionForm from pretix.control.permissions import ( AdministratorPermissionRequiredMixin, StaffMemberRequiredMixin, ) -from pretix.control.views.auth import get_u2f_appid +from pretix.control.views.auth import get_u2f_appid, get_webauthn_rp_id from pretix.helpers.http import redirect_to_url -from pretix.helpers.webauthn import generate_challenge, generate_ukey +from pretix.helpers.u2f import websafe_encode REAL_DEVICE_TYPES = (TOTPDevice, WebAuthnDevice, U2FDevice) logger = logging.getLogger(__name__) @@ -105,25 +105,41 @@ class ReauthView(TemplateView): devices = U2FDevice.objects.filter(user=self.request.user) for d in devices: + credential_current_sign_count = d.sign_count if isinstance(d, WebAuthnDevice) else 0 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 + webauthn_assertion_response = webauthn.verify_authentication_response( + credential=resp, + expected_challenge=base64.b64decode(challenge), + expected_rp_id=get_webauthn_rp_id(self.request), + expected_origin=settings.SITE_URL, + credential_public_key=d.webauthnpubkey, + credential_current_sign_count=credential_current_sign_count, ) - sign_count = webauthn_assertion_response.verify() + sign_count = webauthn_assertion_response.new_sign_count + if sign_count < credential_current_sign_count: + raise Exception("Possible replay attack, sign count not higher") except Exception: - logger.exception('U2F login failed') + if isinstance(d, U2FDevice): + # https://www.w3.org/TR/webauthn/#sctn-appid-extension says + # "When verifying the assertion, expect that the rpIdHash MAY be the hash of the AppID instead of the RP ID." + try: + webauthn_assertion_response = webauthn.verify_authentication_response( + credential=resp, + expected_challenge=base64.b64decode(challenge), + expected_rp_id=get_u2f_appid(self.request), + expected_origin=settings.SITE_URL, + credential_public_key=d.webauthnpubkey, + credential_current_sign_count=credential_current_sign_count, + ) + if webauthn_assertion_response.new_sign_count < 1: + raise Exception("Possible replay attack, sign count set") + except Exception: + logger.exception('U2F login failed') + else: + valid = True + break + else: + logger.exception('Webauthn login failed') else: if isinstance(d, WebAuthnDevice): d.sign_count = sign_count @@ -162,23 +178,24 @@ class ReauthView(TemplateView): ctx = super().get_context_data() if 'webauthn_challenge' in self.request.session: del self.request.session['webauthn_challenge'] - challenge = generate_challenge(32) - self.request.session['webauthn_challenge'] = challenge + challenge = generate_challenge() + self.request.session['webauthn_challenge'] = base64.b64encode(challenge).decode() devices = [ - device.webauthnuser for device in WebAuthnDevice.objects.filter(confirmed=True, user=self.request.user) + device.webauthndevice 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) + device.webauthndevice for device in U2FDevice.objects.filter(confirmed=True, user=self.request.user) ] if devices: - webauthn_assertion_options = webauthn.WebAuthnAssertionOptions( - devices, - challenge + auth_options = webauthn.generate_authentication_options( + rp_id=get_webauthn_rp_id(self.request), + challenge=challenge, + allow_credentials=devices, ) - ad = webauthn_assertion_options.assertion_dict - ad['extensions'] = { - 'appid': get_u2f_appid(self.request) - } - ctx['jsondata'] = json.dumps(ad) + + # Backwards compatibility to U2F + j = json.loads(webauthn.options_to_json(auth_options)) + j["extensions"] = {"appid": get_u2f_appid(self.request)} + ctx['jsondata'] = json.dumps(j) ctx['form'] = self.form return ctx @@ -387,23 +404,26 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa if 'webauthn_challenge' in self.request.session: del self.request.session['webauthn_challenge'] - challenge = generate_challenge(32) - ukey = generate_ukey() + challenge = generate_challenge() + ukey = generate_user_handle() - self.request.session['webauthn_challenge'] = challenge - self.request.session['webauthn_register_ukey'] = ukey + self.request.session['webauthn_challenge'] = base64.b64encode(challenge).decode() + self.request.session['webauthn_register_ukey'] = base64.b64encode(ukey).decode() - 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, - attestation="none" + devices = [ + device.webauthndevice for device in WebAuthnDevice.objects.filter(confirmed=True, user=self.request.user) + ] + [ + device.webauthndevice for device in U2FDevice.objects.filter(confirmed=True, user=self.request.user) + ] + make_credential_options = webauthn.generate_registration_options( + rp_id=get_webauthn_rp_id(self.request), + rp_name=get_webauthn_rp_id(self.request), + user_id=ukey, + user_name=self.request.user.email, + challenge=challenge, + exclude_credentials=devices, ) - ctx['jsondata'] = json.dumps(make_credential_options.registration_dict) + ctx['jsondata'] = webauthn.options_to_json(make_credential_options) return ctx @@ -412,30 +432,13 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa 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 + registration_verification = webauthn.verify_registration_response( + credential=resp, + expected_challenge=base64.b64decode(challenge), + expected_rp_id=get_webauthn_rp_id(self.request), + expected_origin=settings.SITE_URL, ) - 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 @@ -443,7 +446,7 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa # 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 + credential_id=registration_verification.credential_id ).first() if credential_id_exists: messages.error(request, _('This security device is already registered.')) @@ -451,14 +454,11 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa '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.credential_id = websafe_encode(registration_verification.credential_id) + self.device.ukey = websafe_encode(ukey) + self.device.pub_key = websafe_encode(registration_verification.credential_public_key) + self.device.sign_count = registration_verification.sign_count + self.device.rp_id = get_webauthn_rp_id(request) self.device.icon_url = settings.SITE_URL self.device.confirmed = True self.device.save() diff --git a/src/pretix/helpers/webauthn.py b/src/pretix/helpers/webauthn.py deleted file mode 100644 index beba1c7e8..000000000 --- a/src/pretix/helpers/webauthn.py +++ /dev/null @@ -1,45 +0,0 @@ -# -# This file is part of pretix (Community Edition). -# -# Copyright (C) 2014-2020 Raphael Michel and contributors -# Copyright (C) 2020-2021 rami.io GmbH and contributors -# -# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General -# Public License as published by the Free Software Foundation in version 3 of the License. -# -# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are -# applicable granting you additional permissions and placing additional restrictions on your usage of this software. -# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive -# this file, see . -# -# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied -# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -# details. -# -# You should have received a copy of the GNU Affero General Public License along with this program. If not, see -# . -# -import random -import string - - -def generate_challenge(challenge_len): - return ''.join([ - random.SystemRandom().choice(string.ascii_letters + string.digits) - for i in range(challenge_len) - ]) - - -def generate_ukey(): - """ - Its value's id member is required, and contains an identifier - for the account, specified by the Relying Party. This is not meant - to be displayed to the user, but is used by the Relying Party to - control the number of credentials - an authenticator will never - contain more than one credential for a given Relying Party under - the same id. - A unique identifier for the entity. For a relying party entity, - sets the RP ID. For a user account entity, this will be an - arbitrary string specified by the relying party. - """ - return generate_challenge(20) diff --git a/src/pretix/static/pretixcontrol/js/ui/webauthn.js b/src/pretix/static/pretixcontrol/js/ui/webauthn.js index 7b4094c45..a1722a2d4 100644 --- a/src/pretix/static/pretixcontrol/js/ui/webauthn.js +++ b/src/pretix/static/pretixcontrol/js/ui/webauthn.js @@ -35,16 +35,23 @@ async function fetch_json(url, options) { * @param {Object} credentialCreateOptionsFromServer */ const transformCredentialCreateOptions = function (credentialCreateOptionsFromServer) { - let {challenge, user} = credentialCreateOptionsFromServer; - user.id = Uint8Array.from( - atob(credentialCreateOptionsFromServer.user.id), c => c.charCodeAt(0)); + let {challenge, user, excludeCredentials} = credentialCreateOptionsFromServer; + user.id = user.id.replace(/\_/g, "/").replace(/\-/g, "+"); + user.id = Uint8Array.from(atob(user.id), c => c.charCodeAt(0)); - challenge = Uint8Array.from( - atob(credentialCreateOptionsFromServer.challenge), c => c.charCodeAt(0)); + challenge = challenge.replace(/\_/g, "/").replace(/\-/g, "+"); + challenge = Uint8Array.from(atob(challenge), c => c.charCodeAt(0)); + + excludeCredentials = excludeCredentials.map(credentialDescriptor => { + let {id} = credentialDescriptor; + id = id.replace(/\_/g, "/").replace(/\-/g, "+"); + id = Uint8Array.from(atob(id), c => c.charCodeAt(0)); + return Object.assign({}, credentialDescriptor, {id}); + }); const transformedCredentialCreateOptions = Object.assign( {}, credentialCreateOptionsFromServer, - {challenge, user}); + {challenge, user, excludeCredentials}); return transformedCredentialCreateOptions; }; @@ -56,22 +63,25 @@ const transformCredentialCreateOptions = function (credentialCreateOptionsFromSe * @param {PublicKeyCredential} newAssertion */ const transformNewAssertionForServer = (newAssertion) => { - const attObj = new Uint8Array( - newAssertion.response.attestationObject); - const clientDataJSON = new Uint8Array( - newAssertion.response.clientDataJSON); - const rawId = new Uint8Array( - newAssertion.rawId); + const attObj = new Uint8Array(newAssertion.response.attestationObject); + const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON); + const rawId = new Uint8Array(newAssertion.rawId); + const transports = newAssertion.response.getTransports(); + const authenticatorAttachment = newAssertion.authenticatorAttachment; const registrationClientExtensions = newAssertion.getClientExtensionResults(); return { id: newAssertion.id, rawId: b64enc(rawId), + response: { + attestationObject: b64enc(attObj), + clientDataJSON: b64enc(clientDataJSON), + transports: transports, + }, type: newAssertion.type, - attObj: b64enc(attObj), - clientData: b64enc(clientDataJSON), - registrationClientExtensions: JSON.stringify(registrationClientExtensions) + clientExtensionResults: JSON.stringify(registrationClientExtensions), + authenticatorAttachment: authenticatorAttachment, }; }; @@ -79,8 +89,8 @@ const transformNewAssertionForServer = (newAssertion) => { const transformCredentialRequestOptions = (credentialRequestOptionsFromServer) => { let {challenge, allowCredentials} = credentialRequestOptionsFromServer; - challenge = Uint8Array.from( - atob(challenge), c => c.charCodeAt(0)); + challenge = challenge.replace(/\_/g, "/").replace(/\-/g, "+"); + challenge = Uint8Array.from(atob(challenge), c => c.charCodeAt(0)); allowCredentials = allowCredentials.map(credentialDescriptor => { let {id} = credentialDescriptor; @@ -106,16 +116,22 @@ const transformAssertionForServer = (newAssertion) => { const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON); const rawId = new Uint8Array(newAssertion.rawId); const sig = new Uint8Array(newAssertion.response.signature); + const userHandle = new Uint8Array(newAssertion.response.userHandle); const assertionClientExtensions = newAssertion.getClientExtensionResults(); + const authenticatorAttachment = newAssertion.authenticatorAttachment; return { id: newAssertion.id, rawId: b64enc(rawId), type: newAssertion.type, - authData: b64RawEnc(authData), - clientData: b64RawEnc(clientDataJSON), - signature: hexEncode(sig), - assertionClientExtensions: JSON.stringify(assertionClientExtensions) + response: { + authenticatorData: b64RawEnc(authData), + clientDataJSON: b64RawEnc(clientDataJSON), + signature: b64RawEnc(sig), + userHandle: b64RawEnc(userHandle), + }, + authenticatorAttachment: authenticatorAttachment, + clientExtensionResults: JSON.stringify(assertionClientExtensions) }; }; diff --git a/src/tests/control/test_auth.py b/src/tests/control/test_auth.py index e531904e1..148e0f2cf 100644 --- a/src/tests/control/test_auth.py +++ b/src/tests/control/test_auth.py @@ -45,6 +45,9 @@ from django.test import TestCase, override_settings from django.utils.timezone import now from django_otp.oath import TOTP from django_otp.plugins.otp_totp.models import TOTPDevice +from webauthn.authentication.verify_authentication_response import ( + VerifiedAuthentication, +) from pretix.base.models import U2FDevice, User from pretix.helpers import security @@ -382,7 +385,7 @@ class Login2FAFormTest(TestCase): raise Exception("Failed") m = self.monkeypatch - m.setattr("webauthn.WebAuthnAssertionResponse.verify", fail) + m.setattr("webauthn.verify_authentication_response", fail) U2FDevice.objects.create( user=self.user, name='test', json_data='{"appId": "https://local.pretix.eu", "keyHandle": ' @@ -403,7 +406,10 @@ class Login2FAFormTest(TestCase): def test_u2f_valid(self): m = self.monkeypatch - m.setattr("webauthn.WebAuthnAssertionResponse.verify", lambda *args, **kwargs: 1) + m.setattr("webauthn.verify_authentication_response", + lambda *args, **kwargs: VerifiedAuthentication( + b'', 1, 'single_device', True, + )) U2FDevice.objects.create( user=self.user, name='test', diff --git a/src/tests/control/test_user.py b/src/tests/control/test_user.py index 333a8c372..f28368ee7 100644 --- a/src/tests/control/test_user.py +++ b/src/tests/control/test_user.py @@ -40,7 +40,9 @@ from django_otp.oath import TOTP from django_otp.plugins.otp_static.models import StaticDevice from django_otp.plugins.otp_totp.models import TOTPDevice from tests.base import SoupTest, extract_form_fields -from webauthn import WebAuthnCredential +from webauthn.registration.verify_registration_response import ( + VerifiedRegistration, +) from pretix.base.models import ( Event, Organizer, U2FDevice, User, WebAuthnDevice, @@ -356,9 +358,9 @@ class UserSettings2FATest(SoupTest): }, follow=True) m = self.monkeypatch - m.setattr("webauthn.WebAuthnRegistrationResponse.verify", - lambda *args, **kwargs: WebAuthnCredential( - '', '', b'asd', b'foo', 1 + m.setattr("webauthn.verify_registration_response", + lambda *args, **kwargs: VerifiedRegistration( + b'', b'', 1, '', 'foo', 'public-key', True, b'', 'single_device', True )) d = WebAuthnDevice.objects.first()