From 64be87137b479c069cb2481a9594c97d723a07d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Sat, 14 Nov 2020 12:54:26 +0200 Subject: [PATCH] Add a two-factor authentication form (WIP) --- hc/accounts/forms.py | 31 ++++++++++- hc/accounts/urls.py | 1 + hc/accounts/views.py | 34 ++++++++++++ static/css/add_credential.css | 4 +- static/js/add_credential.js | 16 +++--- static/js/login_tfa.js | 37 +++++++++++++ templates/accounts/add_credential.html | 8 +-- templates/accounts/login_tfa.html | 65 +++++++++++++++++++++++ templates/accounts/remove_credential.html | 10 ---- 9 files changed, 181 insertions(+), 25 deletions(-) create mode 100644 static/js/login_tfa.js create mode 100644 templates/accounts/login_tfa.html diff --git a/hc/accounts/forms.py b/hc/accounts/forms.py index ad8f11f0..5aac74ec 100644 --- a/hc/accounts/forms.py +++ b/hc/accounts/forms.py @@ -4,7 +4,7 @@ from datetime import timedelta as td from django import forms from django.contrib.auth import authenticate from django.contrib.auth.models import User -from fido2.ctap2 import AttestationObject +from fido2.ctap2 import AttestationObject, AuthenticatorData from fido2.client import ClientData from hc.api.models import TokenBucket @@ -136,3 +136,32 @@ class AddCredentialForm(forms.Form): obj = AttestationObject(binary) return obj + + +class LoginTfaForm(forms.Form): + credential_id = forms.CharField(required=True) + client_data_json = forms.CharField(required=True) + authenticator_data = forms.CharField(required=True) + signature = forms.CharField(required=True) + + def clean_credential_id(self): + v = self.cleaned_data["credential_id"] + return base64.b64decode(v.encode()) + + def clean_client_data_json(self): + v = self.cleaned_data["client_data_json"] + binary = base64.b64decode(v.encode()) + obj = ClientData(binary) + + return obj + + def clean_authenticator_data(self): + v = self.cleaned_data["authenticator_data"] + binary = base64.b64decode(v.encode()) + obj = AuthenticatorData(binary) + + return obj + + def clean_signature(self): + v = self.cleaned_data["signature"] + return base64.b64decode(v.encode()) diff --git a/hc/accounts/urls.py b/hc/accounts/urls.py index d139e2b2..b96d1986 100644 --- a/hc/accounts/urls.py +++ b/hc/accounts/urls.py @@ -3,6 +3,7 @@ from hc.accounts import views urlpatterns = [ path("login/", views.login, name="hc-login"), + path("login/two_factor/", views.login_tfa, name="hc-login-tfa"), path("logout/", views.logout, name="hc-logout"), path("signup/", views.signup, name="hc-signup"), path("login_link_sent/", views.login_link_sent, name="hc-login-link-sent"), diff --git a/hc/accounts/views.py b/hc/accounts/views.py index 942636db..f996e72f 100644 --- a/hc/accounts/views.py +++ b/hc/accounts/views.py @@ -623,3 +623,37 @@ def remove_credential(request, code): ctx = {"credential": credential} return render(request, "accounts/remove_credential.html", ctx) + + +def login_tfa(request): + rp = PublicKeyCredentialRpEntity("localhost", "Healthchecks") + # FIXME use HTTPS, remove the verify_origin hack + server = Fido2Server(rp, verify_origin=_verify_origin) + + # FIXME + user_id = 1 + user = User.objects.get(id=user_id) + credentials = [c.unpack() for c in user.credentials.all()] + + if request.method == "POST": + form = forms.LoginTfaForm(request.POST) + if not form.is_valid(): + return HttpResponseBadRequest() + + server.authenticate_complete( + request.session.pop("state", ""), + credentials, + form.cleaned_data["credential_id"], + form.cleaned_data["client_data_json"], + form.cleaned_data["authenticator_data"], + form.cleaned_data["signature"], + ) + from django.http import HttpResponse + + return HttpResponse("all is well!") + + options, state = server.authenticate_begin(credentials) + + request.session["state"] = state + ctx = {"options": base64.b64encode(cbor.encode(options)).decode()} + return render(request, "accounts/login_tfa.html", ctx) diff --git a/static/css/add_credential.css b/static/css/add_credential.css index 868070fe..cd7fd6af 100644 --- a/static/css/add_credential.css +++ b/static/css/add_credential.css @@ -1,8 +1,8 @@ -#add-credential-waiting .spinner { +#waiting .spinner { margin: 0; } -#add-credential-error-text { +#add-credential-form #error-text, #login-tfa-form #error-text { font-family: "Lucida Console", Monaco, monospace; margin: 16px 0; } \ No newline at end of file diff --git a/static/js/add_credential.js b/static/js/add_credential.js index 7b427df9..c9fe54c4 100644 --- a/static/js/add_credential.js +++ b/static/js/add_credential.js @@ -11,22 +11,22 @@ $(function() { function requestCredentials() { // Hide error & success messages, show the "waiting" message $("#name-next").addClass("hide"); - $("#add-credential-waiting").removeClass("hide"); - $("#add-credential-error").addClass("hide"); - $("#add-credential-success").addClass("hide"); + $("#waiting").removeClass("hide"); + $("#error").addClass("hide"); + $("#success").addClass("hide"); navigator.credentials.create(options).then(function(attestation) { $("#attestation_object").val(b64(attestation.response.attestationObject)); $("#client_data_json").val(b64(attestation.response.clientDataJSON)); // Show the success message and save button - $("#add-credential-waiting").addClass("hide"); - $("#add-credential-success").removeClass("hide"); + $("#waiting").addClass("hide"); + $("#success").removeClass("hide"); }).catch(function(err) { // Show the error message - $("#add-credential-waiting").addClass("hide"); - $("#add-credential-error-text").text(err); - $("#add-credential-error").removeClass("hide"); + $("#waiting").addClass("hide"); + $("#error-text").text(err); + $("#error").removeClass("hide"); }); } diff --git a/static/js/login_tfa.js b/static/js/login_tfa.js new file mode 100644 index 00000000..bbba6fc8 --- /dev/null +++ b/static/js/login_tfa.js @@ -0,0 +1,37 @@ +$(function() { + var form = document.getElementById("login-tfa-form"); + var optionsBytes = Uint8Array.from(atob(form.dataset.options), c => c.charCodeAt(0)); + // cbor.js expects ArrayBuffer as input when decoding + var options = CBOR.decode(optionsBytes.buffer); + + function b64(arraybuffer) { + return btoa(String.fromCharCode.apply(null, new Uint8Array(arraybuffer))); + } + + function authenticate() { + $("#waiting").removeClass("hide"); + $("#error").addClass("hide"); + + navigator.credentials.get(options).then(function(assertion) { + $("#credential_id").val(b64(assertion.rawId)); + $("#authenticator_data").val(b64(assertion.response.authenticatorData)); + $("#client_data_json").val(b64(assertion.response.clientDataJSON)); + $("#signature").val(b64(assertion.response.signature)); + + // Show the success message and save button + $("#waiting").addClass("hide"); + $("#success").removeClass("hide"); + form.submit() + }).catch(function(err) { + // Show the error message + $("#waiting").addClass("hide"); + $("#error-text").text(err); + $("#error").removeClass("hide"); + }); + } + + $("#retry").click(authenticate); + + authenticate(); + +}); diff --git a/templates/accounts/add_credential.html b/templates/accounts/add_credential.html index 65003b21..0e9230de 100644 --- a/templates/accounts/add_credential.html +++ b/templates/accounts/add_credential.html @@ -38,7 +38,7 @@ -
+

Waiting for security key

Follow your browser's steps to register your security key @@ -52,11 +52,11 @@

-
+

Something went wrong.

-

+

-
+
Success! Credential acquired. diff --git a/templates/accounts/login_tfa.html b/templates/accounts/login_tfa.html new file mode 100644 index 00000000..6502baa9 --- /dev/null +++ b/templates/accounts/login_tfa.html @@ -0,0 +1,65 @@ +{% extends "base.html" %} +{% load compress static hc_extras %} + +{% block content %} + +
+
+

Two-factor Authentication

+ + {% csrf_token %} + + + + + +
+

Waiting for security key

+

+ Follow your browser's steps to register your security key + with {% site_name %}. +

+ +
+
+
+
+
+
+ +
+

+ Something went wrong. +

+

+ +
+ +
+
+ +
+
+ Success! + Credential acquired. +
+
+
+
+{% endblock %} + +{% block scripts %} +{% compress js %} + + + + +{% endcompress %} +{% endblock %} diff --git a/templates/accounts/remove_credential.html b/templates/accounts/remove_credential.html index 0ede5b9e..ba58fd38 100644 --- a/templates/accounts/remove_credential.html +++ b/templates/accounts/remove_credential.html @@ -3,7 +3,6 @@ {% block content %} -{{ registration_dict|json_script:"registration" }}
{% csrf_token %} @@ -31,12 +30,3 @@
{% endblock %} - -{% block scripts %} -{% compress js %} - - - - -{% endcompress %} -{% endblock %}