forked from CGM_Public/pretix_original
Update webauthn requirement from ==0.4.* to ==2.0.* (#3880)
* Get rid of unmaintained dependency python-u2flib-server * Update webauthn requirement from ==0.4.* to ==2.0.* * Fix tests * Update src/pretix/control/views/auth.py Co-authored-by: Richard Schreiber <schreiber@rami.io> * Update src/pretix/control/views/auth.py Co-authored-by: Richard Schreiber <schreiber@rami.io> * Update src/pretix/control/views/user.py Co-authored-by: Richard Schreiber <schreiber@rami.io> * Update src/pretix/control/views/user.py Co-authored-by: Richard Schreiber <schreiber@rami.io> * Update src/pretix/control/views/user.py Co-authored-by: Richard Schreiber <schreiber@rami.io> --------- Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# 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
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
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)
|
||||
@@ -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)
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user