0
0
Fork 0
mirror of https://github.com/healthchecks/healthchecks.git synced 2025-04-11 15:51:19 +00:00

Add a two-factor authentication form (WIP)

This commit is contained in:
Pēteris Caune 2020-11-14 12:54:26 +02:00
parent 2ac0f87560
commit 64be87137b
No known key found for this signature in database
GPG key ID: E28D7679E9A9EDE2
9 changed files with 181 additions and 25 deletions

View file

@ -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())

View file

@ -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"),

View file

@ -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)

View file

@ -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;
}

View file

@ -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");
});
}

37
static/js/login_tfa.js Normal file
View file

@ -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();
});

View file

@ -38,7 +38,7 @@
</button>
</div>
<div id="add-credential-waiting" class="hide">
<div id="waiting" class="hide">
<h2>Waiting for security key</h2>
<p>
Follow your browser's steps to register your security key
@ -52,11 +52,11 @@
</div>
</div>
<div id="add-credential-error" class="alert alert-danger hide">
<div id="error" class="alert alert-danger hide">
<p>
<strong>Something went wrong.</strong>
</p>
<p id="add-credential-error-text"></p>
<p id="error-text"></p>
<div class="text-right">
<button id="retry" type="button" class="btn btn-danger">
@ -65,7 +65,7 @@
</div>
</div>
<div id="add-credential-success" class="hide">
<div id="success" class="hide">
<div class="alert alert-success">
<strong>Success!</strong>
Credential acquired.

View file

@ -0,0 +1,65 @@
{% extends "base.html" %}
{% load compress static hc_extras %}
{% block content %}
<div class="row">
<form
id="login-tfa-form"
class="col-sm-6 col-sm-offset-3"
data-options="{{ options }}"
method="post"
encrypt="multipart/form-data">
<h1>Two-factor Authentication</h1>
{% csrf_token %}
<input id="credential_id" type="hidden" name="credential_id">
<input id="authenticator_data" type="hidden" name="authenticator_data">
<input id="client_data_json" type="hidden" name="client_data_json">
<input id="signature" type="hidden" name="signature">
<div id="waiting" class="hide">
<h2>Waiting for security key</h2>
<p>
Follow your browser's steps to register your security key
with {% site_name %}.
</p>
<div class="spinner started">
<div class="d1"></div>
<div class="d2"></div>
<div class="d3"></div>
</div>
</div>
<div id="error" class="alert alert-danger hide">
<p>
<strong>Something went wrong.</strong>
</p>
<p id="error-text"></p>
<div class="text-right">
<button id="retry" type="button" class="btn btn-danger">
Try Again
</button>
</div>
</div>
<div id="success" class="hide">
<div class="alert alert-success">
<strong>Success!</strong>
Credential acquired.
</div>
</div>
</form>
</div>
{% endblock %}
{% block scripts %}
{% compress js %}
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/cbor.js' %}"></script>
<script src="{% static 'js/login_tfa.js' %}"></script>
{% endcompress %}
{% endblock %}

View file

@ -3,7 +3,6 @@
{% block content %}
{{ registration_dict|json_script:"registration" }}
<div class="row">
<form class="col-sm-6 col-sm-offset-3" method="post">
{% csrf_token %}
@ -31,12 +30,3 @@
</form>
</div>
{% endblock %}
{% block scripts %}
{% compress js %}
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/cbor.js' %}"></script>
<script src="{% static 'js/add_credential.js' %}"></script>
{% endcompress %}
{% endblock %}