overhauled auth system
This commit is contained in:
parent
c744143431
commit
0f6fa87668
130
auth.py
130
auth.py
|
@ -3,6 +3,9 @@
|
||||||
Contains authentication methods for the app.
|
Contains authentication methods for the app.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import sqlite3
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
from fido2.client import ClientData
|
from fido2.client import ClientData
|
||||||
|
@ -10,12 +13,13 @@ from fido2.server import Fido2Server, RelyingParty
|
||||||
from fido2.ctap2 import AttestationObject, AuthenticatorData, \
|
from fido2.ctap2 import AttestationObject, AuthenticatorData, \
|
||||||
AttestedCredentialData
|
AttestedCredentialData
|
||||||
from fido2 import cbor
|
from fido2 import cbor
|
||||||
from flask import Blueprint, session, render_template, request, abort, \
|
from flask import Blueprint, session, render_template, request, \
|
||||||
redirect, url_for, g
|
redirect, url_for, jsonify
|
||||||
from passlib.hash import argon2
|
from passlib.hash import argon2
|
||||||
|
|
||||||
import config
|
|
||||||
import db
|
import db
|
||||||
|
import config
|
||||||
|
from tools import make_error
|
||||||
|
|
||||||
auth_views = Blueprint("auth_views", __name__)
|
auth_views = Blueprint("auth_views", __name__)
|
||||||
|
|
||||||
|
@ -37,13 +41,22 @@ def auth_required(func):
|
||||||
data = db.get_user(username)
|
data = db.get_user(username)
|
||||||
if not data:
|
if not data:
|
||||||
session.pop('username')
|
session.pop('username')
|
||||||
|
session.pop('token')
|
||||||
return redirect(url_for('auth_views.login'))
|
return redirect(url_for('auth_views.login'))
|
||||||
token_hash = data[2]
|
user_id = data[0]
|
||||||
if not token_hash:
|
|
||||||
|
tokens = db.get_tokens(user_id)
|
||||||
|
if not tokens:
|
||||||
return redirect(url_for('auth_views.login'))
|
return redirect(url_for('auth_views.login'))
|
||||||
|
|
||||||
if argon2.verify(token, token_hash):
|
for token_line in tokens:
|
||||||
return func(*args, **kwargs)
|
token_hash = token_line[2]
|
||||||
|
date_expired = token_line[4]
|
||||||
|
if int(time.time()) >= date_expired:
|
||||||
|
continue
|
||||||
|
if argon2.verify(token, token_hash):
|
||||||
|
db.refresh_token(token_line[0])
|
||||||
|
return func(*args, **kwargs)
|
||||||
else:
|
else:
|
||||||
session.pop('token')
|
session.pop('token')
|
||||||
return redirect(url_for('auth_views.login'))
|
return redirect(url_for('auth_views.login'))
|
||||||
|
@ -86,10 +99,10 @@ def register_complete():
|
||||||
username = session.get('username')
|
username = session.get('username')
|
||||||
if not username:
|
if not username:
|
||||||
return 'invalid'
|
return 'invalid'
|
||||||
|
session.pop('username')
|
||||||
|
|
||||||
data = db.get_user(username)
|
data = db.get_user(username)
|
||||||
if not data:
|
if not data:
|
||||||
session.pop('username')
|
|
||||||
return 'invalid'
|
return 'invalid'
|
||||||
user_id = data[0]
|
user_id = data[0]
|
||||||
|
|
||||||
|
@ -97,30 +110,34 @@ def register_complete():
|
||||||
client_data = ClientData(data['clientDataJSON'])
|
client_data = ClientData(data['clientDataJSON'])
|
||||||
att_obj = AttestationObject(data['attestationObject'])
|
att_obj = AttestationObject(data['attestationObject'])
|
||||||
|
|
||||||
|
nick = data['security_key_nick']
|
||||||
|
try:
|
||||||
|
assert 64 >= len(nick) >= 1
|
||||||
|
except AssertionError:
|
||||||
|
return make_error(400, "security key nick too long/short")
|
||||||
|
|
||||||
auth_data = server.register_complete(
|
auth_data = server.register_complete(
|
||||||
session.pop('state'),
|
session.pop('state'),
|
||||||
client_data,
|
client_data,
|
||||||
att_obj
|
att_obj
|
||||||
)
|
)
|
||||||
|
|
||||||
db.set_credential(user_id, auth_data.credential_data)
|
db.set_credential(user_id, nick, auth_data.credential_data)
|
||||||
return cbor.encode({'status': 'OK'})
|
return jsonify(ok=True)
|
||||||
|
|
||||||
|
|
||||||
@auth_views.route('/api/authenticate/begin', methods=['POST'])
|
@auth_views.route('/api/authenticate/begin', methods=['POST'])
|
||||||
def authenticate_begin():
|
def authenticate_begin():
|
||||||
username = session.get('username')
|
data = cbor.decode(request.get_data())
|
||||||
if not username:
|
username = data.get('username')
|
||||||
return 'invalid'
|
user = db.get_user(username)
|
||||||
|
if not user:
|
||||||
data = db.get_user(session['username'])
|
return make_error(404, "username not found")
|
||||||
if not data:
|
session['username'] = username
|
||||||
session.pop('username')
|
user_id = user[0]
|
||||||
return 'invalid'
|
|
||||||
user_id = data[0]
|
|
||||||
|
|
||||||
credentials = db.get_credentials(user_id)
|
credentials = db.get_credentials(user_id)
|
||||||
credentials = [AttestedCredentialData(cd[2]) for cd in credentials]
|
credentials = [AttestedCredentialData(cd[3]) for cd in credentials]
|
||||||
|
|
||||||
auth_data, state = server.authenticate_begin(credentials)
|
auth_data, state = server.authenticate_begin(credentials)
|
||||||
session['state'] = state
|
session['state'] = state
|
||||||
|
@ -130,17 +147,14 @@ def authenticate_begin():
|
||||||
@auth_views.route('/api/authenticate/complete', methods=['POST'])
|
@auth_views.route('/api/authenticate/complete', methods=['POST'])
|
||||||
def authenticate_complete():
|
def authenticate_complete():
|
||||||
username = session.get('username')
|
username = session.get('username')
|
||||||
if not username:
|
user = db.get_user(username)
|
||||||
return 'invalid'
|
if not user:
|
||||||
|
|
||||||
data = db.get_user(session['username'])
|
|
||||||
if not data:
|
|
||||||
session.pop('username')
|
session.pop('username')
|
||||||
return 'invalid'
|
return make_error(404, "username not found")
|
||||||
user_id = data[0]
|
user_id = user[0]
|
||||||
|
|
||||||
credentials = db.get_credentials(user_id)
|
credentials = db.get_credentials(user_id)
|
||||||
credentials = [AttestedCredentialData(cd[2]) for cd in credentials]
|
credentials = [AttestedCredentialData(cd[3]) for cd in credentials]
|
||||||
|
|
||||||
data = cbor.decode(request.get_data())
|
data = cbor.decode(request.get_data())
|
||||||
credential_id = data['credentialId']
|
credential_id = data['credentialId']
|
||||||
|
@ -159,10 +173,10 @@ def authenticate_complete():
|
||||||
|
|
||||||
token = os.urandom(32)
|
token = os.urandom(32)
|
||||||
token_hash = argon2.hash(token)
|
token_hash = argon2.hash(token)
|
||||||
db.set_user_token(username, token_hash)
|
db.set_token(user_id, token_hash)
|
||||||
session['token'] = token
|
session['token'] = token
|
||||||
|
|
||||||
return cbor.encode({'status': 'OK'})
|
return jsonify(ok=True)
|
||||||
|
|
||||||
|
|
||||||
@auth_views.route('/register', methods=['GET', 'POST'])
|
@auth_views.route('/register', methods=['GET', 'POST'])
|
||||||
|
@ -175,52 +189,44 @@ def register():
|
||||||
|
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
params = {
|
params = {
|
||||||
'title': 'Register',
|
|
||||||
'heading': 'Register an account',
|
|
||||||
'form_url': url_for('auth_views.register'),
|
'form_url': url_for('auth_views.register'),
|
||||||
|
'url_prefix': config.url_prefix,
|
||||||
}
|
}
|
||||||
return render_template('auth.html', **params)
|
return render_template('register.html', **params)
|
||||||
|
|
||||||
username = request.form.get('username')
|
username = request.form.get('username')
|
||||||
|
email = request.form.get('email')
|
||||||
if len(username) > 64:
|
|
||||||
return "username too long"
|
|
||||||
elif len(username) < 3:
|
|
||||||
return "username too short"
|
|
||||||
|
|
||||||
db.set_user(username) # TODO: handle username error
|
try:
|
||||||
|
assert 64 >= len(username) >= 3
|
||||||
|
except AssertionError:
|
||||||
|
return make_error(400, "username too long/short")
|
||||||
|
|
||||||
|
try:
|
||||||
|
assert 100 >= len(email)
|
||||||
|
except AssertionError:
|
||||||
|
return "email too long"
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_id = db.set_user(username, email)
|
||||||
|
except sqlite3.IntegrityError as e:
|
||||||
|
field = re.search(r'user\.(.*)', str(e)).group(1)
|
||||||
|
return make_error(400, f"{field} already exists")
|
||||||
session['username'] = username
|
session['username'] = username
|
||||||
|
session['user_id'] = user_id
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
'title': 'Register',
|
'url_prefix': config.url_prefix,
|
||||||
'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)
|
return render_template('register_key.html', **params)
|
||||||
|
|
||||||
|
|
||||||
@auth_views.route('/login', methods=['GET', 'POST'])
|
@auth_views.route('/login')
|
||||||
def login():
|
def login():
|
||||||
"""
|
"""
|
||||||
Login page.
|
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 = {
|
params = {
|
||||||
'title': 'Login',
|
'url_prefix': config.url_prefix,
|
||||||
'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)
|
return render_template('login.html', **params)
|
||||||
|
|
95
db.py
95
db.py
|
@ -2,6 +2,7 @@
|
||||||
"""
|
"""
|
||||||
A module for interacting with Juice's database.
|
A module for interacting with Juice's database.
|
||||||
"""
|
"""
|
||||||
|
import time
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
@ -19,19 +20,29 @@ def init_db():
|
||||||
try:
|
try:
|
||||||
cur.execute("SELECT * FROM user LIMIT 1").fetchone()
|
cur.execute("SELECT * FROM user LIMIT 1").fetchone()
|
||||||
cur.execute("SELECT * FROM credential LIMIT 1").fetchone()
|
cur.execute("SELECT * FROM credential LIMIT 1").fetchone()
|
||||||
|
cur.execute("SELECT * FROM token LIMIT 1").fetchone()
|
||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
cur.execute("CREATE TABLE user("
|
cur.execute("CREATE TABLE user("
|
||||||
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
"id INTEGER PRIMARY KEY, "
|
||||||
"name TEXT UNIQUE, "
|
"username TEXT UNIQUE, "
|
||||||
"token_hash TEXT"
|
"email TEXT UNIQUE"
|
||||||
")"
|
")"
|
||||||
)
|
)
|
||||||
cur.execute("CREATE TABLE credential("
|
cur.execute("CREATE TABLE credential("
|
||||||
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
"id INTEGER PRIMARY KEY, "
|
||||||
"user_id INTEGER REFERENCES user(id) ON UPDATE CASCADE, "
|
"user_id INTEGER REFERENCES user(id) ON UPDATE CASCADE, "
|
||||||
|
"nick TEXT, "
|
||||||
"credential BLOB"
|
"credential BLOB"
|
||||||
")"
|
")"
|
||||||
)
|
)
|
||||||
|
cur.execute("CREATE TABLE token("
|
||||||
|
"id INTEGER PRIMARY KEY, "
|
||||||
|
"user_id INTEGER REFERENCES user(id) ON UPDATE CASCADE, "
|
||||||
|
"token_hash TEXT, "
|
||||||
|
"date_issued INTEGER, "
|
||||||
|
"date_expired INTEGER"
|
||||||
|
")"
|
||||||
|
)
|
||||||
con.commit()
|
con.commit()
|
||||||
con.close()
|
con.close()
|
||||||
init_db()
|
init_db()
|
||||||
|
@ -50,26 +61,15 @@ def db_execute(*args, **kwargs):
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
def set_user(username):
|
def set_user(username, email):
|
||||||
"""
|
"""
|
||||||
Adds a new user.
|
Adds a new user.
|
||||||
"""
|
"""
|
||||||
db_execute(
|
user_id = db_execute(
|
||||||
"INSERT INTO user(name) VALUES (?)",
|
"INSERT INTO user(username, email) VALUES (?, ?)",
|
||||||
(username,)
|
(username, email)
|
||||||
)
|
).lastrowid
|
||||||
return True
|
return user_id
|
||||||
|
|
||||||
|
|
||||||
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):
|
def get_user(username):
|
||||||
|
@ -77,21 +77,60 @@ def get_user(username):
|
||||||
Returns a user entry.
|
Returns a user entry.
|
||||||
"""
|
"""
|
||||||
data = db_execute(
|
data = db_execute(
|
||||||
"SELECT * FROM user WHERE name = ?",
|
"SELECT * FROM user WHERE username = ?",
|
||||||
(username,)
|
(username,)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def set_credential(user_id, credential):
|
def set_token(user_id, token_hash):
|
||||||
|
"""
|
||||||
|
Sets a user's token hash.
|
||||||
|
"""
|
||||||
|
date_issued = int(time.time())
|
||||||
|
date_expired = date_issued + 30*24*60*60
|
||||||
|
token_id = db_execute(
|
||||||
|
"INSERT INTO "
|
||||||
|
"token(user_id, token_hash, date_issued, date_expired) "
|
||||||
|
"VALUES (?,?,?,?)",
|
||||||
|
(user_id, token_hash, date_issued, date_expired)
|
||||||
|
).lastrowid
|
||||||
|
return token_id
|
||||||
|
|
||||||
|
|
||||||
|
def get_tokens(user_id):
|
||||||
|
"""
|
||||||
|
Returns all tokens assigned to a user.
|
||||||
|
"""
|
||||||
|
data = db_execute(
|
||||||
|
"SELECT * FROM token WHERE user_id = ?",
|
||||||
|
(user_id,)
|
||||||
|
).fetchall()
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_token(token_id):
|
||||||
|
"""
|
||||||
|
Extends a token's expiration date.
|
||||||
|
"""
|
||||||
|
new_date_expired = int(time.time()) + 30*24*60*60
|
||||||
|
db_execute(
|
||||||
|
"UPDATE token SET date_expired = ? WHERE id = ?",
|
||||||
|
(new_date_expired, token_id)
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def set_credential(user_id, nick, credential):
|
||||||
"""
|
"""
|
||||||
Adds a credential to the database.
|
Adds a credential to the database.
|
||||||
"""
|
"""
|
||||||
db_execute(
|
cred_id = db_execute(
|
||||||
"INSERT INTO credential(user_id, credential) VALUES (?, ?)",
|
"INSERT INTO credential(user_id, nick, credential) "
|
||||||
(user_id, credential)
|
"VALUES (?, ?, ?)",
|
||||||
)
|
(user_id, nick, credential)
|
||||||
return True
|
).lastrowid
|
||||||
|
return cred_id
|
||||||
|
|
||||||
|
|
||||||
def get_credentials(user_id):
|
def get_credentials(user_id):
|
||||||
|
|
18
juice.py
18
juice.py
|
@ -11,9 +11,10 @@ import requests
|
||||||
from flask import Flask, render_template, request, abort, jsonify
|
from flask import Flask, render_template, request, abort, jsonify
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
|
|
||||||
import config
|
|
||||||
import auth
|
import auth
|
||||||
|
import config
|
||||||
from auth import auth_required
|
from auth import auth_required
|
||||||
|
from tools import make_error
|
||||||
|
|
||||||
class RelayDevice:
|
class RelayDevice:
|
||||||
"""
|
"""
|
||||||
|
@ -225,6 +226,7 @@ def edit():
|
||||||
|
|
||||||
|
|
||||||
@app_views.route('/new_device')
|
@app_views.route('/new_device')
|
||||||
|
@auth_required
|
||||||
def new_device():
|
def new_device():
|
||||||
"""
|
"""
|
||||||
Allows adding a new device. Accepts device_type parameter, returns
|
Allows adding a new device. Accepts device_type parameter, returns
|
||||||
|
@ -255,6 +257,7 @@ def new_device():
|
||||||
|
|
||||||
|
|
||||||
@app_views.route('/lock_device')
|
@app_views.route('/lock_device')
|
||||||
|
@auth_required
|
||||||
def lock_device():
|
def lock_device():
|
||||||
"""
|
"""
|
||||||
Locks or unlocks a device to prevent or allow editing it's fields.
|
Locks or unlocks a device to prevent or allow editing it's fields.
|
||||||
|
@ -278,6 +281,7 @@ def lock_device():
|
||||||
|
|
||||||
|
|
||||||
@app_views.route('/delete')
|
@app_views.route('/delete')
|
||||||
|
@auth_required
|
||||||
def delete():
|
def delete():
|
||||||
"""
|
"""
|
||||||
Deletes a device.
|
Deletes a device.
|
||||||
|
@ -296,18 +300,10 @@ def delete():
|
||||||
return jsonify(True)
|
return jsonify(True)
|
||||||
|
|
||||||
|
|
||||||
def make_error(code, message):
|
|
||||||
"""
|
|
||||||
Returns a JSON error.
|
|
||||||
"""
|
|
||||||
res = jsonify(ok=False, status=code, message=message)
|
|
||||||
res.status_code = code
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.register_blueprint(app_views, url_prefix=config.url_prefix)
|
app.register_blueprint(app_views, url_prefix=config.url_prefix)
|
||||||
app.register_blueprint(auth.auth_views, url_prefix=config.url_prefix)
|
app.register_blueprint(auth.auth_views, url_prefix=config.url_prefix)
|
||||||
|
app.jinja_env.undefined = "StrictUndefined"
|
||||||
if os.path.isfile('secret_key'):
|
if os.path.isfile('secret_key'):
|
||||||
with open('secret_key', 'rb') as file:
|
with open('secret_key', 'rb') as file:
|
||||||
app.secret_key = file.read()
|
app.secret_key = file.read()
|
||||||
|
@ -320,4 +316,4 @@ network = init_network()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', port=5300)
|
app.run(host='0.0.0.0', port=5300, debug=True)
|
||||||
|
|
60
static/juice-auth.js
Normal file
60
static/juice-auth.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
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),
|
||||||
|
"security_key_nick": document.querySelector('#security_key_nick').value,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}).then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
}).then(function(json) {
|
||||||
|
console.log(json);
|
||||||
|
if (!json.ok) { throw new Error('HTTP error, status = ' + json.status + ', message = ' + json.message); }
|
||||||
|
window.location = url_prefix + '/login';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function login() {
|
||||||
|
fetch(url_prefix + '/api/authenticate/begin', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/cbor'},
|
||||||
|
body: CBOR.encode({
|
||||||
|
"username": document.querySelector('#username').value,
|
||||||
|
})
|
||||||
|
}).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.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)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}, function(reason) {
|
||||||
|
console.log('navigator.credentials.get() failed for the following reason: ' + reason);
|
||||||
|
}).then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
}).then(function(json) {
|
||||||
|
console.log(json);
|
||||||
|
if (!json.ok) { throw new Error('HTTP error, status = ' + json.status + ', message = ' + json.message); }
|
||||||
|
window.location = url_prefix + '/';
|
||||||
|
});
|
||||||
|
}
|
|
@ -24,6 +24,11 @@ nav span {
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nav a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
nav span:hover {
|
nav span:hover {
|
||||||
background-color: lightgray;
|
background-color: lightgray;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
<!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>
|
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
<!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>
|
|
||||||
|
|
|
@ -9,9 +9,10 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
|
<h1>Juice</h1>
|
||||||
<nav>
|
<nav>
|
||||||
<span onclick="new_device()" title="Add new device"><span class="font-awesome"></span></span>
|
<span title="Add new device" onclick="new_device()"><span class="font-awesome"></span></span>
|
||||||
<span title="Register a new authenticator">Register</span>
|
<span title="Manage authenticators"><a href="./manage">Manage</a></span>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
|
|
21
templates/login.html
Normal file
21
templates/login.html
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Juice - Login</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/juice.css">
|
||||||
|
<script type="text/javascript" src="/static/cbor.js"></script>
|
||||||
|
<script type="text/javascript" src="/static/juice-auth.js"></script>
|
||||||
|
<script>url_prefix = '{{ url_prefix }}';</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<div id="devices">
|
||||||
|
<div class="device">
|
||||||
|
<h1>Login</h1>
|
||||||
|
<p><label for="username">Username</label> <input id="username" type="text" minlength="3" maxlength="64" required>
|
||||||
|
<p><input type="button" value="Login" onclick="login()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
31
templates/register.html
Normal file
31
templates/register.html
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Juice - Register</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/juice.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<div id="devices">
|
||||||
|
<div class="device">
|
||||||
|
<h1>Register</h1>
|
||||||
|
<form method="post" action="{{ form_url }}">
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Username</td><td><input name="username" type="text" minlength="3" maxlength="64" required></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Email</td><td><input name="email" type="email"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><input type="submit" value="Submit"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
22
templates/register_key.html
Normal file
22
templates/register_key.html
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Juice - Register Security Key</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/juice.css">
|
||||||
|
<script type="text/javascript" src="/static/cbor.js"></script>
|
||||||
|
<script type="text/javascript" src="/static/juice-auth.js"></script>
|
||||||
|
<script>url_prefix = '{{ url_prefix }}';</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<div id="devices">
|
||||||
|
<div class="device">
|
||||||
|
<h1>Register Security Key</h1>
|
||||||
|
<p>Upon clicking submit, your security key will begin flashing. Have it ready.
|
||||||
|
<p><label for="security_key_nick">Security key nick</label> <input id="security_key_nick" type="text" minlength="1" maxlength="64" required>
|
||||||
|
<p><input type="button" value="Submit" onclick="register()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user