forked from CGM_Public/pretix_original
2FA: Login via U2F
This commit is contained in:
@@ -2,8 +2,9 @@
|
||||
{% load bootstrap3 %}
|
||||
{% load i18n %}
|
||||
{% load staticfiles %}
|
||||
{% load compress %}
|
||||
{% block content %}
|
||||
<form class="form-signin" action="" method="post">
|
||||
<form class="form-signin" action="" method="post" id="u2f-form">
|
||||
{% csrf_token %}
|
||||
<h3>{% trans "Welcome back!" %}</h3>
|
||||
<p>
|
||||
@@ -11,12 +12,31 @@
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<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 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">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{% trans "Continue" %}
|
||||
</button>
|
||||
</div>
|
||||
</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 %}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
import time
|
||||
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.views.generic import TemplateView
|
||||
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 (
|
||||
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.helpers.urls import build_absolute_uri
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def login(request):
|
||||
"""
|
||||
@@ -204,9 +210,17 @@ class Recover(TemplateView):
|
||||
return context
|
||||
|
||||
|
||||
def get_u2f_appid(request):
|
||||
return '%s://%s' % ('https' if request.is_secure() else 'http', request.get_host())
|
||||
|
||||
|
||||
class Login2FAView(TemplateView):
|
||||
template_name = 'pretixcontrol/auth/login_2fa.html'
|
||||
|
||||
@property
|
||||
def app_id(self):
|
||||
return get_u2f_appid(self.request)
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
fail = False
|
||||
if 'pretix_auth_2fa_user' not in request.session:
|
||||
@@ -226,7 +240,21 @@ class Login2FAView(TemplateView):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
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)
|
||||
del request.session['pretix_auth_2fa_user']
|
||||
del request.session['pretix_auth_2fa_time']
|
||||
@@ -236,3 +264,21 @@ class Login2FAView(TemplateView):
|
||||
else:
|
||||
messages.error(request, _('Invalid code, please try again.'))
|
||||
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_otp.plugins.otp_static.models import StaticDevice
|
||||
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.u2f import complete_register, start_register
|
||||
|
||||
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)
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -128,7 +127,7 @@ class User2FADeviceConfirmU2FView(TemplateView):
|
||||
|
||||
@property
|
||||
def app_id(self):
|
||||
return get_origin(self.request)
|
||||
return get_u2f_appid(self.request)
|
||||
|
||||
@cached_property
|
||||
def device(self):
|
||||
@@ -139,7 +138,7 @@ class User2FADeviceConfirmU2FView(TemplateView):
|
||||
ctx['device'] = self.device
|
||||
|
||||
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)
|
||||
self.request.session['_u2f_enroll'] = enroll.json
|
||||
ctx['jsondata'] = enroll.json
|
||||
@@ -156,7 +155,7 @@ class User2FADeviceConfirmU2FView(TemplateView):
|
||||
self.device.save()
|
||||
messages.success(request, _('The device has been verified and can now be used.'))
|
||||
return redirect(reverse('control:user.settings.2fa'))
|
||||
except ValueError:
|
||||
except Exception:
|
||||
messages.error(request, _('The registration could not be completed. Please try again.'))
|
||||
logger.exception('U2F registration failed')
|
||||
return redirect(reverse('control:user.settings.2fa.confirm.2fa', kwargs={
|
||||
|
||||
Reference in New Issue
Block a user