Browse Source

Add a two-factor authentication form (WIP)

pull/456/head
Pēteris Caune 4 years ago
parent
commit
64be87137b
No known key found for this signature in database GPG Key ID: E28D7679E9A9EDE2
9 changed files with 181 additions and 25 deletions
  1. +30
    -1
      hc/accounts/forms.py
  2. +1
    -0
      hc/accounts/urls.py
  3. +34
    -0
      hc/accounts/views.py
  4. +2
    -2
      static/css/add_credential.css
  5. +8
    -8
      static/js/add_credential.js
  6. +37
    -0
      static/js/login_tfa.js
  7. +4
    -4
      templates/accounts/add_credential.html
  8. +65
    -0
      templates/accounts/login_tfa.html
  9. +0
    -10
      templates/accounts/remove_credential.html

+ 30
- 1
hc/accounts/forms.py 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())

+ 1
- 0
hc/accounts/urls.py 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"),


+ 34
- 0
hc/accounts/views.py 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)

+ 2
- 2
static/css/add_credential.css 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;
}

+ 8
- 8
static/js/add_credential.js 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
- 0
static/js/login_tfa.js 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();
});

+ 4
- 4
templates/accounts/add_credential.html 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.


+ 65
- 0
templates/accounts/login_tfa.html 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 %}

+ 0
- 10
templates/accounts/remove_credential.html 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 %}

Loading…
Cancel
Save