added authentication

This commit is contained in:
iou1name 2019-06-16 18:22:47 -04:00
parent bc0e15f706
commit 34927741a9
12 changed files with 913 additions and 30 deletions

3
.gitignore vendored
View File

@ -3,3 +3,6 @@ __pycache__/
*.swo
devices.json
config.py
secret_key
cred
juice.db

View File

@ -3,11 +3,11 @@ A hub for controlling IOT devices.
## Requirements
Python 3.6+
Python packages: `flask gunicorn requests`
Python packages: `flask gunicorn requests passlib argon2_cffi`
## Install
1. Get on the floor
2. Walk the dinosaur
## Usage
`gunicorn -b localhost:5300 -e SCRIPT_NAME=/juice juice:app`
`gunicorn -b localhost:5300 juice:app`

226
auth.py Normal file
View File

@ -0,0 +1,226 @@
#!/usr/bin/env python3
"""
Contains authentication methods for the app.
"""
import os
import functools
from fido2.client import ClientData
from fido2.server import Fido2Server, RelyingParty
from fido2.ctap2 import AttestationObject, AuthenticatorData, \
AttestedCredentialData
from fido2 import cbor
from flask import Blueprint, session, render_template, request, abort, \
redirect, url_for, g
from passlib.hash import argon2
import config
import db
auth_views = Blueprint("auth_views", __name__)
rp = RelyingParty('steelbea.me', 'Juice')
server = Fido2Server(rp)
def auth_required(func):
"""
Wrapper for views which should be protected by authentication.
"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
username = session.get('username')
token = session.get('token')
if not username or not token:
return redirect(url_for('auth_views.login'))
data = db.get_user(username)
if not data:
session.pop('username')
return redirect(url_for('auth_views.login'))
token_hash = data[2]
if not token_hash:
return redirect(url_for('auth_views.login'))
if argon2.verify(token, token_hash):
return func(*args, **kwargs)
else:
session.pop('token')
return redirect(url_for('auth_views.login'))
return wrapper
@auth_views.route('/api/register/begin', methods=['POST'])
def register_begin():
if not config.registration_open:
return "Registration is closed."
username = session.get('username')
if not username:
return 'invalid'
data = db.get_user(username)
if not data:
session.pop('username')
return 'invalid'
user_id = data[0]
exist_cred = db.get_credentials(user_id)
exist_cred = [AttestedCredentialData(cd[2]) for cd in exist_cred]
registration_data, state = server.register_begin({
'id': str(user_id).encode('utf8'),
'name': username,
'displayName': username,
}, exist_cred, user_verification='discouraged')
session['state'] = state
return cbor.encode(registration_data)
@auth_views.route('/api/register/complete', methods=['POST'])
def register_complete():
if not config.registration_open:
return "Registration is closed."
username = session.get('username')
if not username:
return 'invalid'
data = db.get_user(username)
if not data:
session.pop('username')
return 'invalid'
user_id = data[0]
data = cbor.decode(request.get_data())
client_data = ClientData(data['clientDataJSON'])
att_obj = AttestationObject(data['attestationObject'])
auth_data = server.register_complete(
session.pop('state'),
client_data,
att_obj
)
db.set_credential(user_id, auth_data.credential_data)
return cbor.encode({'status': 'OK'})
@auth_views.route('/api/authenticate/begin', methods=['POST'])
def authenticate_begin():
username = session.get('username')
if not username:
return 'invalid'
data = db.get_user(session['username'])
if not data:
session.pop('username')
return 'invalid'
user_id = data[0]
credentials = db.get_credentials(user_id)
credentials = [AttestedCredentialData(cd[2]) for cd in credentials]
auth_data, state = server.authenticate_begin(credentials)
session['state'] = state
return cbor.encode(auth_data)
@auth_views.route('/api/authenticate/complete', methods=['POST'])
def authenticate_complete():
username = session.get('username')
if not username:
return 'invalid'
data = db.get_user(session['username'])
if not data:
session.pop('username')
return 'invalid'
user_id = data[0]
credentials = db.get_credentials(user_id)
credentials = [AttestedCredentialData(cd[2]) for cd in credentials]
data = cbor.decode(request.get_data())
credential_id = data['credentialId']
client_data = ClientData(data['clientDataJSON'])
auth_data = AuthenticatorData(data['authenticatorData'])
signature = data['signature']
server.authenticate_complete(
session.pop('state'),
credentials,
credential_id,
client_data,
auth_data,
signature
)
token = os.urandom(32)
token_hash = argon2.hash(token)
db.set_user_token(username, token_hash)
session['token'] = token
return cbor.encode({'status': 'OK'})
@auth_views.route('/register', methods=['GET', 'POST'])
def register():
"""
Registration page.
"""
if not config.registration_open:
return "Registration is closed."
if request.method == 'GET':
params = {
'title': 'Register',
'heading': 'Register an account',
'form_url': url_for('auth_views.register'),
}
return render_template('auth.html', **params)
username = request.form.get('username')
if len(username) > 64:
return "username too long"
elif len(username) < 3:
return "username too short"
db.set_user(username) # TODO: handle username error
session['username'] = username
params = {
'title': 'Register',
'heading': 'Register your authenticator',
'api_begin': url_for('auth_views.register_begin'),
'api_complete': url_for('auth_views.register_complete'),
}
return render_template('auth_fido.html', **params)
@auth_views.route('/login', methods=['GET', 'POST'])
def login():
"""
Login page.
"""
if request.method == 'GET':
params = {
'title': 'Login',
'heading': 'Login',
'form_url': url_for('auth_views.login'),
}
return render_template('auth.html', **params)
username = request.form.get('username')
session['username'] = username
params = {
'title': 'Login',
'heading': 'Login with your authenticator',
'api_begin': url_for('auth_views.authenticate_begin'),
'api_complete': url_for('auth_views.authenticate_complete'),
}
return render_template('auth_fido.html', **params)

View File

@ -3,5 +3,7 @@
Configuration settings for the Juice IOT hub server.
`url_prefix` is the root path you wish app to reside at
eg. https://example.com/juice.
`registration_open` whether or not new accounts may be registered.
"""
url_prefix = '/juice'
registration_open = True

