Compare commits

...

5 Commits

Author SHA1 Message Date
9d1a720ab7 add buckler_flask.py 2019-09-25 19:47:20 -04:00
4f01847b03 implement /set_session 2019-09-25 19:46:57 -04:00
c66efb985a style and lighthouse score 2019-09-25 15:12:58 -04:00
a93a178b57 add active sessions section 2019-09-25 15:00:29 -04:00
e3c33a7492 collapsible sections 2019-09-25 12:51:36 -04:00
11 changed files with 364 additions and 118 deletions

View File

@ -44,6 +44,10 @@ async def index(request):
fido2_keys = await conn.fetch(
"SELECT * FROM user_credential WHERE user_id = $1",
request['session']['id'])
active_sessions = await conn.fetch(
"SELECT ip_address FROM user_session "
"WHERE user_id = $1",
request['session']['id'])
if request['session']['admin']:
apps = [app['name'] for app in apps]
@ -229,7 +233,8 @@ async def get_session(request):
session = session[0]
data = await conn.fetchrow(
"SELECT user_info.username, app_user.session_data "
"FROM user_info, app_user "
"FROM user_info LEFT JOIN app_user "
"ON (user_info.id = app_user.user_id) "
"WHERE user_info.id = $1 AND app_user.app_id = $2",
session['user_id'], app['id'])
@ -244,11 +249,14 @@ async def get_session(request):
await conn.close()
data_meta = dict(data)
data_meta.update(
{'last_used': session['last_used'].isoformat()})
data_meta['last_used'] = session['last_used'].isoformat()
data_meta['user_sid'] = user_sid
data_meta['user_id'] = user_id
session_data = data_meta.pop('session_data')
data = {
'meta': data_meta,
'session_data': json.loads(data_meta.pop('session_data'))}
'session_data': json.loads(session_data)
}
return web.json_response(data)
else:
@ -264,7 +272,44 @@ async def get_session(request):
@routes.post(config.url_prefix + '/set_session', name='set_session')
async def set_session(request):
"""Allows an application to set a user app session."""
pass
# TODO: only allow LAN IPs
app_id = request.query.get('app_id')
app_key = request.query.get('app_key')
user_id = request.query.get('userid')
user_sid = request.query.get('session')
try:
app_id = int(app_id)
user_id = int(user_id)
assert all((app_id, app_key, user_id, user_sid))
except (ValueError, AssertionError):
return web.json_response({'error': "Invalid credentials."})
conn = await request.app['pool'].acquire()
app = await conn.fetchrow("SELECT * FROM app_info WHERE id = $1", app_id)
if app:
if argon2.verify(app_key, app['key_hash']):
session = await conn.fetchrow(
"SELECT * FROM user_session "
"WHERE user_id = $1 AND id = $2 AND expires > NOW()",
user_id, user_sid)
if session:
session_data = await request.text()
# TODO: error handling, verify json
await conn.execute(
"UPDATE app_user SET session_data = $1 "
"WHERE user_id = $2 AND app_id = $3",
session_data, user_id, app_id)
await conn.close()
return web.json_response({'success': True})
else:
error = {'error': "User ID or Session ID invalid."}
else:
error = {'error': "App ID or Key invalid."}
else:
error = {'error': "App ID or Key invalid."}
await conn.close()
return web.json_response(error)
async def init_app():

View File

