forked from GithubBackups/healthchecks
Add experimental code for registering Webauthn credentials
This commit is contained in:
parent
cdd2e98bd0
commit
1eaa216d3a
27
hc/accounts/migrations/0034_credential.py
Normal file
27
hc/accounts/migrations/0034_credential.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Generated by Django 3.1.2 on 2020-11-12 13:39
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('accounts', '0033_member_rw'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Credential',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.UUIDField(default=uuid.uuid4, unique=True)),
|
||||
('name', models.CharField(blank=True, max_length=200)),
|
||||
('data', models.BinaryField()),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
@ -11,6 +11,7 @@ from django.db import models
|
||||
from django.db.models import Count, Q
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from fido2.ctap2 import AttestedCredentialData
|
||||
from hc.lib import emails
|
||||
from hc.lib.date import month_boundaries
|
||||
|
||||
@ -390,3 +391,14 @@ class Member(models.Model):
|
||||
|
||||
def can_accept(self):
|
||||
return self.user.profile.can_accept(self.project)
|
||||
|
||||
|
||||
class Credential(models.Model):
|
||||
code = models.UUIDField(default=uuid.uuid4, unique=True)
|
||||
name = models.CharField(max_length=200, blank=True)
|
||||
user = models.ForeignKey(User, models.CASCADE, related_name="credentials")
|
||||
data = models.BinaryField()
|
||||
|
||||
def unpack(self):
|
||||
unpacked, remaining_data = AttestedCredentialData.unpack_from(self.data)
|
||||
return unpacked
|
||||
|
@ -23,4 +23,5 @@ urlpatterns = [
|
||||
path("set_password/<slug:token>/", views.set_password, name="hc-set-password"),
|
||||
path("change_email/done/", views.change_email_done, name="hc-change-email-done"),
|
||||
path("change_email/<slug:token>/", views.change_email, name="hc-change-email"),
|
||||
path("two_factor/add/", views.add_credential, name="hc-add-credential"),
|
||||
]
|
||||
|
@ -1,3 +1,4 @@
|
||||
import base64
|
||||
from datetime import timedelta as td
|
||||
from urllib.parse import urlparse
|
||||
import uuid
|
||||
@ -17,8 +18,13 @@ from django.utils.timezone import now
|
||||
from django.urls import resolve, Resolver404
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_POST
|
||||
from fido2.client import ClientData
|
||||
from fido2.ctap2 import AttestationObject
|
||||
from fido2.server import Fido2Server
|
||||
from fido2.webauthn import PublicKeyCredentialRpEntity
|
||||
from fido2 import cbor
|
||||
from hc.accounts import forms
|
||||
from hc.accounts.models import Profile, Project, Member
|
||||
from hc.accounts.models import Credential, Profile, Project, Member
|
||||
from hc.api.models import Channel, Check, TokenBucket
|
||||
from hc.lib.date import choose_next_report_date
|
||||
from hc.payments.models import Subscription
|
||||
@ -176,8 +182,8 @@ def check_token(request, username, token):
|
||||
# To work around this, we sign user in if the method is POST
|
||||
# *or* if the browser presents a cookie we had set when sending the login link.
|
||||
#
|
||||
# If the method is GET, we instead serve a HTML form and a piece
|
||||
# of Javascript to automatically submit it.
|
||||
# If the method is GET and the auto-login cookie isn't present, we serve
|
||||
# a HTML form with a submit button.
|
||||
|
||||
if request.method == "POST" or "auto-login" in request.COOKIES:
|
||||
user = authenticate(username=username, token=token)
|
||||
@ -541,3 +547,54 @@ def remove_project(request, code):
|
||||
project = get_object_or_404(Project, code=code, owner=request.user)
|
||||
project.delete()
|
||||
return redirect("hc-index")
|
||||
|
||||
|
||||
def _verify_origin(aaa):
|
||||
return lambda o: True
|
||||
|
||||
|
||||
@login_required
|
||||
def add_credential(request):
|
||||
rp = PublicKeyCredentialRpEntity("localhost", "Healthchecks")
|
||||
# FIXME use HTTPS, remove the verify_origin hack
|
||||
server = Fido2Server(rp, verify_origin=_verify_origin)
|
||||
|
||||
def decode(form, key):
|
||||
return base64.b64decode(request.POST[key].encode())
|
||||
|
||||
if request.method == "POST":
|
||||
# idea: use AddCredentialForm
|
||||
client_data = ClientData(decode(request.POST, "clientDataJSON"))
|
||||
att_obj = AttestationObject(decode(request.POST, "attestationObject"))
|
||||
print("clientData", client_data)
|
||||
print("AttestationObject:", att_obj)
|
||||
|
||||
auth_data = server.register_complete(
|
||||
request.session["state"], client_data, att_obj
|
||||
)
|
||||
|
||||
c = Credential(user=request.user)
|
||||
c.name = request.POST["name"]
|
||||
c.data = auth_data.credential_data
|
||||
c.save()
|
||||
|
||||
print("REGISTERED CREDENTIAL:", auth_data.credential_data)
|
||||
return render(request, "accounts/success.html")
|
||||
|
||||
credentials = [c.unpack() for c in request.user.credentials.all()]
|
||||
print(credentials)
|
||||
|
||||
options, state = server.register_begin(
|
||||
{
|
||||
"id": request.user.username.encode(),
|
||||
"name": request.user.email,
|
||||
"displayName": request.user.email,
|
||||
},
|
||||
credentials,
|
||||
)
|
||||
|
||||
request.session["state"] = state
|
||||
|
||||
# FIXME: avoid using cbor and cbor.js?
|
||||
ctx = {"options": base64.b64encode(cbor.encode(options)).decode()}
|
||||
return render(request, "accounts/add_credential.html", ctx)
|
||||
|
@ -2,6 +2,7 @@ cron-descriptor==1.2.24
|
||||
croniter==0.3.31
|
||||
Django==3.1.2
|
||||
django-compressor==2.4
|
||||
fido2
|
||||
psycopg2==2.8.4
|
||||
pytz==2020.1
|
||||
requests==2.23.0
|
||||
|
22
static/js/add_credential.js
Normal file
22
static/js/add_credential.js
Normal file
@ -0,0 +1,22 @@
|
||||
$(function() {
|
||||
var form = document.getElementById("add-credential-form");
|
||||
var optionsBinary = btoa(form.dataset.options);
|
||||
var array = Uint8Array.from(atob(form.dataset.options), c => c.charCodeAt(0));
|
||||
var options = CBOR.decode(array.buffer);
|
||||
console.log("decoded options:", options);
|
||||
|
||||
function b64(arraybuffer) {
|
||||
return btoa(String.fromCharCode.apply(null, new Uint8Array(arraybuffer)));
|
||||
}
|
||||
|
||||
navigator.credentials.create(options).then(function(attestation) {
|
||||
console.log("got attestation: ", attestation);
|
||||
|
||||
document.getElementById("attestationObject").value = b64(attestation.response.attestationObject);
|
||||
document.getElementById("clientDataJSON").value = b64(attestation.response.clientDataJSON);
|
||||
console.log("form updated, all is well");
|
||||
$("#add-credential-submit").prop("disabled", "");
|
||||
}).catch(function(err) {
|
||||
console.log("Something went wrong", err);
|
||||
});
|
||||
});
|
406
static/js/cbor.js
Normal file
406
static/js/cbor.js
Normal file
@ -0,0 +1,406 @@
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2016 Patrick Gansterer <paroga@paroga.com>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
(function(global, undefined) { "use strict";
|
||||
var POW_2_24 = 5.960464477539063e-8,
|
||||
POW_2_32 = 4294967296,
|
||||
POW_2_53 = 9007199254740992;
|
||||
|
||||
function encode(value) {
|
||||
var data = new ArrayBuffer(256);
|
||||
var dataView = new DataView(data);
|
||||
var lastLength;
|
||||
var offset = 0;
|
||||
|
||||
function prepareWrite(length) {
|
||||
var newByteLength = data.byteLength;
|
||||
var requiredLength = offset + length;
|
||||
while (newByteLength < requiredLength)
|
||||
newByteLength <<= 1;
|
||||
if (newByteLength !== data.byteLength) {
|
||||
var oldDataView = dataView;
|
||||
data = new ArrayBuffer(newByteLength);
|
||||
dataView = new DataView(data);
|
||||
var uint32count = (offset + 3) >> 2;
|
||||
for (var i = 0; i < uint32count; ++i)
|
||||
dataView.setUint32(i << 2, oldDataView.getUint32(i << 2));
|
||||
}
|
||||
|
||||
lastLength = length;
|
||||
return dataView;
|
||||
}
|
||||
function commitWrite() {
|
||||
offset += lastLength;
|
||||
}
|
||||
function writeFloat64(value) {
|
||||
commitWrite(prepareWrite(8).setFloat64(offset, value));
|
||||
}
|
||||
function writeUint8(value) {
|
||||
commitWrite(prepareWrite(1).setUint8(offset, value));
|
||||
}
|
||||
function writeUint8Array(value) {
|
||||
var dataView = prepareWrite(value.length);
|
||||
for (var i = 0; i < value.length; ++i)
|
||||
dataView.setUint8(offset + i, value[i]);
|
||||
commitWrite();
|
||||
}
|
||||
function writeUint16(value) {
|
||||
commitWrite(prepareWrite(2).setUint16(offset, value));
|
||||
}
|
||||
function writeUint32(value) {
|
||||
commitWrite(prepareWrite(4).setUint32(offset, value));
|
||||
}
|
||||
function writeUint64(value) {
|
||||
var low = value % POW_2_32;
|
||||
var high = (value - low) / POW_2_32;
|
||||
var dataView = prepareWrite(8);
|
||||
dataView.setUint32(offset, high);
|
||||
dataView.setUint32(offset + 4, low);
|
||||
commitWrite();
|
||||
}
|
||||
function writeTypeAndLength(type, length) {
|
||||
if (length < 24) {
|
||||
writeUint8(type << 5 | length);
|
||||
} else if (length < 0x100) {
|
||||
writeUint8(type << 5 | 24);
|
||||
writeUint8(length);
|
||||
} else if (length < 0x10000) {
|
||||
writeUint8(type << 5 | 25);
|
||||
writeUint16(length);
|
||||
} else if (length < 0x100000000) {
|
||||
writeUint8(type << 5 | 26);
|
||||
writeUint32(length);
|
||||
} else {
|
||||
writeUint8(type << 5 | 27);
|
||||
writeUint64(length);
|
||||
}
|
||||
}
|
||||
|
||||
function encodeItem(value) {
|
||||
var i;
|
||||
|
||||
if (value === false)
|
||||
return writeUint8(0xf4);
|
||||
if (value === true)
|
||||
return writeUint8(0xf5);
|
||||
if (value === null)
|
||||
return writeUint8(0xf6);
|
||||
if (value === undefined)
|
||||
return writeUint8(0xf7);
|
||||
|
||||
switch (typeof value) {
|
||||
case "number":
|
||||
if (Math.floor(value) === value) {
|
||||
if (0 <= value && value <= POW_2_53)
|
||||
return writeTypeAndLength(0, value);
|
||||
if (-POW_2_53 <= value && value < 0)
|
||||
return writeTypeAndLength(1, -(value + 1));
|
||||
}
|
||||
writeUint8(0xfb);
|
||||
return writeFloat64(value);
|
||||
|
||||
case "string":
|
||||
var utf8data = [];
|
||||
for (i = 0; i < value.length; ++i) {
|
||||
var charCode = value.charCodeAt(i);
|
||||
if (charCode < 0x80) {
|
||||
utf8data.push(charCode);
|
||||
} else if (charCode < 0x800) {
|
||||
utf8data.push(0xc0 | charCode >> 6);
|
||||
utf8data.push(0x80 | charCode & 0x3f);
|
||||
} else if (charCode < 0xd800) {
|
||||
utf8data.push(0xe0 | charCode >> 12);
|
||||
utf8data.push(0x80 | (charCode >> 6) & 0x3f);
|
||||
utf8data.push(0x80 | charCode & 0x3f);
|
||||
} else {
|
||||
charCode = (charCode & 0x3ff) << 10;
|
||||
charCode |= value.charCodeAt(++i) & 0x3ff;
|
||||
charCode += 0x10000;
|
||||
|
||||
utf8data.push(0xf0 | charCode >> 18);
|
||||
utf8data.push(0x80 | (charCode >> 12) & 0x3f);
|
||||
utf8data.push(0x80 | (charCode >> 6) & 0x3f);
|
||||
utf8data.push(0x80 | charCode & 0x3f);
|
||||
}
|
||||
}
|
||||
|
||||
writeTypeAndLength(3, utf8data.length);
|
||||
return writeUint8Array(utf8data);
|
||||
|
||||
default:
|
||||
var length;
|
||||
if (Array.isArray(value)) {
|
||||
length = value.length;
|
||||
writeTypeAndLength(4, length);
|
||||
for (i = 0; i < length; ++i)
|
||||
encodeItem(value[i]);
|
||||
} else if (value instanceof Uint8Array) {
|
||||
writeTypeAndLength(2, value.length);
|
||||
writeUint8Array(value);
|
||||
} else {
|
||||
var keys = Object.keys(value);
|
||||
length = keys.length;
|
||||
writeTypeAndLength(5, length);
|
||||
for (i = 0; i < length; ++i) {
|
||||
var key = keys[i];
|
||||
encodeItem(key);
|
||||
encodeItem(value[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
encodeItem(value);
|
||||
|
||||
if ("slice" in data)
|
||||
return data.slice(0, offset);
|
||||
|
||||
var ret = new ArrayBuffer(offset);
|
||||
var retView = new DataView(ret);
|
||||
for (var i = 0; i < offset; ++i)
|
||||
retView.setUint8(i, dataView.getUint8(i));
|
||||
return ret;
|
||||
}
|
||||
|
||||
function decode(data, tagger, simpleValue) {
|
||||
var dataView = new DataView(data);
|
||||
var offset = 0;
|
||||
|
||||
if (typeof tagger !== "function")
|
||||
tagger = function(value) { return value; };
|
||||
if (typeof simpleValue !== "function")
|
||||
simpleValue = function() { return undefined; };
|
||||
|
||||
function commitRead(length, value) {
|
||||
offset += length;
|
||||
return value;
|
||||
}
|
||||
function readArrayBuffer(length) {
|
||||
return commitRead(length, new Uint8Array(data, offset, length));
|
||||
}
|
||||
function readFloat16() {
|
||||
var tempArrayBuffer = new ArrayBuffer(4);
|
||||
var tempDataView = new DataView(tempArrayBuffer);
|
||||
var value = readUint16();
|
||||
|
||||
var sign = value & 0x8000;
|
||||
var exponent = value & 0x7c00;
|
||||
var fraction = value & 0x03ff;
|
||||
|
||||
if (exponent === 0x7c00)
|
||||
exponent = 0xff << 10;
|
||||
else if (exponent !== 0)
|
||||
exponent += (127 - 15) << 10;
|
||||
else if (fraction !== 0)
|
||||
return (sign ? -1 : 1) * fraction * POW_2_24;
|
||||
|
||||
tempDataView.setUint32(0, sign << 16 | exponent << 13 | fraction << 13);
|
||||
return tempDataView.getFloat32(0);
|
||||
}
|
||||
function readFloat32() {
|
||||
return commitRead(4, dataView.getFloat32(offset));
|
||||
}
|
||||
function readFloat64() {
|
||||
return commitRead(8, dataView.getFloat64(offset));
|
||||
}
|
||||
function readUint8() {
|
||||
return commitRead(1, dataView.getUint8(offset));
|
||||
}
|
||||
function readUint16() {
|
||||
return commitRead(2, dataView.getUint16(offset));
|
||||
}
|
||||
function readUint32() {
|
||||
return commitRead(4, dataView.getUint32(offset));
|
||||
}
|
||||
function readUint64() {
|
||||
return readUint32() * POW_2_32 + readUint32();
|
||||
}
|
||||
function readBreak() {
|
||||
if (dataView.getUint8(offset) !== 0xff)
|
||||
return false;
|
||||
offset += 1;
|
||||
return true;
|
||||
}
|
||||
function readLength(additionalInformation) {
|
||||
if (additionalInformation < 24)
|
||||
return additionalInformation;
|
||||
if (additionalInformation === 24)
|
||||
return readUint8();
|
||||
if (additionalInformation === 25)
|
||||
return readUint16();
|
||||
if (additionalInformation === 26)
|
||||
return readUint32();
|
||||
if (additionalInformation === 27)
|
||||
return readUint64();
|
||||
if (additionalInformation === 31)
|
||||
return -1;
|
||||
throw "Invalid length encoding";
|
||||
}
|
||||
function readIndefiniteStringLength(majorType) {
|
||||
var initialByte = readUint8();
|
||||
if (initialByte === 0xff)
|
||||
return -1;
|
||||
var length = readLength(initialByte & 0x1f);
|
||||
if (length < 0 || (initialByte >> 5) !== majorType)
|
||||
throw "Invalid indefinite length element";
|
||||
return length;
|
||||
}
|
||||
|
||||
function appendUtf16Data(utf16data, length) {
|
||||
for (var i = 0; i < length; ++i) {
|
||||
var value = readUint8();
|
||||
if (value & 0x80) {
|
||||
if (value < 0xe0) {
|
||||
value = (value & 0x1f) << 6
|
||||
| (readUint8() & 0x3f);
|
||||
length -= 1;
|
||||
} else if (value < 0xf0) {
|
||||
value = (value & 0x0f) << 12
|
||||
| (readUint8() & 0x3f) << 6
|
||||
| (readUint8() & 0x3f);
|
||||
length -= 2;
|
||||
} else {
|
||||
value = (value & 0x0f) << 18
|
||||
| (readUint8() & 0x3f) << 12
|
||||
| (readUint8() & 0x3f) << 6
|
||||
| (readUint8() & 0x3f);
|
||||
length -= 3;
|
||||
}
|
||||
}
|
||||
|
||||
if (value < 0x10000) {
|
||||
utf16data.push(value);
|
||||
} else {
|
||||
value -= 0x10000;
|
||||
utf16data.push(0xd800 | (value >> 10));
|
||||
utf16data.push(0xdc00 | (value & 0x3ff));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function decodeItem() {
|
||||
var initialByte = readUint8();
|
||||
var majorType = initialByte >> 5;
|
||||
var additionalInformation = initialByte & 0x1f;
|
||||
var i;
|
||||
var length;
|
||||
|
||||
if (majorType === 7) {
|
||||
switch (additionalInformation) {
|
||||
case 25:
|
||||
return readFloat16();
|
||||
case 26:
|
||||
return readFloat32();
|
||||
case 27:
|
||||
return readFloat64();
|
||||
}
|
||||
}
|
||||
|
||||
length = readLength(additionalInformation);
|
||||
if (length < 0 && (majorType < 2 || 6 < majorType))
|
||||
throw "Invalid length";
|
||||
|
||||
switch (majorType) {
|
||||
case 0:
|
||||
return length;
|
||||
case 1:
|
||||
return -1 - length;
|
||||
case 2:
|
||||
if (length < 0) {
|
||||
var elements = [];
|
||||
var fullArrayLength = 0;
|
||||
while ((length = readIndefiniteStringLength(majorType)) >= 0) {
|
||||
fullArrayLength += length;
|
||||
elements.push(readArrayBuffer(length));
|
||||
}
|
||||
var fullArray = new Uint8Array(fullArrayLength);
|
||||
var fullArrayOffset = 0;
|
||||
for (i = 0; i < elements.length; ++i) {
|
||||
fullArray.set(elements[i], fullArrayOffset);
|
||||
fullArrayOffset += elements[i].length;
|
||||
}
|
||||
return fullArray;
|
||||
}
|
||||
return readArrayBuffer(length);
|
||||
case 3:
|
||||
var utf16data = [];
|
||||
if (length < 0) {
|
||||
while ((length = readIndefiniteStringLength(majorType)) >= 0)
|
||||
appendUtf16Data(utf16data, length);
|
||||
} else
|
||||
appendUtf16Data(utf16data, length);
|
||||
return String.fromCharCode.apply(null, utf16data);
|
||||
case 4:
|
||||
var retArray;
|
||||
if (length < 0) {
|
||||
retArray = [];
|
||||
while (!readBreak())
|
||||
retArray.push(decodeItem());
|
||||
} else {
|
||||
retArray = new Array(length);
|
||||
for (i = 0; i < length; ++i)
|
||||
retArray[i] = decodeItem();
|
||||
}
|
||||
return retArray;
|
||||
case 5:
|
||||
var retObject = {};
|
||||
for (i = 0; i < length || length < 0 && !readBreak(); ++i) {
|
||||
var key = decodeItem();
|
||||
retObject[key] = decodeItem();
|
||||
}
|
||||
return retObject;
|
||||
case 6:
|
||||
return tagger(decodeItem(), length);
|
||||
case 7:
|
||||
switch (length) {
|
||||
case 20:
|
||||
return false;
|
||||
case 21:
|
||||
return true;
|
||||
case 22:
|
||||
return null;
|
||||
case 23:
|
||||
return undefined;
|
||||
default:
|
||||
return simpleValue(length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var ret = decodeItem();
|
||||
if (offset !== data.byteLength)
|
||||
throw "Remaining bytes";
|
||||
return ret;
|
||||
}
|
||||
|
||||
var obj = { encode: encode, decode: decode };
|
||||
|
||||
if (typeof define === "function" && define.amd)
|
||||
define("cbor/cbor", obj);
|
||||
else if (typeof module !== "undefined" && module.exports)
|
||||
module.exports = obj;
|
||||
else if (!global.CBOR)
|
||||
global.CBOR = obj;
|
||||
|
||||
})(this);
|
43
templates/accounts/add_credential.html
Normal file
43
templates/accounts/add_credential.html
Normal file
@ -0,0 +1,43 @@
|
||||
{% extends "base.html" %}
|
||||
{% load compress static %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Add Credential</h1>
|
||||
|
||||
{{ registration_dict|json_script:"registration" }}
|
||||
|
||||
<form
|
||||
id="add-credential-form"
|
||||
data-options="{{ options }}"
|
||||
method="post"
|
||||
encrypt="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<input id="attestationObject" type="hidden" name="attestationObject">
|
||||
<input id="clientDataJSON" type="hidden" name="clientDataJSON">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<input type="text" class="form-control" id="name" name="name">
|
||||
<div class="help-block">
|
||||
Give this credential a descriptive name. Example: "My primary Yubikey"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
id="add-credential-submit"
|
||||
class="btn btn-default"
|
||||
type="submit"
|
||||
name=""
|
||||
value="Save Credential" disabled>
|
||||
</form>
|
||||
|
||||
{% 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 %}
|
@ -59,6 +59,35 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body settings-block">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<h2>Two Factor Authentication</h2>
|
||||
{% if profile.user.credentials.exists %}
|
||||
<table class="table">
|
||||
{% for credential in profile.user.credentials.all %}
|
||||
<tr>
|
||||
<td>{{ credential.code }}</td>
|
||||
<td>{{ credential.name|default:"unnamed" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{% else %}
|
||||
<p>
|
||||
Your account has no registered two factor authentication
|
||||
methods.
|
||||
</p>
|
||||
{% endif %}
|
||||
<a
|
||||
href="{% url 'hc-add-credential' %}"
|
||||
class="btn btn-default pull-right">Add 2FA Credential</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="panel panel-{{ my_projects_status }}">
|
||||
<div class="panel-body settings-block">
|
||||
{% csrf_token %}
|
||||
|
5
templates/accounts/success.html
Normal file
5
templates/accounts/success.html
Normal file
@ -0,0 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Success</h1>
|
||||
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user