105
db.py Normal file
View File

@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""
A module for interacting with Juice's database.
"""
import sqlite3
import threading
DB_LOCK = threading.Lock()
def init_db():
"""
Checks to see if the database is initialized yet or not. If not,
the appropriate tables are created.
"""
con = sqlite3.connect('juice.db')
cur = con.cursor()
try:
cur.execute("SELECT * FROM user LIMIT 1").fetchone()
cur.execute("SELECT * FROM credential LIMIT 1").fetchone()
except sqlite3.OperationalError:
cur.execute("CREATE TABLE user("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"name TEXT UNIQUE, "
"token_hash TEXT"
")"
)
cur.execute("CREATE TABLE credential("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"user_id INTEGER REFERENCES user(id) ON UPDATE CASCADE, "
"credential BLOB"
")"
)
con.commit()
con.close()
init_db()
def db_execute(*args, **kwargs):
"""
Opens a connection to the app's database and executes the SQL
statements passed to this function.
"""
with sqlite3.connect('juice.db') as con:
DB_LOCK.acquire()
cur = con.cursor()
res = cur.execute(*args, **kwargs)
DB_LOCK.release()
return res
def set_user(username):
"""
Adds a new user.
"""
db_execute(
"INSERT INTO user(name) VALUES (?)",
(username,)
)
return True
def set_user_token(username, token_hash):
"""
Sets a user's token hash.
"""
db_execute(
"UPDATE user SET token_hash = ? WHERE name = ?",
(token_hash, username)
)
return True
def get_user(username):
"""
Returns a user entry.
"""
data = db_execute(
"SELECT * FROM user WHERE name = ?",
(username,)
).fetchone()
return data
def set_credential(user_id, credential):
"""
Adds a credential to the database.
"""
db_execute(
"INSERT INTO credential(user_id, credential) VALUES (?, ?)",
(user_id, credential)
)
return True
def get_credentials(user_id):
"""
Returns all credentials registered to a user.
"""
data = db_execute(
"SELECT * FROM credential WHERE user_id = ?",
(user_id,)
).fetchall()
return data

View File

@ -12,7 +12,8 @@ from flask import Flask, render_template, request, abort, jsonify
from flask import Blueprint
import config
import auth
from auth import auth_required
class RelayDevice:
"""
@ -134,6 +135,7 @@ def save_network(filepath="devices.json"):
app_views = Blueprint('app_views', __name__)
@app_views.route('/')
@auth_required
def index():
"""
The index page.
@ -142,6 +144,7 @@ def index():
@app_views.route('/toggle')
@auth_required
def toggle():
"""
Toggles the state of a RelayDevice.
@ -161,6 +164,7 @@ def toggle():
return res
@app_views.route('/edit')
@auth_required
def edit():
"""
Edits the text of a particular field.
@ -212,8 +216,15 @@ def make_error(code, message):
app = Flask(__name__)
app.register_blueprint(app_views, url_prefix=config.url_prefix)
app.secret_key = os.urandom(32)
app.config['APPLICATION_ROOT'] = config.url_prefix
app.register_blueprint(auth.auth_views, url_prefix=config.url_prefix)
if os.path.isfile('secret_key'):
with open('secret_key', 'rb') as file:
app.secret_key = file.read()
else:
secret_key = os.urandom(32)
app.secret_key = secret_key
with open('secret_key', 'wb') as file:
file.write(secret_key)
network = init_network()

406
static/cbor.js Normal file
View 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);

View File

@ -1,11 +1,14 @@
body {
padding: 5%;
display: flex;
justify-content: center;
font-family: Helvetica,sans-serif;
background-color: lightblue;
}
#container {
display: flex;
justify-content: center;
}
.device {
border: 2px solid darkgray;
border-radius: 0.5em;

View File

@ -74,3 +74,59 @@ function save_field(field) {
field.children[1].replaceWith(edit);
});
}
function register() {
fetch(url_prefix + '/api/register/begin', {
method: 'POST',
}).then(function(response) {
if(!response.ok) { throw new Error('Error getting registration data!'); }
return response.arrayBuffer();
}).then(CBOR.decode).then(function(options) {
return navigator.credentials.create(options);
}).then(function(attestation) {
return fetch(url_prefix + '/api/register/complete', {
method: 'POST',
headers: {'Content-Type': 'application/cbor'},
body: CBOR.encode({
"attestationObject": new Uint8Array(attestation.response.attestationObject),
"clientDataJSON": new Uint8Array(attestation.response.clientDataJSON),
})
});
}).then(function(response) {
let stat = response.ok ? 'successful' : 'unsuccessful';
alert('Registration ' + stat + ' More details in server log...');
}, function(reason) {
alert(reason);
}).then(function() {
window.location = url_prefix;
});
}
function login() {
fetch(url_prefix + '/api/authenticate/begin', {
method: 'POST',
}).then(function(response) {
if(!response.ok) { throw new Error('No credential available to authenticate!'); }
return response.arrayBuffer();
}).then(CBOR.decode).then(function(options) {
return navigator.credentials.get(options);
}).then(function(assertion) {
return fetch(url_prefix + '/api/authenticate/complete', {
method: 'POST',
headers: {'Content-Type': 'application/cbor'},
body: CBOR.encode({
"credentialId": new Uint8Array(assertion.rawId),
"authenticatorData": new Uint8Array(assertion.response.authenticatorData),
"clientDataJSON": new Uint8Array(assertion.response.clientDataJSON),
"signature": new Uint8Array(assertion.response.signature)
})
})
}).then(function(response) {
let stat = response.ok ? 'successful' : 'unsuccessful';
alert('Authentication ' + stat + ' More details in server log...');
}, function(reason) {
alert(reason);
}).then(function() {
window.location = url_prefix;
});
}

16
templates/auth.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<title>Juice - {{ title }}</title>
<link rel="stylesheet" type="text/css" href="/static/juice.css">
<script type="text/javascript" src="/static/cbor.js"></script>
</head>
<body>
<h1>{{ heading }}</h1>
<form method="post" action="{{ form_url }}">
Username: <input name="username" maxlength="64" required><br>
<input type="submit" value="Submit">
</form>
</body>
</html>

53
templates/auth_fido.html Normal file
View File

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html>
<head>
<title>Juice - {{ title }}</title>
<link rel="stylesheet" type="text/css" href="/static/juice.css">
<script type="text/javascript" src="/static/cbor.js"></script>
</head>
<body>
<h1>{{ heading }}</h1>
<p>Touch your authenticactor device now...
<script>
fetch('{{ api_begin }}', {
method: 'POST',
}).then(function(response) {
if(!response.ok) { throw new Error('Error getting registration data!'); }
return response.arrayBuffer();
}).then(CBOR.decode).then(function(options) {
return navigator.credentials.{% if title == 'Register' %}create{% else %}get{% endif %}(options);
{% if title == 'Register' %}
}).then(function(attestation) {
return fetch('{{ api_complete }}', {
method: 'POST',
headers: {'Content-Type': 'application/cbor'},
body: CBOR.encode({
"attestationObject": new Uint8Array(attestation.response.attestationObject),
"clientDataJSON": new Uint8Array(attestation.response.clientDataJSON),
})
});
{% else %}
}).then(function(assertion) {
return fetch('{{ api_complete }}', {
method: 'POST',
headers: {'Content-Type': 'application/cbor'},
body: CBOR.encode({
"credentialId": new Uint8Array(assertion.rawId),
"authenticatorData": new Uint8Array(assertion.response.authenticatorData),
"clientDataJSON": new Uint8Array(assertion.response.clientDataJSON),
"signature": new Uint8Array(assertion.response.signature)
})
});
{% endif %}
}).then(function(response) {
let stat = response.ok ? 'successful' : 'unsuccessful';
alert('Registration ' + stat + ' More details in server log...');
}, function(reason) {
alert(reason);
}).then(function() {
window.location = '{{ url_for("app_views.index") }}';
});
</script>
</body>
</html>

View File

@ -6,6 +6,7 @@
<script type="text/javascript" src="/static/juice.js"></script>
</head>
<body>
<div id="container">
{% for dev_type, devices in network.items() %}
{% for device in devices %}
<div class="device {{ dev_type }}" id="{{ device.id }}">
@ -32,5 +33,6 @@
</div>
{% endfor %}
{% endfor %}
</div>
</body>
</html>