added /manage page

This commit is contained in:
iou1name 2019-06-23 14:01:03 -04:00
parent 0f6fa87668
commit b15e0be464
6 changed files with 262 additions and 23 deletions

109
auth.py
View File

@ -7,6 +7,7 @@ import re
import time import time
import sqlite3 import sqlite3
import functools import functools
from datetime import datetime
from fido2.client import ClientData from fido2.client import ClientData
from fido2.server import Fido2Server, RelyingParty from fido2.server import Fido2Server, RelyingParty
@ -16,6 +17,7 @@ from fido2 import cbor
from flask import Blueprint, session, render_template, request, \ from flask import Blueprint, session, render_template, request, \
redirect, url_for, jsonify redirect, url_for, jsonify
from passlib.hash import argon2 from passlib.hash import argon2
from werkzeug.useragents import UserAgent
import db import db
import config import config
@ -38,24 +40,24 @@ def auth_required(func):
if not username or not token: if not username or not token:
return redirect(url_for('auth_views.login')) return redirect(url_for('auth_views.login'))
data = db.get_user(username) user = db.get_user(username)
if not data: if not user:
session.pop('username') session.pop('username')
session.pop('token') session.pop('token')
return redirect(url_for('auth_views.login')) return redirect(url_for('auth_views.login'))
user_id = data[0] user_id = user['id']
tokens = db.get_tokens(user_id) tokens = db.get_tokens(user_id)
if not tokens: if not tokens:
return redirect(url_for('auth_views.login')) return redirect(url_for('auth_views.login'))
for token_line in tokens: for token_line in tokens:
token_hash = token_line[2] token_hash = token_line['token_hash']
date_expired = token_line[4] date_expired = token_line['date_expired']
if int(time.time()) >= date_expired: if int(time.time()) >= date_expired:
continue continue
if argon2.verify(token, token_hash): if argon2.verify(token, token_hash):
db.refresh_token(token_line[0]) db.refresh_token(token_line['id'])
return func(*args, **kwargs) return func(*args, **kwargs)
else: else:
session.pop('token') session.pop('token')
@ -72,14 +74,14 @@ def register_begin():
if not username: if not username:
return 'invalid' return 'invalid'
data = db.get_user(username) user = db.get_user(username)
if not data: if not user:
session.pop('username') session.pop('username')
return 'invalid' return 'invalid'
user_id = data[0] user_id = user['id']
exist_cred = db.get_credentials(user_id) exist_cred = db.get_credentials(user_id)
exist_cred = [AttestedCredentialData(cd[2]) for cd in exist_cred] exist_cred = [AttestedCredentialData(c['credential']) for c in exist_cred]
registration_data, state = server.register_begin({ registration_data, state = server.register_begin({
'id': str(user_id).encode('utf8'), 'id': str(user_id).encode('utf8'),
@ -101,10 +103,10 @@ def register_complete():
return 'invalid' return 'invalid'
session.pop('username') session.pop('username')
data = db.get_user(username) user = db.get_user(username)
if not data: if not user:
return 'invalid' return 'invalid'
user_id = data[0] user_id = user['id']
data = cbor.decode(request.get_data()) data = cbor.decode(request.get_data())
client_data = ClientData(data['clientDataJSON']) client_data = ClientData(data['clientDataJSON'])
@ -134,10 +136,10 @@ def authenticate_begin():
if not user: if not user:
return make_error(404, "username not found") return make_error(404, "username not found")
session['username'] = username session['username'] = username
user_id = user[0] user_id = user['id']
credentials = db.get_credentials(user_id) credentials = db.get_credentials(user_id)
credentials = [AttestedCredentialData(cd[3]) for cd in credentials] credentials =[AttestedCredentialData(c['credential']) for c in credentials]
auth_data, state = server.authenticate_begin(credentials) auth_data, state = server.authenticate_begin(credentials)
session['state'] = state session['state'] = state
@ -151,10 +153,10 @@ def authenticate_complete():
if not user: if not user:
session.pop('username') session.pop('username')
return make_error(404, "username not found") return make_error(404, "username not found")
user_id = user[0] user_id = user['id']
credentials = db.get_credentials(user_id) credentials = db.get_credentials(user_id)
credentials = [AttestedCredentialData(cd[3]) for cd in credentials] credentials =[AttestedCredentialData(c['credential']) for c in credentials]
data = cbor.decode(request.get_data()) data = cbor.decode(request.get_data())
credential_id = data['credentialId'] credential_id = data['credentialId']
@ -173,7 +175,9 @@ def authenticate_complete():
token = os.urandom(32) token = os.urandom(32)
token_hash = argon2.hash(token) token_hash = argon2.hash(token)
db.set_token(user_id, token_hash) user_agent = request.user_agent.string
ip_address = request.headers.get("X-Real-Ip")
db.set_token(user_id, user_agent, ip_address, token_hash)
session['token'] = token session['token'] = token
return jsonify(ok=True) return jsonify(ok=True)
@ -230,3 +234,72 @@ def login():
'url_prefix': config.url_prefix, 'url_prefix': config.url_prefix,
} }
return render_template('login.html', **params) return render_template('login.html', **params)
@auth_views.route('/manage')
@auth_required
def manage():
"""
Allows a user to manage their security keys and tokens.
"""
url_prefix = config.url_prefix
username = session['username']
user_id = db.get_user(username)['id']
credentials = db.get_credentials(user_id)
tokens = db.get_tokens(user_id)
tokens_pretty = []
for token in tokens:
token_pretty = {}
token_pretty['id'] = token['id']
token_pretty['user_agent'] = UserAgent(token['user_agent'])
token_pretty['ip_address'] = token['ip_address']
di = token['date_issued']
di = datetime.utcfromtimestamp(di).strftime('%Y-%m-%d')
token_pretty['date_issued'] = di
de = token['date_expired']
de = datetime.utcfromtimestamp(de).strftime('%Y-%m-%d')
token_pretty['date_expired'] = de
tokens_pretty.append(token_pretty)
return render_template('manage.html', **locals())
@auth_views.route('/delete_key')
@auth_required
def delete_key():
"""
Allows a user to delete a security key credential.
"""
cred_id = request.args.get('key_id')
username = session['username']
user_id = db.get_user(username)['id']
cred = db.get_credential(cred_id)
if not cred:
return make_error(404, "security key not found")
if cred['user_id'] == user_id:
db.delete_credential(cred_id)
return jsonify(ok=True)
else:
return make_error(404, "security key not found")
@auth_views.route('/delete_token')
@auth_required
def delete_token():
"""
Allows a user to delete a token.
"""
token_id = request.args.get('token_id')
username = session['username']
user_id = db.get_user(username)['id']
token = db.get_token(token_id)
if not token:
return make_error(404, "token not found")
if token['user_id'] == user_id:
db.delete_token(token_id)
return jsonify(ok=True)
else:
return make_error(404, "token not found")

57
db.py
View File

@ -38,6 +38,8 @@ def init_db():
cur.execute("CREATE TABLE token(" cur.execute("CREATE TABLE token("
"id INTEGER PRIMARY KEY, " "id INTEGER PRIMARY KEY, "
"user_id INTEGER REFERENCES user(id) ON UPDATE CASCADE, " "user_id INTEGER REFERENCES user(id) ON UPDATE CASCADE, "
"user_agent TEXT, "
"ip_address TEXT, "
"token_hash TEXT, " "token_hash TEXT, "
"date_issued INTEGER, " "date_issued INTEGER, "
"date_expired INTEGER" "date_expired INTEGER"
@ -55,6 +57,7 @@ def db_execute(*args, **kwargs):
""" """
with sqlite3.connect('juice.db') as con: with sqlite3.connect('juice.db') as con:
DB_LOCK.acquire() DB_LOCK.acquire()
con.row_factory = sqlite3.Row
cur = con.cursor() cur = con.cursor()
res = cur.execute(*args, **kwargs) res = cur.execute(*args, **kwargs)
DB_LOCK.release() DB_LOCK.release()
@ -83,7 +86,7 @@ def get_user(username):
return data return data
def set_token(user_id, token_hash): def set_token(user_id, user_agent, ip_address, token_hash):
""" """
Sets a user's token hash. Sets a user's token hash.
""" """
@ -91,13 +94,37 @@ def set_token(user_id, token_hash):
date_expired = date_issued + 30*24*60*60 date_expired = date_issued + 30*24*60*60
token_id = db_execute( token_id = db_execute(
"INSERT INTO " "INSERT INTO "
"token(user_id, token_hash, date_issued, date_expired) " "token("
"VALUES (?,?,?,?)", "user_id, user_agent, ip_address, token_hash, date_issued,date_expired"
(user_id, token_hash, date_issued, date_expired) ") "
"VALUES (?,?,?,?,?,?)",
(user_id, user_agent, ip_address, token_hash, date_issued,date_expired)
).lastrowid ).lastrowid
return token_id return token_id
def get_token(token_id):
"""
Returns the token of the specified id.
"""
data = db_execute(
"SELECT * FROM token WHERE id = ?",
(token_id,)
).fetchone()
return data
def delete_token(token_id):
"""
Deletes the token of the specified id.
"""
db_execute(
"DELETE FROM token WHERE id = ?",
(token_id,)
).fetchone()
return True
def get_tokens(user_id): def get_tokens(user_id):
""" """
Returns all tokens assigned to a user. Returns all tokens assigned to a user.
@ -133,6 +160,28 @@ def set_credential(user_id, nick, credential):
return cred_id return cred_id
def get_credential(cred_id):
"""
Returns the credential of the specified id.
"""
data = db_execute(
"SELECT * FROM credential WHERE id = ?",
(cred_id,)
).fetchone()
return data
def delete_credential(cred_id):
"""
Deletes the credential of the specified id.
"""
db_execute(
"DELETE FROM credential WHERE id = ?",
(cred_id,)
).fetchone()
return True
def get_credentials(user_id): def get_credentials(user_id):
""" """
Returns all credentials registered to a user. Returns all credentials registered to a user.

View File

@ -92,3 +92,23 @@ nav span:hover {
color: red; color: red;
cursor: pointer; cursor: pointer;
} }
.manage-table {
border-collapse: collapse;
}
.manage-table th {
padding-left: 0.5em;
padding-right: 0.5em;
}
.manage-table td {
border: 1px solid darkgray;
padding-left: 0.5em;
padding-right: 0.5em;
text-align: center;
}
.sub-table {
margin-top: 5em;
}

View File

@ -210,3 +210,41 @@ function delete_device(device) {
device.remove() device.remove()
}); });
} }
function delete_key(key_id) {
if (!window.confirm("Are you sure you want to delete this key? This action may leave you unable to access your account.")) { return; }
let params = {
key_id: key_id,
};
let query = Object.keys(params)
.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
.join('&');
fetch(url_prefix + '/delete_key?' + query)
.then(function(response) {
return response.json();
})
.then(function(json) {
if (!json.ok) { return; }
document.querySelector('#key_' + key_id).remove();
});
}
function delete_token(token_id) {
if (!window.confirm("Are you sure you want to delete this token?")) { return; }
let params = {
token_id: token_id,
};
let query = Object.keys(params)
.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
.join('&');
fetch(url_prefix + '/delete_token?' + query)
.then(function(response) {
return response.json();
})
.then(function(json) {
if (!json.ok) { return; }
document.querySelector('#token_' + token_id).remove();
});
}

