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:
parent
2ac0f87560
commit
64be87137b
9 changed files with 181 additions and 25 deletions
hc/accounts
static
templates/accounts
|
@ -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())
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
37
static/js/login_tfa.js
Normal 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();
|
||||
|
||||
});
|
|
@ -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.
|
||||
|
|
65
templates/accounts/login_tfa.html
Normal file
65
templates/accounts/login_tfa.html
Normal 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 %}
|
|
@ -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 %}
|
||||
|
|
Loading…
Add table
Reference in a new issue