mirror of
https://github.com/pretix/pretix.git
synced 2026-08-17 11:56:27 +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:
@@ -115,5 +115,5 @@ class User2FADeviceAddForm(forms.Form):
|
|||||||
name = forms.CharField(label=_('Device name'), max_length=64)
|
name = forms.CharField(label=_('Device name'), max_length=64)
|
||||||
devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=(
|
devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=(
|
||||||
('totp', _('Smartphone with the Authenticator application')),
|
('totp', _('Smartphone with the Authenticator application')),
|
||||||
('u2f', _('U2F-compatible hardware token (e.g. Yubikey)')),
|
('webauthn', _('WebAuthn-compatible hardware token (e.g. Yubikey)')),
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
from ..settings import GlobalSettingsObject_SettingsStore
|
from ..settings import GlobalSettingsObject_SettingsStore
|
||||||
from .auth import U2FDevice, User
|
from .auth import U2FDevice, User, WebAuthnDevice
|
||||||
from .base import CachedFile, LoggedModel, cachedfile_name
|
from .base import CachedFile, LoggedModel, cachedfile_name
|
||||||
from .checkin import Checkin, CheckinList
|
from .checkin import Checkin, CheckinList
|
||||||
from .devices import Device
|
from .devices import Device
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
import binascii
|
||||||
|
import json
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import webauthn
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import (
|
from django.contrib.auth.models import (
|
||||||
AbstractBaseUser, BaseUserManager, PermissionsMixin,
|
AbstractBaseUser, BaseUserManager, PermissionsMixin,
|
||||||
@@ -13,6 +17,9 @@ from django.utils.timezone import now
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django_otp.models import Device
|
from django_otp.models import Device
|
||||||
from django_scopes import scopes_disabled
|
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.base.i18n import language
|
||||||
from pretix.helpers.urls import build_absolute_uri
|
from pretix.helpers.urls import build_absolute_uri
|
||||||
@@ -380,3 +387,49 @@ class StaffSessionAuditLog(models.Model):
|
|||||||
|
|
||||||
class U2FDevice(Device):
|
class U2FDevice(Device):
|
||||||
json_data = models.TextField()
|
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
|
||||||
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load compress %}
|
{% load compress %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form class="form-signin" action="" method="post" id="u2f-form">
|
<form class="form-signin" action="" method="post" id="webauthn-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<h3>{% trans "Welcome back!" %}</h3>
|
<h3>{% trans "Welcome back!" %}</h3>
|
||||||
<p>
|
<p>
|
||||||
@@ -12,14 +12,14 @@
|
|||||||
</p>
|
</p>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input class="form-control" name="token" placeholder="{% trans "Token" %}"
|
<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>
|
||||||
<div class="sr-only alert alert-danger" id="u2f-error">
|
<div class="sr-only alert alert-danger" id="webauthn-error">
|
||||||
{% trans "U2F failed. Check that the correct authentication device is correctly plugged in." %}
|
{% trans "WebAuthn failed. Check that the correct authentication device is correctly plugged in." %}
|
||||||
</div>
|
</div>
|
||||||
{% if jsondata %}
|
{% if jsondata %}
|
||||||
<p><small>
|
<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>
|
</small></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="form-group buttons">
|
<div class="form-group buttons">
|
||||||
@@ -29,14 +29,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% if jsondata %}
|
{% if jsondata %}
|
||||||
<script type="text/json" id="u2f-login">
|
<script type="text/json" id="webauthn-login">
|
||||||
{{ jsondata|safe }}
|
{{ jsondata|safe }}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% compress js %}
|
{% compress js %}
|
||||||
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
|
<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/base64js.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/u2f.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/webauthn.js" %}"></script>
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+8
-8
@@ -6,13 +6,13 @@
|
|||||||
{% block title %}{% trans "Add a two-factor authentication device" %}{% endblock %}
|
{% block title %}{% trans "Add a two-factor authentication device" %}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{% trans "Add a two-factor authentication device" %}</h1>
|
<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>
|
<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>
|
</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 %}
|
{% 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>
|
<p>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" name="activate" checked="checked" value="on">
|
<input type="checkbox" name="activate" checked="checked" value="on">
|
||||||
@@ -22,16 +22,16 @@
|
|||||||
<button class="btn btn-primary sr-only" type="submit"></button>
|
<button class="btn btn-primary sr-only" type="submit"></button>
|
||||||
</form>
|
</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." %}
|
{% trans "Device registration failed." %}
|
||||||
</div>
|
</div>
|
||||||
<script type="text/json" id="u2f-enroll">
|
<script type="text/json" id="webauthn-enroll">
|
||||||
{{ jsondata|safe }}
|
{{ jsondata|safe }}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% compress js %}
|
{% compress js %}
|
||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/u2f-api.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/base64js.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/u2f.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/webauthn.js" %}"></script>
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -78,6 +78,8 @@
|
|||||||
</a>
|
</a>
|
||||||
{% if d.devicetype == "totp" %}
|
{% if d.devicetype == "totp" %}
|
||||||
<span class="fa fa-mobile"></span>
|
<span class="fa fa-mobile"></span>
|
||||||
|
{% elif d.devicetype == "webauthn" %}
|
||||||
|
<span class="fa fa-usb"></span>
|
||||||
{% elif d.devicetype == "u2f" %}
|
{% elif d.devicetype == "u2f" %}
|
||||||
<span class="fa fa-usb"></span>
|
<span class="fa fa-usb"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
{% load compress %}
|
{% load compress %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form class="form-signin" id="u2f-form" action="" method="post">
|
<form class="form-signin" id="webauthn-form" action="" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<h3>{% trans "Welcome back!" %}</h3>
|
<h3>{% trans "Welcome back!" %}</h3>
|
||||||
<p>
|
<p>
|
||||||
@@ -19,12 +19,12 @@
|
|||||||
title="" type="password" required="" autofocus>
|
title="" type="password" required="" autofocus>
|
||||||
</div>
|
</div>
|
||||||
{% if jsondata %}
|
{% if jsondata %}
|
||||||
<div class="sr-only alert alert-danger" id="u2f-error">
|
<div class="sr-only alert alert-danger" id="webauthn-error">
|
||||||
{% trans "U2F failed. Check that the correct authentication device is correctly plugged in." %}
|
{% trans "WebAuthn failed. Check that the correct authentication device is correctly plugged in." %}
|
||||||
</div>
|
</div>
|
||||||
<p><small>
|
<p><small>
|
||||||
<span class="fa fa-usb"></span>
|
<span class="fa fa-usb"></span>
|
||||||
{% trans "Alternatively, you can use your U2F device." %}
|
{% trans "Alternatively, you can use your WebAuthn device." %}
|
||||||
</small></p>
|
</small></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="form-group text-right">
|
<div class="form-group text-right">
|
||||||
@@ -37,14 +37,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if jsondata %}
|
{% if jsondata %}
|
||||||
<script type="text/json" id="u2f-login">
|
<script type="text/json" id="webauthn-login">
|
||||||
{{ jsondata|safe }}
|
{{ jsondata|safe }}
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% compress js %}
|
{% compress js %}
|
||||||
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
|
<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/base64js.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/u2f.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/webauthn.js" %}"></script>
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ urlpatterns = [
|
|||||||
name='user.settings.2fa.regenemergency'),
|
name='user.settings.2fa.regenemergency'),
|
||||||
url(r'^settings/2fa/totp/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmTOTPView.as_view(),
|
url(r'^settings/2fa/totp/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmTOTPView.as_view(),
|
||||||
name='user.settings.2fa.confirm.totp'),
|
name='user.settings.2fa.confirm.totp'),
|
||||||
url(r'^settings/2fa/u2f/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmU2FView.as_view(),
|
url(r'^settings/2fa/webauthn/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmWebAuthnView.as_view(),
|
||||||
name='user.settings.2fa.confirm.u2f'),
|
name='user.settings.2fa.confirm.webauthn'),
|
||||||
url(r'^settings/2fa/(?P<devicetype>[^/]+)/(?P<device>[0-9]+)/delete', user.User2FADeviceDeleteView.as_view(),
|
url(r'^settings/2fa/(?P<devicetype>[^/]+)/(?P<device>[0-9]+)/delete', user.User2FADeviceDeleteView.as_view(),
|
||||||
name='user.settings.2fa.delete'),
|
name='user.settings.2fa.delete'),
|
||||||
url(r'^organizers/$', organizer.OrganizerList.as_view(), name='organizers'),
|
url(r'^organizers/$', organizer.OrganizerList.as_view(), name='organizers'),
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
import webauthn
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth import (
|
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.utils.translation import ugettext_lazy as _
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
from django_otp import match_token
|
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 (
|
from pretix.base.forms.auth import (
|
||||||
LoginForm, PasswordForgotForm, PasswordRecoverForm, RegistrationForm,
|
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.base.services.mail import SendMailException
|
||||||
|
from pretix.helpers.webauthn import generate_challenge
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -302,7 +302,7 @@ class Recover(TemplateView):
|
|||||||
|
|
||||||
|
|
||||||
def get_u2f_appid(request):
|
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):
|
class Login2FAView(TemplateView):
|
||||||
@@ -333,15 +333,41 @@ class Login2FAView(TemplateView):
|
|||||||
token = request.POST.get('token', '').strip().replace(' ', '')
|
token = request.POST.get('token', '').strip().replace(' ', '')
|
||||||
|
|
||||||
valid = False
|
valid = False
|
||||||
if '_u2f_challenge' in self.request.session and token.startswith('{'):
|
if 'webauthn_challenge' in self.request.session and token.startswith('{'):
|
||||||
devices = [DeviceRegistration.wrap(device.json_data)
|
challenge = self.request.session['webauthn_challenge']
|
||||||
for device in U2FDevice.objects.filter(confirmed=True, user=self.user)]
|
|
||||||
challenge = self.request.session.pop('_u2f_challenge')
|
resp = json.loads(self.request.POST.get("token"))
|
||||||
try:
|
try:
|
||||||
u2f.verify_authenticate(devices, challenge, token, [self.app_id])
|
devices = [WebAuthnDevice.objects.get(user=self.user, credential_id=resp.get("id"))]
|
||||||
valid = True
|
except WebAuthnDevice.DoesNotExist:
|
||||||
except Exception:
|
devices = U2FDevice.objects.filter(user=self.user)
|
||||||
logger.exception('U2F login failed')
|
|
||||||
|
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:
|
else:
|
||||||
valid = match_token(self.user, token)
|
valid = match_token(self.user, token)
|
||||||
|
|
||||||
@@ -359,18 +385,25 @@ class Login2FAView(TemplateView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data()
|
ctx = super().get_context_data()
|
||||||
|
if 'webauthn_challenge' in self.request.session:
|
||||||
devices = [DeviceRegistration.wrap(device.json_data)
|
del self.request.session['webauthn_challenge']
|
||||||
for device in U2FDevice.objects.filter(confirmed=True, user=self.user)]
|
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:
|
if devices:
|
||||||
challenge = u2f.start_authenticate(devices, challenge=rand_bytes(32))
|
webauthn_assertion_options = webauthn.WebAuthnAssertionOptions(
|
||||||
self.request.session['_u2f_challenge'] = challenge.json
|
devices,
|
||||||
ctx['jsondata'] = challenge.json
|
challenge
|
||||||
else:
|
)
|
||||||
if '_u2f_challenge' in self.request.session:
|
ad = webauthn_assertion_options.assertion_dict
|
||||||
del self.request.session['_u2f_challenge']
|
ad['extensions'] = {
|
||||||
ctx['jsondata'] = None
|
'appid': get_u2f_appid(self.request)
|
||||||
|
}
|
||||||
|
ctx['jsondata'] = json.dumps(ad)
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import base64
|
import base64
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote, urlparse
|
||||||
|
|
||||||
|
import webauthn
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth import update_session_auth_hash
|
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.views.generic import FormView, ListView, TemplateView, UpdateView
|
||||||
from django_otp.plugins.otp_static.models import StaticDevice
|
from django_otp.plugins.otp_static.models import StaticDevice
|
||||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
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.forms.user import User2FADeviceAddForm, UserSettingsForm
|
||||||
from pretix.base.models import (
|
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.models.auth import StaffSession
|
||||||
from pretix.base.notifications import get_all_notification_types
|
from pretix.base.notifications import get_all_notification_types
|
||||||
@@ -34,8 +34,9 @@ from pretix.control.permissions import (
|
|||||||
AdministratorPermissionRequiredMixin, StaffMemberRequiredMixin,
|
AdministratorPermissionRequiredMixin, StaffMemberRequiredMixin,
|
||||||
)
|
)
|
||||||
from pretix.control.views.auth import get_u2f_appid
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -52,23 +53,45 @@ class RecentAuthenticationRequiredMixin:
|
|||||||
class ReauthView(TemplateView):
|
class ReauthView(TemplateView):
|
||||||
template_name = 'pretixcontrol/user/reauth.html'
|
template_name = 'pretixcontrol/user/reauth.html'
|
||||||
|
|
||||||
@property
|
|
||||||
def app_id(self):
|
|
||||||
return get_u2f_appid(self.request)
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
password = request.POST.get("password", "")
|
password = request.POST.get("password", "")
|
||||||
valid = False
|
valid = False
|
||||||
|
|
||||||
if '_u2f_challenge' in self.request.session and password.startswith('{'):
|
if 'webauthn_challenge' in self.request.session and password.startswith('{'):
|
||||||
devices = [DeviceRegistration.wrap(device.json_data)
|
challenge = self.request.session['webauthn_challenge']
|
||||||
for device in U2FDevice.objects.filter(confirmed=True, user=self.request.user)]
|
|
||||||
challenge = self.request.session.pop('_u2f_challenge')
|
resp = json.loads(password)
|
||||||
try:
|
try:
|
||||||
u2f.verify_authenticate(devices, challenge, password, [self.app_id])
|
devices = [WebAuthnDevice.objects.get(user=self.request.user, credential_id=resp.get("id"))]
|
||||||
valid = True
|
except WebAuthnDevice.DoesNotExist:
|
||||||
except Exception:
|
devices = U2FDevice.objects.filter(user=self.request.user)
|
||||||
logger.exception('U2F login failed')
|
|
||||||
|
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)
|
valid = valid or request.user.check_password(password)
|
||||||
|
|
||||||
@@ -85,18 +108,25 @@ class ReauthView(TemplateView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data()
|
ctx = super().get_context_data()
|
||||||
|
if 'webauthn_challenge' in self.request.session:
|
||||||
devices = [DeviceRegistration.wrap(device.json_data)
|
del self.request.session['webauthn_challenge']
|
||||||
for device in U2FDevice.objects.filter(confirmed=True, user=self.request.user)]
|
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:
|
if devices:
|
||||||
challenge = u2f.start_authenticate(devices, challenge=rand_bytes(32))
|
webauthn_assertion_options = webauthn.WebAuthnAssertionOptions(
|
||||||
self.request.session['_u2f_challenge'] = challenge.json
|
devices,
|
||||||
ctx['jsondata'] = challenge.json
|
challenge
|
||||||
else:
|
)
|
||||||
if '_u2f_challenge' in self.request.session:
|
ad = webauthn_assertion_options.assertion_dict
|
||||||
del self.request.session['_u2f_challenge']
|
ad['extensions'] = {
|
||||||
ctx['jsondata'] = None
|
'appid': get_u2f_appid(self.request)
|
||||||
|
}
|
||||||
|
ctx['jsondata'] = json.dumps(ad)
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
@@ -200,6 +230,8 @@ class User2FAMainView(RecentAuthenticationRequiredMixin, TemplateView):
|
|||||||
obj.devicetype = 'totp'
|
obj.devicetype = 'totp'
|
||||||
elif dt == U2FDevice:
|
elif dt == U2FDevice:
|
||||||
obj.devicetype = 'u2f'
|
obj.devicetype = 'u2f'
|
||||||
|
elif dt == WebAuthnDevice:
|
||||||
|
obj.devicetype = 'webauthn'
|
||||||
ctx['devices'] += objs
|
ctx['devices'] += objs
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
@@ -212,11 +244,12 @@ class User2FADeviceAddView(RecentAuthenticationRequiredMixin, FormView):
|
|||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
if form.cleaned_data['devicetype'] == 'totp':
|
if form.cleaned_data['devicetype'] == 'totp':
|
||||||
dev = TOTPDevice.objects.create(user=self.request.user, confirmed=False, name=form.cleaned_data['name'])
|
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():
|
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)
|
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={
|
return redirect(reverse('control:user.settings.2fa.confirm.' + form.cleaned_data['devicetype'], kwargs={
|
||||||
'device': dev.pk
|
'device': dev.pk
|
||||||
}))
|
}))
|
||||||
@@ -233,6 +266,8 @@ class User2FADeviceDeleteView(RecentAuthenticationRequiredMixin, TemplateView):
|
|||||||
def device(self):
|
def device(self):
|
||||||
if self.kwargs['devicetype'] == 'totp':
|
if self.kwargs['devicetype'] == 'totp':
|
||||||
return get_object_or_404(TOTPDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
|
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':
|
elif self.kwargs['devicetype'] == 'u2f':
|
||||||
return get_object_or_404(U2FDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
|
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'))
|
return redirect(reverse('control:user.settings.2fa'))
|
||||||
|
|
||||||
|
|
||||||
class User2FADeviceConfirmU2FView(RecentAuthenticationRequiredMixin, TemplateView):
|
class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, TemplateView):
|
||||||
template_name = 'pretixcontrol/user/2fa_confirm_u2f.html'
|
template_name = 'pretixcontrol/user/2fa_confirm_webauthn.html'
|
||||||
|
|
||||||
@property
|
|
||||||
def app_id(self):
|
|
||||||
return get_u2f_appid(self.request)
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def device(self):
|
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):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data()
|
ctx = super().get_context_data()
|
||||||
ctx['device'] = self.device
|
ctx['device'] = self.device
|
||||||
|
|
||||||
devices = [DeviceRegistration.wrap(device.json_data)
|
if 'webauthn_register_ukey' in self.request.session:
|
||||||
for device in U2FDevice.objects.filter(confirmed=True, user=self.request.user)]
|
del self.request.session['webauthn_register_ukey']
|
||||||
enroll = u2f.start_register(self.app_id, devices)
|
if 'webauthn_challenge' in self.request.session:
|
||||||
self.request.session['_u2f_enroll'] = enroll.json
|
del self.request.session['webauthn_challenge']
|
||||||
ctx['jsondata'] = enroll.json
|
|
||||||
|
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
|
return ctx
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
binding, cert = u2f.complete_register(self.request.session.pop('_u2f_enroll'),
|
challenge = self.request.session['webauthn_challenge']
|
||||||
request.POST.get('token'),
|
ukey = self.request.session['webauthn_register_ukey']
|
||||||
[self.app_id])
|
resp = json.loads(self.request.POST.get("token"))
|
||||||
self.device.json_data = binding.json
|
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.confirmed = True
|
||||||
self.device.save()
|
self.device.save()
|
||||||
self.request.user.log_action('pretix.user.settings.2fa.device.added', user=self.request.user, data={
|
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'))
|
return redirect(reverse('control:user.settings.2fa'))
|
||||||
except Exception:
|
except Exception:
|
||||||
messages.error(request, _('The registration could not be completed. Please try again.'))
|
messages.error(request, _('The registration could not be completed. Please try again.'))
|
||||||
logger.exception('U2F registration failed')
|
logger.exception('WebAuthn registration failed')
|
||||||
return redirect(reverse('control:user.settings.2fa.confirm.u2f', kwargs={
|
return redirect(reverse('control:user.settings.2fa.confirm.webauthn', kwargs={
|
||||||
'device': self.device.pk
|
'device': self.device.pk
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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)
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
var lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||||
|
|
||||||
|
;(function (exports) {
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
var Arr = (typeof Uint8Array !== 'undefined')
|
||||||
|
? Uint8Array
|
||||||
|
: Array
|
||||||
|
|
||||||
|
var PLUS = '+'.charCodeAt(0)
|
||||||
|
var SLASH = '/'.charCodeAt(0)
|
||||||
|
var NUMBER = '0'.charCodeAt(0)
|
||||||
|
var LOWER = 'a'.charCodeAt(0)
|
||||||
|
var UPPER = 'A'.charCodeAt(0)
|
||||||
|
var PLUS_URL_SAFE = '-'.charCodeAt(0)
|
||||||
|
var SLASH_URL_SAFE = '_'.charCodeAt(0)
|
||||||
|
|
||||||
|
function decode (elt) {
|
||||||
|
var code = elt.charCodeAt(0)
|
||||||
|
if (code === PLUS || code === PLUS_URL_SAFE) return 62 // '+'
|
||||||
|
if (code === SLASH || code === SLASH_URL_SAFE) return 63 // '/'
|
||||||
|
if (code < NUMBER) return -1 // no match
|
||||||
|
if (code < NUMBER + 10) return code - NUMBER + 26 + 26
|
||||||
|
if (code < UPPER + 26) return code - UPPER
|
||||||
|
if (code < LOWER + 26) return code - LOWER + 26
|
||||||
|
}
|
||||||
|
|
||||||
|
function b64ToByteArray (b64) {
|
||||||
|
var i, j, l, tmp, placeHolders, arr
|
||||||
|
|
||||||
|
if (b64.length % 4 > 0) {
|
||||||
|
throw new Error('Invalid string. Length must be a multiple of 4')
|
||||||
|
}
|
||||||
|
|
||||||
|
// the number of equal signs (place holders)
|
||||||
|
// if there are two placeholders, than the two characters before it
|
||||||
|
// represent one byte
|
||||||
|
// if there is only one, then the three characters before it represent 2 bytes
|
||||||
|
// this is just a cheap hack to not do indexOf twice
|
||||||
|
var len = b64.length
|
||||||
|
placeHolders = b64.charAt(len - 2) === '=' ? 2 : b64.charAt(len - 1) === '=' ? 1 : 0
|
||||||
|
|
||||||
|
// base64 is 4/3 + up to two characters of the original data
|
||||||
|
arr = new Arr(b64.length * 3 / 4 - placeHolders)
|
||||||
|
|
||||||
|
// if there are placeholders, only get up to the last complete 4 chars
|
||||||
|
l = placeHolders > 0 ? b64.length - 4 : b64.length
|
||||||
|
|
||||||
|
var L = 0
|
||||||
|
|
||||||
|
function push (v) {
|
||||||
|
arr[L++] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0, j = 0; i < l; i += 4, j += 3) {
|
||||||
|
tmp = (decode(b64.charAt(i)) << 18) | (decode(b64.charAt(i + 1)) << 12) | (decode(b64.charAt(i + 2)) << 6) | decode(b64.charAt(i + 3))
|
||||||
|
push((tmp & 0xFF0000) >> 16)
|
||||||
|
push((tmp & 0xFF00) >> 8)
|
||||||
|
push(tmp & 0xFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (placeHolders === 2) {
|
||||||
|
tmp = (decode(b64.charAt(i)) << 2) | (decode(b64.charAt(i + 1)) >> 4)
|
||||||
|
push(tmp & 0xFF)
|
||||||
|
} else if (placeHolders === 1) {
|
||||||
|
tmp = (decode(b64.charAt(i)) << 10) | (decode(b64.charAt(i + 1)) << 4) | (decode(b64.charAt(i + 2)) >> 2)
|
||||||
|
push((tmp >> 8) & 0xFF)
|
||||||
|
push(tmp & 0xFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
function uint8ToBase64 (uint8) {
|
||||||
|
var i
|
||||||
|
var extraBytes = uint8.length % 3 // if we have 1 byte left, pad 2 bytes
|
||||||
|
var output = ''
|
||||||
|
var temp, length
|
||||||
|
|
||||||
|
function encode (num) {
|
||||||
|
return lookup.charAt(num)
|
||||||
|
}
|
||||||
|
|
||||||
|
function tripletToBase64 (num) {
|
||||||
|
return encode(num >> 18 & 0x3F) + encode(num >> 12 & 0x3F) + encode(num >> 6 & 0x3F) + encode(num & 0x3F)
|
||||||
|
}
|
||||||
|
|
||||||
|
// go through the array every three bytes, we'll deal with trailing stuff later
|
||||||
|
for (i = 0, length = uint8.length - extraBytes; i < length; i += 3) {
|
||||||
|
temp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2])
|
||||||
|
output += tripletToBase64(temp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pad the end with zeros, but make sure to not forget the extra bytes
|
||||||
|
switch (extraBytes) {
|
||||||
|
case 1:
|
||||||
|
temp = uint8[uint8.length - 1]
|
||||||
|
output += encode(temp >> 2)
|
||||||
|
output += encode((temp << 4) & 0x3F)
|
||||||
|
output += '=='
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
temp = (uint8[uint8.length - 2] << 8) + (uint8[uint8.length - 1])
|
||||||
|
output += encode(temp >> 10)
|
||||||
|
output += encode((temp >> 4) & 0x3F)
|
||||||
|
output += encode((temp << 2) & 0x3F)
|
||||||
|
output += '='
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.toByteArray = b64ToByteArray
|
||||||
|
exports.fromByteArray = uint8ToBase64
|
||||||
|
}(typeof exports === 'undefined' ? (this.base64js = {}) : exports))
|
||||||
@@ -1,760 +0,0 @@
|
|||||||
//Copyright 2014-2015 Google Inc. All rights reserved.
|
|
||||||
// Extended by Raphael Michel 2017 for compatibility with Firefox 57
|
|
||||||
//Use of this source code is governed by a BSD-style
|
|
||||||
//license that can be found in the LICENSE file or at
|
|
||||||
//https://developers.google.com/open-source/licenses/bsd
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @fileoverview The U2F api.
|
|
||||||
*/
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
|
|
||||||
var build_u2f_object = function () {
|
|
||||||
/**
|
|
||||||
* Namespace for the U2F api.
|
|
||||||
* @type {Object}
|
|
||||||
*/
|
|
||||||
var u2f = u2f || {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* FIDO U2F Javascript API Version
|
|
||||||
* @number
|
|
||||||
*/
|
|
||||||
var js_api_version;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The U2F extension id
|
|
||||||
* @const {string}
|
|
||||||
*/
|
|
||||||
// The Chrome packaged app extension ID.
|
|
||||||
// Uncomment this if you want to deploy a server instance that uses
|
|
||||||
// the package Chrome app and does not require installing the U2F Chrome extension.
|
|
||||||
u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
|
|
||||||
// The U2F Chrome extension ID.
|
|
||||||
// Uncomment this if you want to deploy a server instance that uses
|
|
||||||
// the U2F Chrome extension to authenticate.
|
|
||||||
// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne';
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Message types for messsages to/from the extension
|
|
||||||
* @const
|
|
||||||
* @enum {string}
|
|
||||||
*/
|
|
||||||
u2f.MessageTypes = {
|
|
||||||
'U2F_REGISTER_REQUEST': 'u2f_register_request',
|
|
||||||
'U2F_REGISTER_RESPONSE': 'u2f_register_response',
|
|
||||||
'U2F_SIGN_REQUEST': 'u2f_sign_request',
|
|
||||||
'U2F_SIGN_RESPONSE': 'u2f_sign_response',
|
|
||||||
'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request',
|
|
||||||
'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response'
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Response status codes
|
|
||||||
* @const
|
|
||||||
* @enum {number}
|
|
||||||
*/
|
|
||||||
u2f.ErrorCodes = {
|
|
||||||
'OK': 0,
|
|
||||||
'OTHER_ERROR': 1,
|
|
||||||
'BAD_REQUEST': 2,
|
|
||||||
'CONFIGURATION_UNSUPPORTED': 3,
|
|
||||||
'DEVICE_INELIGIBLE': 4,
|
|
||||||
'TIMEOUT': 5
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A message for registration requests
|
|
||||||
* @typedef {{
|
|
||||||
* type: u2f.MessageTypes,
|
|
||||||
* appId: ?string,
|
|
||||||
* timeoutSeconds: ?number,
|
|
||||||
* requestId: ?number
|
|
||||||
* }}
|
|
||||||
*/
|
|
||||||
u2f.U2fRequest;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A message for registration responses
|
|
||||||
* @typedef {{
|
|
||||||
* type: u2f.MessageTypes,
|
|
||||||
* responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse),
|
|
||||||
* requestId: ?number
|
|
||||||
* }}
|
|
||||||
*/
|
|
||||||
u2f.U2fResponse;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An error object for responses
|
|
||||||
* @typedef {{
|
|
||||||
* errorCode: u2f.ErrorCodes,
|
|
||||||
* errorMessage: ?string
|
|
||||||
* }}
|
|
||||||
*/
|
|
||||||
u2f.Error;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data object for a single sign request.
|
|
||||||
* @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}}
|
|
||||||
*/
|
|
||||||
u2f.Transport;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data object for a single sign request.
|
|
||||||
* @typedef {Array<u2f.Transport>}
|
|
||||||
*/
|
|
||||||
u2f.Transports;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data object for a single sign request.
|
|
||||||
* @typedef {{
|
|
||||||
* version: string,
|
|
||||||
* challenge: string,
|
|
||||||
* keyHandle: string,
|
|
||||||
* appId: string
|
|
||||||
* }}
|
|
||||||
*/
|
|
||||||
u2f.SignRequest;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data object for a sign response.
|
|
||||||
* @typedef {{
|
|
||||||
* keyHandle: string,
|
|
||||||
* signatureData: string,
|
|
||||||
* clientData: string
|
|
||||||
* }}
|
|
||||||
*/
|
|
||||||
u2f.SignResponse;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data object for a registration request.
|
|
||||||
* @typedef {{
|
|
||||||
* version: string,
|
|
||||||
* challenge: string
|
|
||||||
* }}
|
|
||||||
*/
|
|
||||||
u2f.RegisterRequest;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data object for a registration response.
|
|
||||||
* @typedef {{
|
|
||||||
* version: string,
|
|
||||||
* keyHandle: string,
|
|
||||||
* transports: Transports,
|
|
||||||
* appId: string
|
|
||||||
* }}
|
|
||||||
*/
|
|
||||||
u2f.RegisterResponse;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data object for a registered key.
|
|
||||||
* @typedef {{
|
|
||||||
* version: string,
|
|
||||||
* keyHandle: string,
|
|
||||||
* transports: ?Transports,
|
|
||||||
* appId: ?string
|
|
||||||
* }}
|
|
||||||
*/
|
|
||||||
u2f.RegisteredKey;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data object for a get API register response.
|
|
||||||
* @typedef {{
|
|
||||||
* js_api_version: number
|
|
||||||
* }}
|
|
||||||
*/
|
|
||||||
u2f.GetJsApiVersionResponse;
|
|
||||||
|
|
||||||
|
|
||||||
//Low level MessagePort API support
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up a MessagePort to the U2F extension using the
|
|
||||||
* available mechanisms.
|
|
||||||
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
|
|
||||||
*/
|
|
||||||
u2f.getMessagePort = function (callback) {
|
|
||||||
if (typeof chrome != 'undefined' && chrome.runtime) {
|
|
||||||
// The actual message here does not matter, but we need to get a reply
|
|
||||||
// for the callback to run. Thus, send an empty signature request
|
|
||||||
// in order to get a failure response.
|
|
||||||
var msg = {
|
|
||||||
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
|
|
||||||
signRequests: []
|
|
||||||
};
|
|
||||||
chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function () {
|
|
||||||
if (!chrome.runtime.lastError) {
|
|
||||||
// We are on a whitelisted origin and can talk directly
|
|
||||||
// with the extension.
|
|
||||||
u2f.getChromeRuntimePort_(callback);
|
|
||||||
} else {
|
|
||||||
// chrome.runtime was available, but we couldn't message
|
|
||||||
// the extension directly, use iframe
|
|
||||||
u2f.getIframePort_(callback);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (u2f.isAndroidChrome_()) {
|
|
||||||
u2f.getAuthenticatorPort_(callback);
|
|
||||||
} else if (u2f.isIosChrome_()) {
|
|
||||||
u2f.getIosPort_(callback);
|
|
||||||
} else {
|
|
||||||
// chrome.runtime was not available at all, which is normal
|
|
||||||
// when this origin doesn't have access to any extensions.
|
|
||||||
u2f.getIframePort_(callback);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect chrome running on android based on the browser's useragent.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
u2f.isAndroidChrome_ = function () {
|
|
||||||
var userAgent = navigator.userAgent;
|
|
||||||
return userAgent.indexOf('Chrome') != -1 &&
|
|
||||||
userAgent.indexOf('Android') != -1;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect chrome running on iOS based on the browser's platform.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
u2f.isIosChrome_ = function () {
|
|
||||||
return $.inArray(navigator.platform, ["iPhone", "iPad", "iPod"]) > -1;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connects directly to the extension via chrome.runtime.connect.
|
|
||||||
* @param {function(u2f.WrappedChromeRuntimePort_)} callback
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
u2f.getChromeRuntimePort_ = function (callback) {
|
|
||||||
var port = chrome.runtime.connect(u2f.EXTENSION_ID,
|
|
||||||
{'includeTlsChannelId': true});
|
|
||||||
setTimeout(function () {
|
|
||||||
callback(new u2f.WrappedChromeRuntimePort_(port));
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a 'port' abstraction to the Authenticator app.
|
|
||||||
* @param {function(u2f.WrappedAuthenticatorPort_)} callback
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
u2f.getAuthenticatorPort_ = function (callback) {
|
|
||||||
setTimeout(function () {
|
|
||||||
callback(new u2f.WrappedAuthenticatorPort_());
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a 'port' abstraction to the iOS client app.
|
|
||||||
* @param {function(u2f.WrappedIosPort_)} callback
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
u2f.getIosPort_ = function (callback) {
|
|
||||||
setTimeout(function () {
|
|
||||||
callback(new u2f.WrappedIosPort_());
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A wrapper for chrome.runtime.Port that is compatible with MessagePort.
|
|
||||||
* @param {Port} port
|
|
||||||
* @constructor
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
u2f.WrappedChromeRuntimePort_ = function (port) {
|
|
||||||
this.port_ = port;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format and return a sign request compliant with the JS API version supported by the extension.
|
|
||||||
* @param {Array<u2f.SignRequest>} signRequests
|
|
||||||
* @param {number} timeoutSeconds
|
|
||||||
* @param {number} reqId
|
|
||||||
* @return {Object}
|
|
||||||
*/
|
|
||||||
u2f.formatSignRequest_ =
|
|
||||||
function (appId, challenge, registeredKeys, timeoutSeconds, reqId) {
|
|
||||||
if (js_api_version === undefined || js_api_version < 1.1) {
|
|
||||||
// Adapt request to the 1.0 JS API
|
|
||||||
var signRequests = [];
|
|
||||||
for (var i = 0; i < registeredKeys.length; i++) {
|
|
||||||
signRequests[i] = {
|
|
||||||
version: registeredKeys[i].version,
|
|
||||||
challenge: challenge,
|
|
||||||
keyHandle: registeredKeys[i].keyHandle,
|
|
||||||
appId: appId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
|
|
||||||
signRequests: signRequests,
|
|
||||||
timeoutSeconds: timeoutSeconds,
|
|
||||||
requestId: reqId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// JS 1.1 API
|
|
||||||
return {
|
|
||||||
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
|
|
||||||
appId: appId,
|
|
||||||
challenge: challenge,
|
|
||||||
registeredKeys: registeredKeys,
|
|
||||||
timeoutSeconds: timeoutSeconds,
|
|
||||||
requestId: reqId
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format and return a register request compliant with the JS API version supported by the extension..
|
|
||||||
* @param {Array<u2f.SignRequest>} signRequests
|
|
||||||
* @param {Array<u2f.RegisterRequest>} signRequests
|
|
||||||
* @param {number} timeoutSeconds
|
|
||||||
* @param {number} reqId
|
|
||||||
* @return {Object}
|
|
||||||
*/
|
|
||||||
u2f.formatRegisterRequest_ =
|
|
||||||
function (appId, registeredKeys, registerRequests, timeoutSeconds, reqId) {
|
|
||||||
if (js_api_version === undefined || js_api_version < 1.1) {
|
|
||||||
// Adapt request to the 1.0 JS API
|
|
||||||
for (var i = 0; i < registerRequests.length; i++) {
|
|
||||||
registerRequests[i].appId = appId;
|
|
||||||
}
|
|
||||||
var signRequests = [];
|
|
||||||
for (var i = 0; i < registeredKeys.length; i++) {
|
|
||||||
signRequests[i] = {
|
|
||||||
version: registeredKeys[i].version,
|
|
||||||
challenge: registerRequests[0],
|
|
||||||
keyHandle: registeredKeys[i].keyHandle,
|
|
||||||
appId: appId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
|
|
||||||
signRequests: signRequests,
|
|
||||||
registerRequests: registerRequests,
|
|
||||||
timeoutSeconds: timeoutSeconds,
|
|
||||||
requestId: reqId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// JS 1.1 API
|
|
||||||
return {
|
|
||||||
type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
|
|
||||||
appId: appId,
|
|
||||||
registerRequests: registerRequests,
|
|
||||||
registeredKeys: registeredKeys,
|
|
||||||
timeoutSeconds: timeoutSeconds,
|
|
||||||
requestId: reqId
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Posts a message on the underlying channel.
|
|
||||||
* @param {Object} message
|
|
||||||
*/
|
|
||||||
u2f.WrappedChromeRuntimePort_.prototype.postMessage = function (message) {
|
|
||||||
this.port_.postMessage(message);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emulates the HTML 5 addEventListener interface. Works only for the
|
|
||||||
* onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
|
|
||||||
* @param {string} eventName
|
|
||||||
* @param {function({data: Object})} handler
|
|
||||||
*/
|
|
||||||
u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
|
|
||||||
function (eventName, handler) {
|
|
||||||
var name = eventName.toLowerCase();
|
|
||||||
if (name == 'message' || name == 'onmessage') {
|
|
||||||
this.port_.onMessage.addListener(function (message) {
|
|
||||||
// Emulate a minimal MessageEvent object
|
|
||||||
handler({'data': message});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.error('WrappedChromeRuntimePort only supports onMessage');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrap the Authenticator app with a MessagePort interface.
|
|
||||||
* @constructor
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
u2f.WrappedAuthenticatorPort_ = function () {
|
|
||||||
this.requestId_ = -1;
|
|
||||||
this.requestObject_ = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launch the Authenticator intent.
|
|
||||||
* @param {Object} message
|
|
||||||
*/
|
|
||||||
u2f.WrappedAuthenticatorPort_.prototype.postMessage = function (message) {
|
|
||||||
var intentUrl =
|
|
||||||
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
|
|
||||||
';S.request=' + encodeURIComponent(JSON.stringify(message)) +
|
|
||||||
';end';
|
|
||||||
document.location = intentUrl;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tells what type of port this is.
|
|
||||||
* @return {String} port type
|
|
||||||
*/
|
|
||||||
u2f.WrappedAuthenticatorPort_.prototype.getPortType = function () {
|
|
||||||
return "WrappedAuthenticatorPort_";
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emulates the HTML 5 addEventListener interface.
|
|
||||||
* @param {string} eventName
|
|
||||||
* @param {function({data: Object})} handler
|
|
||||||
*/
|
|
||||||
u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function (eventName, handler) {
|
|
||||||
var name = eventName.toLowerCase();
|
|
||||||
if (name == 'message') {
|
|
||||||
var self = this;
|
|
||||||
/* Register a callback to that executes when
|
|
||||||
* chrome injects the response. */
|
|
||||||
window.addEventListener(
|
|
||||||
'message', self.onRequestUpdate_.bind(self, handler), false);
|
|
||||||
} else {
|
|
||||||
console.error('WrappedAuthenticatorPort only supports message');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback invoked when a response is received from the Authenticator.
|
|
||||||
* @param function({data: Object}) callback
|
|
||||||
* @param {Object} message message Object
|
|
||||||
*/
|
|
||||||
u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ =
|
|
||||||
function (callback, message) {
|
|
||||||
var messageObject = JSON.parse(message.data);
|
|
||||||
var intentUrl = messageObject['intentURL'];
|
|
||||||
|
|
||||||
var errorCode = messageObject['errorCode'];
|
|
||||||
var responseObject = null;
|
|
||||||
if (messageObject.hasOwnProperty('data')) {
|
|
||||||
responseObject = /** @type {Object} */ (
|
|
||||||
JSON.parse(messageObject['data']));
|
|
||||||
}
|
|
||||||
|
|
||||||
callback({'data': responseObject});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base URL for intents to Authenticator.
|
|
||||||
* @const
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
|
|
||||||
'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrap the iOS client app with a MessagePort interface.
|
|
||||||
* @constructor
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
u2f.WrappedIosPort_ = function () {
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launch the iOS client app request
|
|
||||||
* @param {Object} message
|
|
||||||
*/
|
|
||||||
u2f.WrappedIosPort_.prototype.postMessage = function (message) {
|
|
||||||
var str = JSON.stringify(message);
|
|
||||||
var url = "u2f://auth?" + encodeURI(str);
|
|
||||||
location.replace(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tells what type of port this is.
|
|
||||||
* @return {String} port type
|
|
||||||
*/
|
|
||||||
u2f.WrappedIosPort_.prototype.getPortType = function () {
|
|
||||||
return "WrappedIosPort_";
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emulates the HTML 5 addEventListener interface.
|
|
||||||
* @param {string} eventName
|
|
||||||
* @param {function({data: Object})} handler
|
|
||||||
*/
|
|
||||||
u2f.WrappedIosPort_.prototype.addEventListener = function (eventName, handler) {
|
|
||||||
var name = eventName.toLowerCase();
|
|
||||||
if (name !== 'message') {
|
|
||||||
console.error('WrappedIosPort only supports message');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up an embedded trampoline iframe, sourced from the extension.
|
|
||||||
* @param {function(MessagePort)} callback
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
u2f.getIframePort_ = function (callback) {
|
|
||||||
// Create the iframe
|
|
||||||
var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID;
|
|
||||||
var iframe = document.createElement('iframe');
|
|
||||||
iframe.src = iframeOrigin + '/u2f-comms.html';
|
|
||||||
iframe.setAttribute('style', 'display:none');
|
|
||||||
document.body.appendChild(iframe);
|
|
||||||
|
|
||||||
var channel = new MessageChannel();
|
|
||||||
var ready = function (message) {
|
|
||||||
if (message.data == 'ready') {
|
|
||||||
channel.port1.removeEventListener('message', ready);
|
|
||||||
callback(channel.port1);
|
|
||||||
} else {
|
|
||||||
console.error('First event on iframe port was not "ready"');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
channel.port1.addEventListener('message', ready);
|
|
||||||
channel.port1.start();
|
|
||||||
|
|
||||||
iframe.addEventListener('load', function () {
|
|
||||||
// Deliver the port to the iframe and initialize
|
|
||||||
iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
//High-level JS API
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default extension response timeout in seconds.
|
|
||||||
* @const
|
|
||||||
*/
|
|
||||||
u2f.EXTENSION_TIMEOUT_SEC = 30;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A singleton instance for a MessagePort to the extension.
|
|
||||||
* @type {MessagePort|u2f.WrappedChromeRuntimePort_}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
u2f.port_ = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callbacks waiting for a port
|
|
||||||
* @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
u2f.waitingForPort_ = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A counter for requestIds.
|
|
||||||
* @type {number}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
u2f.reqCounter_ = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A map from requestIds to client callbacks
|
|
||||||
* @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse))
|
|
||||||
* |function((u2f.Error|u2f.SignResponse)))>}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
u2f.callbackMap_ = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates or retrieves the MessagePort singleton to use.
|
|
||||||
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
u2f.getPortSingleton_ = function (callback) {
|
|
||||||
if (u2f.port_) {
|
|
||||||
callback(u2f.port_);
|
|
||||||
} else {
|
|
||||||
if (u2f.waitingForPort_.length == 0) {
|
|
||||||
u2f.getMessagePort(function (port) {
|
|
||||||
u2f.port_ = port;
|
|
||||||
u2f.port_.addEventListener('message',
|
|
||||||
/** @type {function(Event)} */ (u2f.responseHandler_));
|
|
||||||
|
|
||||||
// Careful, here be async callbacks. Maybe.
|
|
||||||
while (u2f.waitingForPort_.length)
|
|
||||||
u2f.waitingForPort_.shift()(u2f.port_);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
u2f.waitingForPort_.push(callback);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles response messages from the extension.
|
|
||||||
* @param {MessageEvent.<u2f.Response>} message
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
u2f.responseHandler_ = function (message) {
|
|
||||||
var response = message.data;
|
|
||||||
var reqId = response['requestId'];
|
|
||||||
if (!reqId || !u2f.callbackMap_[reqId]) {
|
|
||||||
console.error('Unknown or missing requestId in response.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var cb = u2f.callbackMap_[reqId];
|
|
||||||
delete u2f.callbackMap_[reqId];
|
|
||||||
cb(response['responseData']);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatches an array of sign requests to available U2F tokens.
|
|
||||||
* If the JS API version supported by the extension is unknown, it first sends a
|
|
||||||
* message to the extension to find out the supported API version and then it sends
|
|
||||||
* the sign request.
|
|
||||||
* @param {string=} appId
|
|
||||||
* @param {string=} challenge
|
|
||||||
* @param {Array<u2f.RegisteredKey>} registeredKeys
|
|
||||||
* @param {function((u2f.Error|u2f.SignResponse))} callback
|
|
||||||
* @param {number=} opt_timeoutSeconds
|
|
||||||
*/
|
|
||||||
u2f.sign = function (appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
|
|
||||||
if (js_api_version === undefined) {
|
|
||||||
// Send a message to get the extension to JS API version, then send the actual sign request.
|
|
||||||
u2f.getApiVersion(
|
|
||||||
function (response) {
|
|
||||||
js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version'];
|
|
||||||
console.log("Extension JS API Version: ", js_api_version);
|
|
||||||
u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// We know the JS API version. Send the actual sign request in the supported API version.
|
|
||||||
u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatches an array of sign requests to available U2F tokens.
|
|
||||||
* @param {string=} appId
|
|
||||||
* @param {string=} challenge
|
|
||||||
* @param {Array<u2f.RegisteredKey>} registeredKeys
|
|
||||||
* @param {function((u2f.Error|u2f.SignResponse))} callback
|
|
||||||
* @param {number=} opt_timeoutSeconds
|
|
||||||
*/
|
|
||||||
u2f.sendSignRequest = function (appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
|
|
||||||
u2f.getPortSingleton_(function (port) {
|
|
||||||
var reqId = ++u2f.reqCounter_;
|
|
||||||
u2f.callbackMap_[reqId] = callback;
|
|
||||||
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
|
|
||||||
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
|
|
||||||
var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId);
|
|
||||||
port.postMessage(req);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatches register requests to available U2F tokens. An array of sign
|
|
||||||
* requests identifies already registered tokens.
|
|
||||||
* If the JS API version supported by the extension is unknown, it first sends a
|
|
||||||
* message to the extension to find out the supported API version and then it sends
|
|
||||||
* the register request.
|
|
||||||
* @param {string=} appId
|
|
||||||
* @param {Array<u2f.RegisterRequest>} registerRequests
|
|
||||||
* @param {Array<u2f.RegisteredKey>} registeredKeys
|
|
||||||
* @param {function((u2f.Error|u2f.RegisterResponse))} callback
|
|
||||||
* @param {number=} opt_timeoutSeconds
|
|
||||||
*/
|
|
||||||
u2f.register = function (appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
|
|
||||||
if (js_api_version === undefined) {
|
|
||||||
// Send a message to get the extension to JS API version, then send the actual register request.
|
|
||||||
u2f.getApiVersion(
|
|
||||||
function (response) {
|
|
||||||
js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version'];
|
|
||||||
console.log("Extension JS API Version: ", js_api_version);
|
|
||||||
u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
|
|
||||||
callback, opt_timeoutSeconds);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// We know the JS API version. Send the actual register request in the supported API version.
|
|
||||||
u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
|
|
||||||
callback, opt_timeoutSeconds);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatches register requests to available U2F tokens. An array of sign
|
|
||||||
* requests identifies already registered tokens.
|
|
||||||
* @param {string=} appId
|
|
||||||
* @param {Array<u2f.RegisterRequest>} registerRequests
|
|
||||||
* @param {Array<u2f.RegisteredKey>} registeredKeys
|
|
||||||
* @param {function((u2f.Error|u2f.RegisterResponse))} callback
|
|
||||||
* @param {number=} opt_timeoutSeconds
|
|
||||||
*/
|
|
||||||
u2f.sendRegisterRequest = function (appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
|
|
||||||
u2f.getPortSingleton_(function (port) {
|
|
||||||
var reqId = ++u2f.reqCounter_;
|
|
||||||
u2f.callbackMap_[reqId] = callback;
|
|
||||||
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
|
|
||||||
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
|
|
||||||
var req = u2f.formatRegisterRequest_(
|
|
||||||
appId, registeredKeys, registerRequests, timeoutSeconds, reqId);
|
|
||||||
port.postMessage(req);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatches a message to the extension to find out the supported
|
|
||||||
* JS API version.
|
|
||||||
* If the user is on a mobile phone and is thus using Google Authenticator instead
|
|
||||||
* of the Chrome extension, don't send the request and simply return 0.
|
|
||||||
* @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback
|
|
||||||
* @param {number=} opt_timeoutSeconds
|
|
||||||
*/
|
|
||||||
u2f.getApiVersion = function (callback, opt_timeoutSeconds) {
|
|
||||||
u2f.getPortSingleton_(function (port) {
|
|
||||||
// If we are using Android Google Authenticator or iOS client app,
|
|
||||||
// do not fire an intent to ask which JS API version to use.
|
|
||||||
if (port.getPortType) {
|
|
||||||
var apiVersion;
|
|
||||||
switch (port.getPortType()) {
|
|
||||||
case 'WrappedIosPort_':
|
|
||||||
case 'WrappedAuthenticatorPort_':
|
|
||||||
apiVersion = 1.1;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
apiVersion = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
callback({'js_api_version': apiVersion});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var reqId = ++u2f.reqCounter_;
|
|
||||||
u2f.callbackMap_[reqId] = callback;
|
|
||||||
var req = {
|
|
||||||
type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST,
|
|
||||||
timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ?
|
|
||||||
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC),
|
|
||||||
requestId: reqId
|
|
||||||
};
|
|
||||||
port.postMessage(req);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return u2f;
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
var u2f = build_u2f_object();
|
|
||||||
} catch (TypeError) {
|
|
||||||
// Firefox 57 sets the u2f object itself, we are not allowed to override it,
|
|
||||||
// so fail silently.
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
/*global $,u2f */
|
|
||||||
$(function () {
|
|
||||||
$("#u2f-progress").hide();
|
|
||||||
if ($("#u2f-enroll").length) {
|
|
||||||
var request = JSON.parse($.trim($("#u2f-enroll").html()));
|
|
||||||
$("#u2f-progress").show();
|
|
||||||
setTimeout(function () {
|
|
||||||
var appId = request.registerRequests[0].appId;
|
|
||||||
u2f.register(appId, request.registerRequests, [], function (data) {
|
|
||||||
if (data.errorCode) {
|
|
||||||
$("#u2f-error").removeClass("sr-only");
|
|
||||||
$("#u2f-progress").remove();
|
|
||||||
} else {
|
|
||||||
$('#u2f-response').val(JSON.stringify(data));
|
|
||||||
$('#u2f-form').submit();
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
}, 100);
|
|
||||||
} else if ($("#u2f-login").length) {
|
|
||||||
var request = JSON.parse($.trim($("#u2f-login").html()));
|
|
||||||
$("#u2f-progress").show();
|
|
||||||
setTimeout(function () {
|
|
||||||
var firstr = request.authenticateRequests[0];
|
|
||||||
var appId = firstr.appId;
|
|
||||||
var registeredKeys = [];
|
|
||||||
var reqs = request.authenticateRequests;
|
|
||||||
for (var i = 0; i < reqs.length; i++) {
|
|
||||||
registeredKeys.push({version: reqs[i].version, keyHandle: reqs[i].keyHandle});
|
|
||||||
}
|
|
||||||
u2f.sign(appId, firstr.challenge, registeredKeys, function (data) {
|
|
||||||
if (data.errorCode && data.errorCode != 5) {
|
|
||||||
$("#u2f-error").removeClass("sr-only");
|
|
||||||
} else {
|
|
||||||
$('#u2f-response, #id_password').val(JSON.stringify(data));
|
|
||||||
$('#u2f-form').submit();
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
/*global $,u2f */
|
||||||
|
|
||||||
|
function b64enc(buf) {
|
||||||
|
return base64js.fromByteArray(buf)
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_")
|
||||||
|
.replace(/=/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function b64RawEnc(buf) {
|
||||||
|
return base64js.fromByteArray(buf)
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
function hexEncode(buf) {
|
||||||
|
return Array.from(buf)
|
||||||
|
.map(function(x) {
|
||||||
|
return ("0" + x.toString(16)).substr(-2);
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetch_json(url, options) {
|
||||||
|
const response = await fetch(url, options);
|
||||||
|
const body = await response.json();
|
||||||
|
if (body.fail)
|
||||||
|
throw body.fail;
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms items in the credentialCreateOptions generated on the server
|
||||||
|
* into byte arrays expected by the navigator.credentials.create() call
|
||||||
|
* @param {Object} credentialCreateOptionsFromServer
|
||||||
|
*/
|
||||||
|
const transformCredentialCreateOptions = function (credentialCreateOptionsFromServer) {
|
||||||
|
let {challenge, user} = credentialCreateOptionsFromServer;
|
||||||
|
user.id = Uint8Array.from(
|
||||||
|
atob(credentialCreateOptionsFromServer.user.id), c => c.charCodeAt(0));
|
||||||
|
|
||||||
|
challenge = Uint8Array.from(
|
||||||
|
atob(credentialCreateOptionsFromServer.challenge), c => c.charCodeAt(0));
|
||||||
|
|
||||||
|
const transformedCredentialCreateOptions = Object.assign(
|
||||||
|
{}, credentialCreateOptionsFromServer,
|
||||||
|
{challenge, user});
|
||||||
|
|
||||||
|
return transformedCredentialCreateOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the binary data in the credential into base64 strings
|
||||||
|
* for posting to the server.
|
||||||
|
* @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 registrationClientExtensions = newAssertion.getClientExtensionResults();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: newAssertion.id,
|
||||||
|
rawId: b64enc(rawId),
|
||||||
|
type: newAssertion.type,
|
||||||
|
attObj: b64enc(attObj),
|
||||||
|
clientData: b64enc(clientDataJSON),
|
||||||
|
registrationClientExtensions: JSON.stringify(registrationClientExtensions)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const transformCredentialRequestOptions = (credentialRequestOptionsFromServer) => {
|
||||||
|
let {challenge, allowCredentials} = credentialRequestOptionsFromServer;
|
||||||
|
|
||||||
|
challenge = Uint8Array.from(
|
||||||
|
atob(challenge), c => c.charCodeAt(0));
|
||||||
|
|
||||||
|
allowCredentials = allowCredentials.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 transformedCredentialRequestOptions = Object.assign(
|
||||||
|
{},
|
||||||
|
credentialRequestOptionsFromServer,
|
||||||
|
{challenge, allowCredentials});
|
||||||
|
|
||||||
|
return transformedCredentialRequestOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the binary data in the assertion into strings for posting to the server.
|
||||||
|
* @param {PublicKeyCredential} newAssertion
|
||||||
|
*/
|
||||||
|
const transformAssertionForServer = (newAssertion) => {
|
||||||
|
const authData = new Uint8Array(newAssertion.response.authenticatorData);
|
||||||
|
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
|
||||||
|
const rawId = new Uint8Array(newAssertion.rawId);
|
||||||
|
const sig = new Uint8Array(newAssertion.response.signature);
|
||||||
|
const assertionClientExtensions = newAssertion.getClientExtensionResults();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: newAssertion.id,
|
||||||
|
rawId: b64enc(rawId),
|
||||||
|
type: newAssertion.type,
|
||||||
|
authData: b64RawEnc(authData),
|
||||||
|
clientData: b64RawEnc(clientDataJSON),
|
||||||
|
signature: hexEncode(sig),
|
||||||
|
assertionClientExtensions: JSON.stringify(assertionClientExtensions)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const startRegister = async (e) => {
|
||||||
|
const publicKeyCredentialCreateOptions = transformCredentialCreateOptions(JSON.parse($("#webauthn-enroll").text()));
|
||||||
|
|
||||||
|
// request the authenticator(s) to create a new credential keypair.
|
||||||
|
let credential;
|
||||||
|
try {
|
||||||
|
credential = await navigator.credentials.create({
|
||||||
|
publicKey: publicKeyCredentialCreateOptions
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
$("#webauthn-error").removeClass("sr-only");
|
||||||
|
return console.error("Error creating credential:", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// we now have a new credential! We now need to encode the byte arrays
|
||||||
|
// in the credential into strings, for posting to our server.
|
||||||
|
const newAssertionForServer = transformNewAssertionForServer(credential);
|
||||||
|
|
||||||
|
$("#webauthn-response").val(JSON.stringify(newAssertionForServer));
|
||||||
|
$("#webauthn-form").submit();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const startLogin = async (e) => {
|
||||||
|
const transformedCredentialRequestOptions = transformCredentialRequestOptions(JSON.parse($("#webauthn-login").text()));
|
||||||
|
console.log(transformedCredentialRequestOptions);
|
||||||
|
|
||||||
|
// request the authenticator to create an assertion signature using the
|
||||||
|
// credential private key
|
||||||
|
let assertion;
|
||||||
|
try {
|
||||||
|
assertion = await navigator.credentials.get({
|
||||||
|
publicKey: transformedCredentialRequestOptions,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
$("#webauthn-error").removeClass("sr-only");
|
||||||
|
return console.error("Error when creating credential:", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// we now have an authentication assertion! encode the byte arrays contained
|
||||||
|
// in the assertion data as strings for posting to the server
|
||||||
|
const transformedAssertionForServer = transformAssertionForServer(assertion);
|
||||||
|
|
||||||
|
// post the assertion to the server for verification.
|
||||||
|
$("#webauthn-response, #id_password").val(JSON.stringify(transformedAssertionForServer));
|
||||||
|
$("#webauthn-form").submit();
|
||||||
|
};
|
||||||
|
|
||||||
|
$(function () {
|
||||||
|
$("#webauthn-progress").hide();
|
||||||
|
if ($("#webauthn-enroll").length) {
|
||||||
|
$("#webauthn-progress").show();
|
||||||
|
startRegister();
|
||||||
|
} else if ($("#webauthn-login").length) {
|
||||||
|
$("#webauthn-progress").show();
|
||||||
|
startLogin();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -17,6 +17,7 @@ django-libsass
|
|||||||
libsass
|
libsass
|
||||||
django-otp==0.5.*
|
django-otp==0.5.*
|
||||||
python-u2flib-server==4.*
|
python-u2flib-server==4.*
|
||||||
|
webauthn==0.4.*
|
||||||
django-formtools==2.1
|
django-formtools==2.1
|
||||||
celery==4.3.*
|
celery==4.3.*
|
||||||
kombu==4.5.*
|
kombu==4.5.*
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ setup(
|
|||||||
'django-libsass',
|
'django-libsass',
|
||||||
'libsass',
|
'libsass',
|
||||||
'django-otp==0.5.*',
|
'django-otp==0.5.*',
|
||||||
|
'webauthn==0.4.*',
|
||||||
'python-u2flib-server==4.*',
|
'python-u2flib-server==4.*',
|
||||||
'django-formtools==2.1',
|
'django-formtools==2.1',
|
||||||
'celery==4.3.*',
|
'celery==4.3.*',
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from django.test import TestCase, override_settings
|
|||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django_otp.oath import TOTP
|
from django_otp.oath import TOTP
|
||||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||||
from u2flib_server.jsapi import JSONDict
|
|
||||||
|
|
||||||
from pretix.base.models import U2FDevice, User
|
from pretix.base.models import U2FDevice, User
|
||||||
|
|
||||||
@@ -268,10 +267,14 @@ class Login2FAFormTest(TestCase):
|
|||||||
raise Exception("Failed")
|
raise Exception("Failed")
|
||||||
|
|
||||||
m = self.monkeypatch
|
m = self.monkeypatch
|
||||||
m.setattr("u2flib_server.u2f.verify_authenticate", fail)
|
m.setattr("webauthn.WebAuthnAssertionResponse.verify", fail)
|
||||||
m.setattr("u2flib_server.u2f.start_authenticate",
|
d = U2FDevice.objects.create(
|
||||||
lambda *args, **kwargs: JSONDict({'authenticateRequests': []}))
|
user=self.user, name='test',
|
||||||
d = U2FDevice.objects.create(user=self.user, name='test', json_data="{}")
|
json_data='{"appId": "https://local.pretix.eu", "keyHandle": '
|
||||||
|
'"j9Rkpon1J5U3eDQMM8YqAvwEapt-m87V8qdCaImiAqmvTJ'
|
||||||
|
'-sBvnACIKKM6J_RVXF4jPtY0LGyjbHi14sxsoC5g", "publ'
|
||||||
|
'icKey": "BP5KRLUGvcHbqkCc7eJNXZ9caVXLSk4wjsq'
|
||||||
|
'L-pLEQcNqVp2E4OeDUIxI0ZLOXry9JSrLn1aAGcGowXiIyB7ynj0"}')
|
||||||
|
|
||||||
response = self.client.get('/control/login/2fa')
|
response = self.client.get('/control/login/2fa')
|
||||||
assert 'token' in response.content.decode()
|
assert 'token' in response.content.decode()
|
||||||
@@ -285,10 +288,15 @@ class Login2FAFormTest(TestCase):
|
|||||||
|
|
||||||
def test_u2f_valid(self):
|
def test_u2f_valid(self):
|
||||||
m = self.monkeypatch
|
m = self.monkeypatch
|
||||||
m.setattr("u2flib_server.u2f.verify_authenticate", lambda *args, **kwargs: True)
|
m.setattr("webauthn.WebAuthnAssertionResponse.verify", lambda *args, **kwargs: 1)
|
||||||
m.setattr("u2flib_server.u2f.start_authenticate",
|
|
||||||
lambda *args, **kwargs: JSONDict({'authenticateRequests': []}))
|
d = U2FDevice.objects.create(
|
||||||
d = U2FDevice.objects.create(user=self.user, name='test', json_data="{}")
|
user=self.user, name='test',
|
||||||
|
json_data='{"appId": "https://local.pretix.eu", "keyHandle": '
|
||||||
|
'"j9Rkpon1J5U3eDQMM8YqAvwEapt-m87V8qdCaImiAqmvTJ'
|
||||||
|
'-sBvnACIKKM6J_RVXF4jPtY0LGyjbHi14sxsoC5g", "publ'
|
||||||
|
'icKey": "BP5KRLUGvcHbqkCc7eJNXZ9caVXLSk4wjsq'
|
||||||
|
'L-pLEQcNqVp2E4OeDUIxI0ZLOXry9JSrLn1aAGcGowXiIyB7ynj0"}')
|
||||||
|
|
||||||
response = self.client.get('/control/login/2fa')
|
response = self.client.get('/control/login/2fa')
|
||||||
assert 'token' in response.content.decode()
|
assert 'token' in response.content.decode()
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ from django_otp.oath import TOTP
|
|||||||
from django_otp.plugins.otp_static.models import StaticDevice
|
from django_otp.plugins.otp_static.models import StaticDevice
|
||||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||||
from tests.base import SoupTest, extract_form_fields
|
from tests.base import SoupTest, extract_form_fields
|
||||||
from u2flib_server.jsapi import JSONDict
|
from webauthn import WebAuthnCredential
|
||||||
|
|
||||||
from pretix.base.models import Event, Organizer, U2FDevice, User
|
from pretix.base.models import (
|
||||||
|
Event, Organizer, U2FDevice, User, WebAuthnDevice,
|
||||||
|
)
|
||||||
from pretix.testutils.mock import mocker_context
|
from pretix.testutils.mock import mocker_context
|
||||||
|
|
||||||
|
|
||||||
@@ -181,27 +183,33 @@ class UserSettings2FATest(SoupTest):
|
|||||||
self.client.post('/control/settings/2fa/u2f/{}/delete'.format(d.pk))
|
self.client.post('/control/settings/2fa/u2f/{}/delete'.format(d.pk))
|
||||||
assert not U2FDevice.objects.exists()
|
assert not U2FDevice.objects.exists()
|
||||||
|
|
||||||
|
def test_delete_webauthn(self):
|
||||||
|
d = WebAuthnDevice.objects.create(user=self.user, name='Test')
|
||||||
|
self.client.get('/control/settings/2fa/webauthn/{}/delete'.format(d.pk))
|
||||||
|
self.client.post('/control/settings/2fa/webauthn/{}/delete'.format(d.pk))
|
||||||
|
assert not WebAuthnDevice.objects.exists()
|
||||||
|
|
||||||
def test_delete_totp(self):
|
def test_delete_totp(self):
|
||||||
d = TOTPDevice.objects.create(user=self.user, name='Test')
|
d = TOTPDevice.objects.create(user=self.user, name='Test')
|
||||||
self.client.get('/control/settings/2fa/totp/{}/delete'.format(d.pk))
|
self.client.get('/control/settings/2fa/totp/{}/delete'.format(d.pk))
|
||||||
self.client.post('/control/settings/2fa/totp/{}/delete'.format(d.pk))
|
self.client.post('/control/settings/2fa/totp/{}/delete'.format(d.pk))
|
||||||
assert not TOTPDevice.objects.exists()
|
assert not TOTPDevice.objects.exists()
|
||||||
|
|
||||||
def test_create_u2f_require_https(self):
|
def test_create_webauthn_require_https(self):
|
||||||
r = self.client.post('/control/settings/2fa/add', {
|
r = self.client.post('/control/settings/2fa/add', {
|
||||||
'devicetype': 'u2f',
|
'devicetype': 'webauthn',
|
||||||
'name': 'Foo'
|
'name': 'Foo'
|
||||||
})
|
})
|
||||||
assert 'alert-danger' in r.content.decode()
|
assert 'alert-danger' in r.content.decode()
|
||||||
|
|
||||||
def test_create_u2f(self):
|
def test_create_webauthn(self):
|
||||||
with mocker_context() as mocker:
|
with mocker_context() as mocker:
|
||||||
mocker.patch('django.http.request.HttpRequest.is_secure')
|
mocker.patch('django.http.request.HttpRequest.is_secure')
|
||||||
self.client.post('/control/settings/2fa/add', {
|
self.client.post('/control/settings/2fa/add', {
|
||||||
'devicetype': 'u2f',
|
'devicetype': 'webauthn',
|
||||||
'name': 'Foo'
|
'name': 'Foo'
|
||||||
})
|
})
|
||||||
d = U2FDevice.objects.first()
|
d = WebAuthnDevice.objects.first()
|
||||||
assert d.name == 'Foo'
|
assert d.name == 'Foo'
|
||||||
assert not d.confirmed
|
assert not d.confirmed
|
||||||
|
|
||||||
@@ -246,35 +254,38 @@ class UserSettings2FATest(SoupTest):
|
|||||||
d.refresh_from_db()
|
d.refresh_from_db()
|
||||||
assert not d.confirmed
|
assert not d.confirmed
|
||||||
|
|
||||||
def test_confirm_u2f_failed(self):
|
def test_confirm_webauthn_failed(self):
|
||||||
with mocker_context() as mocker:
|
with mocker_context() as mocker:
|
||||||
mocker.patch('django.http.request.HttpRequest.is_secure')
|
mocker.patch('django.http.request.HttpRequest.is_secure')
|
||||||
self.client.post('/control/settings/2fa/add', {
|
self.client.post('/control/settings/2fa/add', {
|
||||||
'devicetype': 'u2f',
|
'devicetype': 'webauthn',
|
||||||
'name': 'Foo'
|
'name': 'Foo'
|
||||||
}, follow=True)
|
}, follow=True)
|
||||||
d = U2FDevice.objects.first()
|
d = WebAuthnDevice.objects.first()
|
||||||
r = self.client.post('/control/settings/2fa/u2f/{}/confirm'.format(d.pk), {
|
r = self.client.post('/control/settings/2fa/webauthn/{}/confirm'.format(d.pk), {
|
||||||
'token': 'FOO'
|
'token': 'FOO'
|
||||||
}, follow=True)
|
}, follow=True)
|
||||||
assert 'alert-danger' in r.content.decode()
|
assert 'alert-danger' in r.content.decode()
|
||||||
d.refresh_from_db()
|
d.refresh_from_db()
|
||||||
assert not d.confirmed
|
assert not d.confirmed
|
||||||
|
|
||||||
def test_confirm_u2f_success(self):
|
def test_confirm_webauthn_success(self):
|
||||||
with mocker_context() as mocker:
|
with mocker_context() as mocker:
|
||||||
mocker.patch('django.http.request.HttpRequest.is_secure')
|
mocker.patch('django.http.request.HttpRequest.is_secure')
|
||||||
self.client.post('/control/settings/2fa/add', {
|
self.client.post('/control/settings/2fa/add', {
|
||||||
'devicetype': 'u2f',
|
'devicetype': 'webauthn',
|
||||||
'name': 'Foo'
|
'name': 'Foo'
|
||||||
}, follow=True)
|
}, follow=True)
|
||||||
|
|
||||||
m = self.monkeypatch
|
m = self.monkeypatch
|
||||||
m.setattr("u2flib_server.u2f.complete_register", lambda *args, **kwargs: (JSONDict({}), None))
|
m.setattr("webauthn.WebAuthnRegistrationResponse.verify",
|
||||||
|
lambda *args, **kwargs: WebAuthnCredential(
|
||||||
|
'', '', b'asd', b'foo', 1
|
||||||
|
))
|
||||||
|
|
||||||
d = U2FDevice.objects.first()
|
d = WebAuthnDevice.objects.first()
|
||||||
r = self.client.post('/control/settings/2fa/u2f/{}/confirm'.format(d.pk), {
|
r = self.client.post('/control/settings/2fa/webauthn/{}/confirm'.format(d.pk), {
|
||||||
'token': 'FOO',
|
'token': '{}',
|
||||||
'activate': 'on'
|
'activate': 'on'
|
||||||
}, follow=True)
|
}, follow=True)
|
||||||
d.refresh_from_db()
|
d.refresh_from_db()
|
||||||
@@ -282,7 +293,6 @@ class UserSettings2FATest(SoupTest):
|
|||||||
assert 'alert-success' in r.content.decode()
|
assert 'alert-success' in r.content.decode()
|
||||||
self.user.refresh_from_db()
|
self.user.refresh_from_db()
|
||||||
assert self.user.require_2fa
|
assert self.user.require_2fa
|
||||||
|
|
||||||
m.undo()
|
m.undo()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user