mirror of
https://github.com/pretix/pretix.git
synced 2026-08-26 13:14:40 +00:00
2FA: Login via U2F
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.2 on 2016-10-08 15:38
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0039_user_require_2fa'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='U2FDevice',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(help_text='The human-readable name of this device.', max_length=64)),
|
||||||
|
('confirmed', models.BooleanField(default=True, help_text='Is this device ready for use?')),
|
||||||
|
('json_data', models.TextField()),
|
||||||
|
('user', models.ForeignKey(help_text='The user that this device belongs to.', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from .auth import User
|
from .auth import U2FDevice, User
|
||||||
from .base import CachedFile, LoggedModel, cachedfile_name
|
from .base import CachedFile, LoggedModel, cachedfile_name
|
||||||
from .event import Event, EventLock, EventPermission, EventSetting
|
from .event import Event, EventLock, EventPermission, EventSetting
|
||||||
from .invoices import Invoice, InvoiceLine, invoice_filename
|
from .invoices import Invoice, InvoiceLine, invoice_filename
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from django.contrib.auth.models import (
|
|||||||
)
|
)
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django_otp.models import Device
|
||||||
|
|
||||||
from .base import LoggingMixin
|
from .base import LoggingMixin
|
||||||
|
|
||||||
@@ -126,3 +127,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return self.email
|
return self.email
|
||||||
|
|
||||||
|
|
||||||
|
class U2FDevice(Device):
|
||||||
|
json_data = models.TextField()
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load staticfiles %}
|
{% load staticfiles %}
|
||||||
|
{% load compress %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form class="form-signin" action="" method="post">
|
<form class="form-signin" action="" method="post" id="u2f-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<h3>{% trans "Welcome back!" %}</h3>
|
<h3>{% trans "Welcome back!" %}</h3>
|
||||||
<p>
|
<p>
|
||||||
@@ -11,12 +12,31 @@
|
|||||||
</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="number" required="required" autofocus="autofocus">
|
type="text" required="required" autofocus="autofocus" id="u2f-response">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="sr-only alert alert-danger" id="u2f-error">
|
||||||
|
{% trans "U2F failed. Check that the correct authentication device is correctly plugged in." %}
|
||||||
|
</div>
|
||||||
|
{% if jsondata %}
|
||||||
|
<p><small>
|
||||||
|
{% trans "Alternatively, connect your U2F device. If it has a button, touch it now. You might have to unplug the device and plug it back in again." %}
|
||||||
|
</small></p>
|
||||||
|
{% endif %}
|
||||||
<div class="form-group buttons">
|
<div class="form-group buttons">
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
{% trans "Continue" %}
|
{% trans "Continue" %}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
{% if jsondata %}
|
||||||
|
<script type="text/json" id="u2f-login">
|
||||||
|
{{ jsondata|safe }}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
{% compress js %}
|
||||||
|
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
|
||||||
|
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/u2f-api.js" %}"></script>
|
||||||
|
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/u2f.js" %}"></script>
|
||||||
|
{% endcompress %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
import time
|
import time
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
@@ -15,14 +16,19 @@ 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.jsapi import DeviceRegistration
|
||||||
|
from u2flib_server.u2f import start_authenticate, verify_authenticate
|
||||||
|
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 User
|
from pretix.base.models import U2FDevice, User
|
||||||
from pretix.base.services.mail import SendMailException, mail
|
from pretix.base.services.mail import SendMailException, mail
|
||||||
from pretix.helpers.urls import build_absolute_uri
|
from pretix.helpers.urls import build_absolute_uri
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def login(request):
|
def login(request):
|
||||||
"""
|
"""
|
||||||
@@ -204,9 +210,17 @@ class Recover(TemplateView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
def get_u2f_appid(request):
|
||||||
|
return '%s://%s' % ('https' if request.is_secure() else 'http', request.get_host())
|
||||||
|
|
||||||
|
|
||||||
class Login2FAView(TemplateView):
|
class Login2FAView(TemplateView):
|
||||||
template_name = 'pretixcontrol/auth/login_2fa.html'
|
template_name = 'pretixcontrol/auth/login_2fa.html'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def app_id(self):
|
||||||
|
return get_u2f_appid(self.request)
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
fail = False
|
fail = False
|
||||||
if 'pretix_auth_2fa_user' not in request.session:
|
if 'pretix_auth_2fa_user' not in request.session:
|
||||||
@@ -226,7 +240,21 @@ class Login2FAView(TemplateView):
|
|||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
token = request.POST.get('token', '').strip().replace(' ', '')
|
token = request.POST.get('token', '').strip().replace(' ', '')
|
||||||
if match_token(self.user, token):
|
|
||||||
|
valid = False
|
||||||
|
if '_u2f_challenge' in self.request.session and token.startswith('{'):
|
||||||
|
devices = [DeviceRegistration.wrap(device.json_data)
|
||||||
|
for device in U2FDevice.objects.filter(confirmed=True, user=self.user)]
|
||||||
|
challenge = self.request.session.pop('_u2f_challenge')
|
||||||
|
try:
|
||||||
|
verify_authenticate(devices, challenge, token, [self.app_id])
|
||||||
|
valid = True
|
||||||
|
except Exception:
|
||||||
|
logger.exception('U2F login failed')
|
||||||
|
else:
|
||||||
|
valid = match_token(self.user, token)
|
||||||
|
|
||||||
|
if valid:
|
||||||
auth_login(request, self.user)
|
auth_login(request, self.user)
|
||||||
del request.session['pretix_auth_2fa_user']
|
del request.session['pretix_auth_2fa_user']
|
||||||
del request.session['pretix_auth_2fa_time']
|
del request.session['pretix_auth_2fa_time']
|
||||||
@@ -236,3 +264,21 @@ class Login2FAView(TemplateView):
|
|||||||
else:
|
else:
|
||||||
messages.error(request, _('Invalid code, please try again.'))
|
messages.error(request, _('Invalid code, please try again.'))
|
||||||
return redirect('control:auth.login.2fa')
|
return redirect('control:auth.login.2fa')
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data()
|
||||||
|
|
||||||
|
devices = [DeviceRegistration.wrap(device.json_data)
|
||||||
|
for device in U2FDevice.objects.filter(confirmed=True, user=self.user)]
|
||||||
|
if devices:
|
||||||
|
challenge = start_authenticate(devices, challenge=rand_bytes(32))
|
||||||
|
self.request.session['_u2f_challenge'] = challenge.json
|
||||||
|
ctx['jsondata'] = challenge.json
|
||||||
|
else:
|
||||||
|
del self.request.session['_u2f_challenge']
|
||||||
|
ctx['jsondata'] = None
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
|||||||
@@ -13,13 +13,12 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
from django.views.generic import FormView, TemplateView, UpdateView
|
from django.views.generic import FormView, 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 django_otp_u2f_stub.models import U2FDevice
|
|
||||||
from django_otp_u2f_stub.utils import get_origin
|
|
||||||
from u2flib_server.jsapi import DeviceRegistration
|
from u2flib_server.jsapi import DeviceRegistration
|
||||||
from u2flib_server.u2f import complete_register, start_register
|
from u2flib_server.u2f import complete_register, start_register
|
||||||
|
|
||||||
from pretix.base.forms.user import User2FADeviceAddForm, UserSettingsForm
|
from pretix.base.forms.user import User2FADeviceAddForm, UserSettingsForm
|
||||||
from pretix.base.models import User
|
from pretix.base.models import U2FDevice, User
|
||||||
|
from pretix.control.views.auth import get_u2f_appid
|
||||||
|
|
||||||
REAL_DEVICE_TYPES = (TOTPDevice, U2FDevice)
|
REAL_DEVICE_TYPES = (TOTPDevice, U2FDevice)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -128,7 +127,7 @@ class User2FADeviceConfirmU2FView(TemplateView):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def app_id(self):
|
def app_id(self):
|
||||||
return get_origin(self.request)
|
return get_u2f_appid(self.request)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def device(self):
|
def device(self):
|
||||||
@@ -139,7 +138,7 @@ class User2FADeviceConfirmU2FView(TemplateView):
|
|||||||
ctx['device'] = self.device
|
ctx['device'] = self.device
|
||||||
|
|
||||||
devices = [DeviceRegistration.wrap(device.json_data)
|
devices = [DeviceRegistration.wrap(device.json_data)
|
||||||
for device in U2FDevice.objects.filter(confirmed=True)]
|
for device in U2FDevice.objects.filter(confirmed=True, user=self.request.user)]
|
||||||
enroll = start_register(self.app_id, devices)
|
enroll = start_register(self.app_id, devices)
|
||||||
self.request.session['_u2f_enroll'] = enroll.json
|
self.request.session['_u2f_enroll'] = enroll.json
|
||||||
ctx['jsondata'] = enroll.json
|
ctx['jsondata'] = enroll.json
|
||||||
@@ -156,7 +155,7 @@ class User2FADeviceConfirmU2FView(TemplateView):
|
|||||||
self.device.save()
|
self.device.save()
|
||||||
messages.success(request, _('The device has been verified and can now be used.'))
|
messages.success(request, _('The device has been verified and can now be used.'))
|
||||||
return redirect(reverse('control:user.settings.2fa'))
|
return redirect(reverse('control:user.settings.2fa'))
|
||||||
except ValueError:
|
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('U2F registration failed')
|
||||||
return redirect(reverse('control:user.settings.2fa.confirm.2fa', kwargs={
|
return redirect(reverse('control:user.settings.2fa.confirm.2fa', kwargs={
|
||||||
|
|||||||
@@ -179,7 +179,6 @@ INSTALLED_APPS = [
|
|||||||
'django_otp',
|
'django_otp',
|
||||||
'django_otp.plugins.otp_totp',
|
'django_otp.plugins.otp_totp',
|
||||||
'django_otp.plugins.otp_static',
|
'django_otp.plugins.otp_static',
|
||||||
'django_otp_u2f_stub'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ easy-thumbnails>=2.2,<3
|
|||||||
django-libsass
|
django-libsass
|
||||||
libsass
|
libsass
|
||||||
django-otp==0.3.*
|
django-otp==0.3.*
|
||||||
django-otp-u2f-stub
|
|
||||||
python-u2flib-server==4.*
|
python-u2flib-server==4.*
|
||||||
# celery>=3.1,<3.2
|
# celery>=3.1,<3.2
|
||||||
# until the following issue is fixed, we need our own celery version
|
# until the following issue is fixed, we need our own celery version
|
||||||
|
|||||||
@@ -1,22 +1,40 @@
|
|||||||
/*global $,u2f */
|
/*global $,u2f */
|
||||||
$(function () {
|
$(function () {
|
||||||
|
$("#u2f-progress").hide();
|
||||||
if ($("#u2f-enroll").length) {
|
if ($("#u2f-enroll").length) {
|
||||||
var request = JSON.parse($.trim($("#u2f-enroll").html()));
|
var request = JSON.parse($.trim($("#u2f-enroll").html()));
|
||||||
|
$("#u2f-progress").show();
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
var appId = request.registerRequests[0].appId;
|
var appId = request.registerRequests[0].appId;
|
||||||
$('#promptModal').modal('show');
|
|
||||||
console.log(appId, request.registerRequests);
|
|
||||||
u2f.register(appId, request.registerRequests, [], function (data) {
|
u2f.register(appId, request.registerRequests, [], function (data) {
|
||||||
console.log("callback", data);
|
|
||||||
if (data.errorCode) {
|
if (data.errorCode) {
|
||||||
$("#u2f-error").removeClass("sr-only");
|
$("#u2f-error").removeClass("sr-only");
|
||||||
$("#u2f-progress").remove();
|
$("#u2f-progress").remove();
|
||||||
} else {
|
} else {
|
||||||
console.log("Register callback", data);
|
|
||||||
$('#u2f-response').val(JSON.stringify(data));
|
$('#u2f-response').val(JSON.stringify(data));
|
||||||
$('#u2f-form').submit();
|
$('#u2f-form').submit();
|
||||||
}
|
}
|
||||||
});
|
}, 300);
|
||||||
}, 500);
|
}, 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').val(JSON.stringify(data));
|
||||||
|
$('#u2f-form').submit();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user