From 6ee990d13535e3bb532cd55cba013a628a534af5 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Wed, 16 Jun 2021 18:23:23 +0200 Subject: [PATCH 1/9] add SITE_TITLE setting --- castellum/settings/default.py | 1 + castellum/templates/base.html | 4 ++-- castellum/utils/context_processors.py | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/castellum/settings/default.py b/castellum/settings/default.py index 42427d9cf..bf627bc1e 100644 --- a/castellum/settings/default.py +++ b/castellum/settings/default.py @@ -247,6 +247,7 @@ BOOTSTRAP4 = { AXES_LOCKOUT_TEMPLATE = 'axes-lockout.html' +SITE_TITLE = 'Castellum' # Secondary language in emails for readers who do not understand the # primary language diff --git a/castellum/templates/base.html b/castellum/templates/base.html index 3048aac9d..bc5cfc929 100644 --- a/castellum/templates/base.html +++ b/castellum/templates/base.html @@ -8,7 +8,7 @@ - {% block title %}Castellum{% endblock %} + {% block title %}{{ SITE_TITLE }}{% endblock %} @@ -29,7 +29,7 @@
{% endif %}
+ + {% csrf_token %} + + + + {% endblock %} -- GitLab From 3e79ab77cfeb63835fc5b2686405ecd8679393e0 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Wed, 16 Jun 2021 17:44:06 +0200 Subject: [PATCH 5/9] integrate into login --- castellum/castellum_auth/views.py | 34 +++++++++++++++++++++++++++++-- castellum/settings/default.py | 2 ++ castellum/static/js/fido2.js | 7 +++++++ castellum/templates/login.html | 15 +++++++++++++- castellum/urls.py | 2 +- 5 files changed, 56 insertions(+), 4 deletions(-) diff --git a/castellum/castellum_auth/views.py b/castellum/castellum_auth/views.py index a8736cd28..0a4666bd2 100644 --- a/castellum/castellum_auth/views.py +++ b/castellum/castellum_auth/views.py @@ -20,6 +20,8 @@ # . from django.conf import settings +from django.contrib.auth import login +from django.contrib.auth.views import LoginView as DjangoLoginView from django.http import Http404 from django.http import HttpResponse from django.shortcuts import redirect @@ -34,9 +36,12 @@ from fido2.server import Fido2Server from fido2.utils import websafe_decode from fido2.utils import websafe_encode from fido2.webauthn import PublicKeyCredentialRpEntity +from stronghold.decorators import public from castellum.utils.views import get_next_url +from .models import User + LANGUAGE_QUERY_PARAMETER = 'language' fido2 = Fido2Server(PublicKeyCredentialRpEntity(settings.DOMAIN, settings.SITE_TITLE)) @@ -56,6 +61,22 @@ def set_language(request): return redirect(get_next_url(request)) +class LoginView(DjangoLoginView): + def form_valid(self, form): + if settings.CASTELLUM_REQUIRE_FIDO2: + user = form.get_user() + self.request.session['fido2_user'] = { + 'pk': user.pk, + 'backend': user.backend, + } + return self.render_to_response(self.get_context_data( + fido2_required=True, + success_url=self.get_success_url(), + )) + else: + return super().form_valid(form) + + def get_credentials(user): raw = user.fido2_credential_data if not raw: @@ -91,22 +112,31 @@ def register_complete(request): return HttpResponse() +@public def authenticate_begin(request): - auth_data, state = fido2.authenticate_begin(get_credentials(request.user)) + user = User.objects.get(pk=request.session['fido2_user']['pk']) + + auth_data, state = fido2.authenticate_begin(get_credentials(user)) request.session['fido2_state'] = state return HttpResponse(cbor.encode(auth_data), content_type='application/cbor') +@public def authenticate_complete(request): + user_data = request.session.pop('fido2_user') + user = User.objects.get(pk=user_data['pk']) + user.backend = user_data['backend'] + data = cbor.decode(request.body) fido2.authenticate_complete( request.session.pop('fido2_state'), - get_credentials(request.user), + get_credentials(user), data['credentialId'], ClientData(data['clientData']), AuthenticatorData(data['authenticatorData']), data['signature'], ) + login(request, user) return HttpResponse() diff --git a/castellum/settings/default.py b/castellum/settings/default.py index c1ae529af..d38e29dbe 100644 --- a/castellum/settings/default.py +++ b/castellum/settings/default.py @@ -461,3 +461,5 @@ CASTELLUM_ATTRIBUTE_EXPORTER = 'castellum.recruitment.attribute_exporters.JSONEx SCHEDULER_URL = '' SCHEDULER_TOKEN = '' + +CASTELLUM_REQUIRE_FIDO2 = False diff --git a/castellum/static/js/fido2.js b/castellum/static/js/fido2.js index aadfd5e98..19157b7af 100644 --- a/castellum/static/js/fido2.js +++ b/castellum/static/js/fido2.js @@ -55,4 +55,11 @@ $$.on(document, 'click', '[data-js="fido2-register"]', register); $$.on(document, 'click', '[data-js="fido2-authenticate"]', authenticate); + + var fido2Auto = document.querySelector('[data-js="fido2-auto"]'); + if (fido2Auto) { + authenticate().then(() => { + window.location = fido2Auto.dataset.successUrl; + }); + } })(); diff --git a/castellum/templates/login.html b/castellum/templates/login.html index 4c9470f67..4ae122b9b 100644 --- a/castellum/templates/login.html +++ b/castellum/templates/login.html @@ -1,9 +1,15 @@ {% extends "base.html" %} -{% load i18n bootstrap4 %} +{% load static i18n bootstrap4 %} {% block title %}{% translate "Log in" %} · {{ block.super }}{% endblock %} {% block content %} + {% if fido2_required %} + + {% endif %} +
{% include 'utils/form_errors.html' with form=form %} {% csrf_token %} @@ -17,3 +23,10 @@
{% endblock %} + +{% block extra_scripts %} + {% if fido2_required %} + + + {% endif %} +{% endblock %} diff --git a/castellum/urls.py b/castellum/urls.py index 607f61b5a..d3994be65 100644 --- a/castellum/urls.py +++ b/castellum/urls.py @@ -25,7 +25,6 @@ from django.conf import settings from django.contrib import admin from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.auth.views import LoginView from django.contrib.auth.views import LogoutView from django.http import HttpResponse from django.urls import include @@ -38,6 +37,7 @@ from django.views.static import serve from stronghold.decorators import public from castellum.castellum_auth.forms import AuthenticationForm +from castellum.castellum_auth.views import LoginView from castellum.castellum_auth.views import authenticate_begin from castellum.castellum_auth.views import authenticate_complete from castellum.castellum_auth.views import register_begin -- GitLab From 308fc01b3ec5190ced7e3ad7a73344b746f82941 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Wed, 16 Jun 2021 20:40:47 +0200 Subject: [PATCH 6/9] convert to class based views --- castellum/castellum_auth/views.py | 120 +++++++++++++++--------------- castellum/static/js/fido2.js | 8 +- castellum/urls.py | 12 +-- 3 files changed, 68 insertions(+), 72 deletions(-) diff --git a/castellum/castellum_auth/views.py b/castellum/castellum_auth/views.py index 0a4666bd2..63fc5e99a 100644 --- a/castellum/castellum_auth/views.py +++ b/castellum/castellum_auth/views.py @@ -21,11 +21,13 @@ from django.conf import settings from django.contrib.auth import login +from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.views import LoginView as DjangoLoginView from django.http import Http404 from django.http import HttpResponse from django.shortcuts import redirect from django.utils.translation import check_for_language +from django.views.generic import View from fido2 import cbor from fido2.client import ClientData @@ -36,7 +38,7 @@ from fido2.server import Fido2Server from fido2.utils import websafe_decode from fido2.utils import websafe_encode from fido2.webauthn import PublicKeyCredentialRpEntity -from stronghold.decorators import public +from stronghold.views import StrongholdPublicMixin from castellum.utils.views import get_next_url @@ -81,62 +83,60 @@ def get_credentials(user): raw = user.fido2_credential_data if not raw: raise Http404 - return [AttestedCredentialData(websafe_decode(raw)))] - - -def register_begin(request): - registration_data, state = fido2.register_begin( - { - 'id': str(request.user.id).encode('utf-8'), - 'name': request.user.username, - 'displayName': request.user.get_full_name(), - }, - [], - ) - request.session['fido2_state'] = state - return HttpResponse(cbor.encode(registration_data), content_type='application/cbor') - - -def register_complete(request): - data = cbor.decode(request.body) - - auth_data = fido2.register_complete( - request.session.pop('fido2_state'), - ClientData(data['clientData']), - AttestationObject(data['attestationObject']), - ) - - request.user.fido2_credential_data = websafe_encode(auth_data.credential_data) - request.user.save() - - return HttpResponse() - - -@public -def authenticate_begin(request): - user = User.objects.get(pk=request.session['fido2_user']['pk']) - - auth_data, state = fido2.authenticate_begin(get_credentials(user)) - request.session['fido2_state'] = state - return HttpResponse(cbor.encode(auth_data), content_type='application/cbor') - - -@public -def authenticate_complete(request): - user_data = request.session.pop('fido2_user') - user = User.objects.get(pk=user_data['pk']) - user.backend = user_data['backend'] - - data = cbor.decode(request.body) - - fido2.authenticate_complete( - request.session.pop('fido2_state'), - get_credentials(user), - data['credentialId'], - ClientData(data['clientData']), - AuthenticatorData(data['authenticatorData']), - data['signature'], - ) - login(request, user) - - return HttpResponse() + return [AttestedCredentialData(websafe_decode(raw))] + + +class FIDO2RegisterView(LoginRequiredMixin, View): + def get(self, request, *args, **kwargs): + registration_data, state = fido2.register_begin( + { + 'id': str(request.user.id).encode('utf-8'), + 'name': request.user.username, + 'displayName': request.user.get_full_name(), + }, + [], + ) + request.session['fido2_state'] = state + return HttpResponse(cbor.encode(registration_data), content_type='application/cbor') + + def post(self, request, *args, **kwargs): + data = cbor.decode(request.body) + + auth_data = fido2.register_complete( + request.session.pop('fido2_state'), + ClientData(data['clientData']), + AttestationObject(data['attestationObject']), + ) + + request.user.fido2_credential_data = websafe_encode(auth_data.credential_data) + request.user.save() + + return HttpResponse() + + +class FIDO2AuhenticateView(StrongholdPublicMixin, View): + def get(self, request, *args, **kwargs): + user = User.objects.get(pk=request.session['fido2_user']['pk']) + + auth_data, state = fido2.authenticate_begin(get_credentials(user)) + request.session['fido2_state'] = state + return HttpResponse(cbor.encode(auth_data), content_type='application/cbor') + + def post(self, request, *args, **kwargs): + user_data = request.session.pop('fido2_user') + user = User.objects.get(pk=user_data['pk']) + user.backend = user_data['backend'] + + data = cbor.decode(request.body) + + fido2.authenticate_complete( + request.session.pop('fido2_state'), + get_credentials(user), + data['credentialId'], + ClientData(data['clientData']), + AuthenticatorData(data['authenticatorData']), + data['signature'], + ) + login(request, user) + + return HttpResponse() diff --git a/castellum/static/js/fido2.js b/castellum/static/js/fido2.js index 19157b7af..2f91ecdae 100644 --- a/castellum/static/js/fido2.js +++ b/castellum/static/js/fido2.js @@ -17,10 +17,10 @@ }; var register = function() { - return fetch('/fido2/register/begin/', {credentials: 'same-origin'}) + return fetch('/fido2/register/', {credentials: 'same-origin'}) .then(fromCBOR) .then(options => navigator.credentials.create(options)) - .then(attestation => fetch('/fido2/register/complete/', { + .then(attestation => fetch('/fido2/register/', { method: 'POST', credentials: 'same-origin', headers: {'X-CSRFToken': csrfToken}, @@ -35,10 +35,10 @@ }; var authenticate = function() { - return fetch('/fido2/authenticate/begin/', {credentials: 'same-origin'}) + return fetch('/fido2/authenticate/', {credentials: 'same-origin'}) .then(fromCBOR) .then(options => navigator.credentials.get(options)) - .then(assertion => fetch('/fido2/authenticate/complete/', { + .then(assertion => fetch('/fido2/authenticate/', { method: 'POST', credentials: 'same-origin', headers: {'X-CSRFToken': csrfToken}, diff --git a/castellum/urls.py b/castellum/urls.py index d3994be65..cd706de1e 100644 --- a/castellum/urls.py +++ b/castellum/urls.py @@ -37,11 +37,9 @@ from django.views.static import serve from stronghold.decorators import public from castellum.castellum_auth.forms import AuthenticationForm +from castellum.castellum_auth.views import FIDO2AuhenticateView +from castellum.castellum_auth.views import FIDO2RegisterView from castellum.castellum_auth.views import LoginView -from castellum.castellum_auth.views import authenticate_begin -from castellum.castellum_auth.views import authenticate_complete -from castellum.castellum_auth.views import register_begin -from castellum.castellum_auth.views import register_complete from castellum.castellum_auth.views import set_language from castellum.studies.models import Resource from castellum.studies.models import Study @@ -107,10 +105,8 @@ urlpatterns = [ path( 'data-protection/', include('castellum.data_protection.urls', namespace='data_protection') ), - path('fido2/register/begin/', register_begin), - path('fido2/register/complete/', register_complete), - path('fido2/authenticate/begin/', authenticate_begin), - path('fido2/authenticate/complete/', authenticate_complete), + path('fido2/register/', FIDO2RegisterView.as_view()), + path('fido2/authenticate/', FIDO2AuhenticateView.as_view()), ] if settings.PROTECTED_MEDIA_SERVER: -- GitLab From 6fd712880bcc0d09ada63d6d7ce7dce3d7ca3958 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Wed, 16 Jun 2021 22:06:12 +0200 Subject: [PATCH 7/9] better error handling --- castellum/castellum_auth/views.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/castellum/castellum_auth/views.py b/castellum/castellum_auth/views.py index 63fc5e99a..bf0ca69d9 100644 --- a/castellum/castellum_auth/views.py +++ b/castellum/castellum_auth/views.py @@ -116,6 +116,8 @@ class FIDO2RegisterView(LoginRequiredMixin, View): class FIDO2AuhenticateView(StrongholdPublicMixin, View): def get(self, request, *args, **kwargs): + if 'fido2_user' not in request.session: + raise Http404 user = User.objects.get(pk=request.session['fido2_user']['pk']) auth_data, state = fido2.authenticate_begin(get_credentials(user)) @@ -123,6 +125,8 @@ class FIDO2AuhenticateView(StrongholdPublicMixin, View): return HttpResponse(cbor.encode(auth_data), content_type='application/cbor') def post(self, request, *args, **kwargs): + if 'fido2_user' not in request.session: + raise Http404 user_data = request.session.pop('fido2_user') user = User.objects.get(pk=user_data['pk']) user.backend = user_data['backend'] -- GitLab From 58737be5884fec815f4b683d877c0978a0ad5bc1 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Sun, 20 Jun 2021 14:21:29 +0200 Subject: [PATCH 8/9] js: no success on error message --- castellum/static/js/fido2.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/castellum/static/js/fido2.js b/castellum/static/js/fido2.js index 2f91ecdae..7028d997b 100644 --- a/castellum/static/js/fido2.js +++ b/castellum/static/js/fido2.js @@ -30,8 +30,7 @@ }), })) .then(raiseForStatus) - .catch(alert) - .then(() => alert(django.gettext('second factor has been registered'))); + .then(() => alert(django.gettext('second factor has been registered')), alert); }; var authenticate = function() { @@ -49,8 +48,7 @@ 'signature': new Uint8Array(assertion.response.signature), }), })) - .then(raiseForStatus) - .catch(alert); + .then(raiseForStatus); }; $$.on(document, 'click', '[data-js="fido2-register"]', register); @@ -60,6 +58,6 @@ if (fido2Auto) { authenticate().then(() => { window.location = fido2Auto.dataset.successUrl; - }); + }, alert); } })(); -- GitLab From 10040ac175d063df24ea0ba98b11b68a542f8788 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Sun, 20 Jun 2021 14:21:48 +0200 Subject: [PATCH 9/9] fix js DOM position --- castellum/templates/index.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/castellum/templates/index.html b/castellum/templates/index.html index 8240e3260..5938008b5 100644 --- a/castellum/templates/index.html +++ b/castellum/templates/index.html @@ -73,6 +73,9 @@ {% csrf_token %} +{% endblock %} + +{% block extra_scripts %} {% endblock %} -- GitLab