@ -41,7 +41,7 @@ CREATE TABLE IF NOT EXISTS app_info (
CREATE TABLE IF NOT EXISTS app_user (
user_id INTEGER references user_info(id) ON DELETE CASCADE,
app_id INTEGER references app_info(id) ON DELETE CASCADE,
session_data JSON,
session_data JSON DEFAULT '{}',
PRIMARY KEY (user_id, app_id)
);

107
buckler_flask.py Normal file
View File

@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""
Session interface middlewares to integrate the flask app with Buckler.
"""
import json
import urllib.parse
import urllib.request
from datetime import datetime
from flask.sessions import SessionInterface, SessionMixin
from flask import session, redirect, request
import config
class BucklerSessionInterface(SessionInterface):
"""
Queries the Buckler server for session data to the current user and
application.
"""
def __init__(self):
self.url = config.buckler['url']
self.app_id = config.buckler['app_id']
self.app_key = config.buckler['app_key']
def open_session(self, app, request):
"""Called when a request is initiated."""
user_id = request.cookies.get('userid')
user_sid = request.cookies.get('session')
params = {
'app_id': self.app_id,
'app_key': self.app_key,
'userid': user_id,
'session': user_sid
}
params = urllib.parse.urlencode(params)
req = urllib.request.Request(self.url + f"/get_session?{params}")
res = urllib.request.urlopen(req)
data = json.loads(res.read())
if data.get('error'):
return None
session = BucklerSession()
session.update(data['session_data'])
session.meta = data['meta']
session.cookies = request.cookies
return session
def save_session(self, app, session, response):
"""Called at the end of a request."""
if not session.modified:
return
user_id = session.meta.get('user_id')
user_sid = session.meta.get('user_sid')
params = {
'app_id': self.app_id,
'app_key': self.app_key,
'userid': user_id,
'session': user_sid
}
params = urllib.parse.urlencode(params)
data = json.dumps(session)
req = urllib.request.Request(
self.url + f"/set_session?{params}",
data=data.encode('utf8'),
method='POST')
res = urllib.request.urlopen(req)
last_used = datetime.fromisoformat(session.meta['last_used'])
now = datetime.now(last_used.tzinfo)
delta = now - last_used
if delta.seconds > 600:
response.set_cookie(
'userid',
session.cookies['userid'],
max_age=30*24*60*60,
secure=True,
httponly=True)
response.set_cookie(
'session',
session.cookies['session'],
max_age=30*24*60*60,
secure=True,
httponly=True)
class BucklerSession(dict, SessionMixin):
"""A server side session class based on the Buckler security shield."""
def __init__(self):
super().__init__()
self.modified = False
def __setitem__(self, key, value):
super().__setitem__(key, value)
self.modified = True
def require_auth():
"""
Requires the user to be properly authenticated with Buckler before
accessing any views on the application.
"""
if not hasattr(session, 'meta'):
resp = redirect(config.buckler['login_url'])
resp.set_cookie('redirect', request.url)
return resp

View File

@ -22,13 +22,19 @@ main {
main section {
background-color: whitesmoke;
padding: 5%;
padding: 2% 5%;
border-radius: 0.5em;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 3px 1px -2px rgba(0, 0, 0, 0.2),
0 1px 5px 0 rgba(0, 0, 0, 0.12);
}
h2 {
font-size: 16px;
margin: 0;
cursor: pointer;
}
#avail_sites {
margin: 0;
padding-left: 1em;

View File

@ -1,3 +1,17 @@
function load() {
let headers = document.querySelectorAll('h2');
headers.forEach(function(header) {
header.addEventListener('click', function() {
let article = this.nextElementSibling;
if (article.style.display === 'none') {
article.style.display = '';
} else {
article.style.display = 'none';
}
});
});
}
function perm_change(row) {
let user_perms = users_perms[row.children[0].textContent];
let row_perms = [];

View File

@ -2,85 +2,125 @@
<html lang="en">
<head>
<title>Buckler</title>
<link rel="stylesheet" type="text/css" href="/static/buckler.css">
<script>
var users_perms = {{ users_json|safe }};
</script>
<script type="text/javascript" src="/static/buckler.js"></script>
<link rel="stylesheet" type="text/css" href="/static/buckler.css">
<script>window.onload = load;</script>
<meta name="viewport" content="width=device-width, initial-scale=0.8">
<meta name="description" content="A small shield for web applications.">
</head>
<body>
<header>
<object id="logo" data="/static/buckler_icon.svg"></object>
<object id="logo" title="Buckler logo" data="/static/buckler_icon.svg"></object>
<h1>Buckler</h1>
</header>
<main>
<section>
Available sites
<ul id="avail_sites">
{% for site in avail_sites %}
<li><a href="{{ site['url'] }}">{{ site['name'] }}</a></li>
{% endfor %}
</ul>
<h2>Available Sites</h2>
<article>
<hr>
<ul id="avail_sites">
{% for site in avail_sites %}
<li><a href="{{ site['url'] }}">{{ site['name'] }}</a></li>
{% endfor %}
</ul>
</article>
</section>
{% if request['session']['admin'] %}
<section>
<table id="users">
<thead>
<tr>
<th>User</th>
{% for app in apps %}
<th>{{ app }}</th>
<h2>User Permissions</h2>
<article style="display: none;">
<hr>
<table id="users">
<thead>
<tr>
<th>User</th>
{% for app in apps %}
<th>{{ app }}</th>
{% endfor %}
<th></th>
</tr>
</thead>
<tbody>
{% for username, values in users.items() %}
<tr>
<td>{{ username }}</td>
{% for value in values %}
<td><input type="checkbox" onchange="perm_change(this.parentElement.parentElement)"{% if value %} checked{% endif %}></td>
{% endfor %}
<td><input type="submit"></td>
</tr>
{% endfor %}
<th></th>
</tr>
</thead>
<tbody>
{% for username, values in users.items() %}
<tr>
<td>{{ username }}</td>
{% for value in values %}
<td><input type="checkbox" onchange="perm_change(this.parentElement.parentElement)"{% if value %} checked{% endif %}></td>
{% endfor %}
<td><input type="submit"></td>
</tr>
{% endfor %}
</tbody>
</table>
</tbody>
</table>
</article>
</section>
{% endif %}
<section>
<form action="{{ request.app.router['change_password'].url_for() }}" method="post" enctype="application/x-www-form-urlencoded">
<label for="current_password">Current password</label>
<input id="current_password" name="current_password" type="password"><br>
<label for="new_password">New password</label>
<input id="new_password" name="new_password" type="password"><br>
<label for="verify_password">Verify password</label>
<input id="verify_password" name="verify_password" type="password"><br>
<input type="submit" value="Submit">
</form>
<h2>Change Password</h2>
<article style="display: none;">
<hr>
<form action="{{ request.app.router['change_password'].url_for() }}" method="post" enctype="application/x-www-form-urlencoded">
<label for="current_password">Current password</label>
<input id="current_password" name="current_password" type="password"><br>
<label for="new_password">New password</label>
<input id="new_password" name="new_password" type="password"><br>
<label for="verify_password">Verify password</label>
<input id="verify_password" name="verify_password" type="password"><br>
<input type="submit" value="Submit">
</form>
</article>
</section>
<section>
{% if fido2_keys %}
<table id="security_keys">
<thead>
<tr>
<th>Nick</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{% for key in fido2_keys %}
<tr>
<td>{{ key['nick'] }}</td>
<td><input type="checkbox"></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<span>No registered keys.</span>
{% endif %}
<br><a href="./add_key">Add key</a>
<h2>Security Keys</h2>
<article style="display: none;">
<hr>
{% if fido2_keys %}
<table id="security_keys">
<thead>
<tr>
<th>Nick</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{% for key in fido2_keys %}
<tr>
<td>{{ key['nick'] }}</td>
<td><input type="checkbox"></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<span>No registered keys.</span>
{% endif %}
<br><a href="./add_key">Add key</a>
</article>
</section>
<section>
<h2>Active Sessions</h2>
<article style="display: none;">
<hr>
<table id="active_sessions">
<thead>
<tr>
<th>IP Address</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{% for session in active_sessions %}
<tr>
<td>{{ session['ip_address'] }}</td>
<td><input type="checkbox"></td>
</tr>
{% endfor %}
</tbody>
</table>
</article>
</section>
</main>
</body>

View File

@ -2,22 +2,28 @@
<html lang="en">
<head>
<title>Buckler - Login</title>
<link rel="stylesheet" type="text/css" href="/static/buckler.css">
<meta name="viewport" content="width=device-width, initial-scale=0.8">
<meta name="description" content="A small shield for web applications.">
</head>
<body>
<header>
<object id="logo" title="Buckler logo" data="/static/buckler_icon.svg"></object>
<h1>Buckler Login</h1>
</header>
<main>
<form action="{{ request.app.router['login'].url_for() }}" method="post" enctype="application/x-www-form-urlencoded">
<label for="username">Username</label>
<input id="username" name="username" type="text"><br>
<label for="password">Password</label>
<input id="password" name="password" type="password"><br>
{% if login_failed %}
<ul><li>Username and/or password incorrect</li></ul>
{% endif %}
<input type="submit" value="Login">
</form>
<section>
<form action="{{ request.app.router['login'].url_for() }}" method="post" enctype="application/x-www-form-urlencoded">
<label for="username">Username</label>
<input id="username" name="username" type="text"><br>
<label for="password">Password</label>
<input id="password" name="password" type="password"><br>
{% if login_failed %}
<ul><li>Username and/or password incorrect</li></ul>
{% endif %}
<input type="submit" value="Login">
</form>
</section>
</main>
</body>
</html>

View File

@ -7,9 +7,12 @@
<script type="text/javascript" src="/static/buckler-auth.js"></script>
<script>const url_prefix = '{{ url_prefix }}';</script>
<script>window.onload = login;</script>
<meta name="viewport" content="width=device-width, initial-scale=0.8">
<meta name="description" content="A small shield for web applications.">
</head>
<body>
<header>
<object id="logo" title="Buckler logo" data="/static/buckler_icon.svg"></object>
<h1>Buckler Login</h1>
</header>
<main>

View File

@ -2,40 +2,50 @@
<html lang="en">
<head>
<title>Buckler - Register</title>
<link rel="stylesheet" type="text/css" href="/static/buckler.css">
<meta name="viewport" content="width=device-width, initial-scale=0.8">
<meta name="description" content="A small shield for web applications.">
</head>
<body>
<h1>Buckler Register</h1>
<form method="POST" enctype="application/x-www-form-urlencoded">
<label for="username">Username</label>
<input id="username" name="username" type="text" minlength="3" maxlength="20"><br>
{% if errors['username'] %}
<ul>
{% for error in errors['username'] %}
<li class="error">{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
<label for="email">Email</label>
<input id="email" name="email" type="email"><br>
{% if errors['email'] %}
<ul>
{% for error in errors['email'] %}
<li class="error">{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
<label for="password">Password</label>
<input id="password" name="password" type="password" minlength="8" maxlength="10240"><br>
<label for="password_verify">Verify Password</label>
<input id="password_verify" name="password_verify" type="password" minlength="8" maxlength="10240"><br>
{% if errors['password'] %}
<ul>
{% for error in errors['password'] %}
<li class="error">{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
<input type="submit" value="Register">
</form>
<header>
<object id="logo" title="Buckler logo" data="/static/buckler_icon.svg"></object>
<h1>Buckler Register</h1>
</header>
<main>
<section>
<form method="POST" enctype="application/x-www-form-urlencoded">
<label for="username">Username</label>
<input id="username" name="username" type="text" minlength="3" maxlength="20"><br>
{% if errors['username'] %}
<ul>
{% for error in errors['username'] %}
<li class="error">{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
<label for="email">Email</label>
<input id="email" name="email" type="email"><br>
{% if errors['email'] %}
<ul>
{% for error in errors['email'] %}
<li class="error">{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
<label for="password">Password</label>
<input id="password" name="password" type="password" minlength="8" maxlength="10240"><br>
<label for="password_verify">Verify Password</label>
<input id="password_verify" name="password_verify" type="password" minlength="8" maxlength="10240"><br>
{% if errors['password'] %}
<ul>
{% for error in errors['password'] %}
<li class="error">{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
<input type="submit" value="Register">
</form>
</section>
</main>
</body>
</html>

View File

@ -6,17 +6,21 @@
<script type="text/javascript" src="/static/cbor.js"></script>
<script type="text/javascript" src="/static/buckler-auth.js"></script>
<script>const url_prefix = '{{ url_prefix }}';</script>
<meta name="viewport" content="width=device-width, initial-scale=0.8">
<meta name="description" content="A small shield for web applications.">
</head>
<body>
<header>
<object id="logo" title="Buckler logo" data="/static/buckler_icon.svg"></object>
<h1>Register Security Key</h1>
</header>
<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>
<section>
<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()">
</section>
</main>
</body>
</html>

View File

@ -2,8 +2,19 @@
<html lang="en">
<head>
<title>Buckler - Register</title>
<link rel="stylesheet" type="text/css" href="/static/buckler.css">
<meta name="viewport" content="width=device-width, initial-scale=0.8">
<meta name="description" content="A small shield for web applications.">
</head>
<body>
{{ message }}
<header>
<object id="logo" title="Buckler logo" data="/static/buckler_icon.svg"></object>
<h1>Buckler Register</h1>
</header>
<main>
<section>
{{ message }}
</section>
</main>
</body>
</html>