View File

@ -12,7 +12,7 @@
<h1>Juice</h1> <h1>Juice</h1>
<nav> <nav>
<span title="Add new device" onclick="new_device()"><span class="font-awesome">&#xe802;</span></span> <span title="Add new device" onclick="new_device()"><span class="font-awesome">&#xe802;</span></span>
<span title="Manage authenticators"><a href="./manage">Manage</a></span> <span title="Manage security keys and tokens"><a href="./manage">Manage</a></span>
</nav> </nav>
</header> </header>
<main> <main>

59
templates/manage.html Normal file
View File

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html>
<head>
<title>Juice - Manage</title>
<link rel="stylesheet" type="text/css" href="/static/juice.css">
<script type="text/javascript" src="/static/juice.js"></script>
<script>url_prefix = '{{ url_prefix }}';</script>
</head>
<body>
<main>
<div id="devices">
<div class="device">
<h2>Security Keys</h2>
<table id="credentials" class="manage-table">
<thead>
<tr>
<th>Nick</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{% for credential in credentials %}
<tr id="key_{{ credential.id }}">
<td>{{ credential.nick }}</td>
<td><span class="delete font-awesome" onclick="delete_key({{ credential.id }})">&#xe804;</span></td>
</tr>
{% endfor %}
</tbody>
</table>
<h2 class="sub-table">Tokens</h2>
<table id="tokens" class="manage-table">
<thead>
<tr>
<th>OS</th>
<th>Browser</th>
<th>Ip Address</th>
<th>Date Issued</th>
<th>Date Expires</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{% for token in tokens_pretty %}
<tr id="token_{{ token.id }}">
<td>{{ token.user_agent.platform }}</td>
<td>{{ token.user_agent.browser }} {{token.user_agent.version }}</td>
<td>{{ token.ip_address }}</td>
<td>{{ token.date_issued }}</td>
<td>{{ token.date_expired }}</td>
<td><span class="delete font-awesome" onclick="delete_token({{ token.id }})">&#xe804;</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</main>
</body>
</html>