added /manage page
This commit is contained in:
parent
0f6fa87668
commit
b15e0be464
109
auth.py
109
auth.py
|
@ -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
57
db.py
|
@ -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.
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<h1>Juice</h1>
|
<h1>Juice</h1>
|
||||||
<nav>
|
<nav>
|
||||||
<span title="Add new device" onclick="new_device()"><span class="font-awesome"></span></span>
|
<span title="Add new device" onclick="new_device()"><span class="font-awesome"></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
59
templates/manage.html
Normal 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 }})"></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 }})"></span></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user