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( fido2_keys = await conn.fetch(
"SELECT * FROM user_credential WHERE user_id = $1", "SELECT * FROM user_credential WHERE user_id = $1",
request['session']['id']) 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']: if request['session']['admin']:
apps = [app['name'] for app in apps] apps = [app['name'] for app in apps]
@ -229,7 +233,8 @@ async def get_session(request):
session = session[0] session = session[0]
data = await conn.fetchrow( data = await conn.fetchrow(
"SELECT user_info.username, app_user.session_data " "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", "WHERE user_info.id = $1 AND app_user.app_id = $2",
session['user_id'], app['id']) session['user_id'], app['id'])
@ -244,11 +249,14 @@ async def get_session(request):
await conn.close() await conn.close()
data_meta = dict(data) data_meta = dict(data)
data_meta.update( data_meta['last_used'] = session['last_used'].isoformat()
{'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 = { data = {
'meta': data_meta, 'meta': data_meta,
'session_data': json.loads(data_meta.pop('session_data'))} 'session_data': json.loads(session_data)
}
return web.json_response(data) return web.json_response(data)
else: else:
@ -264,7 +272,44 @@ async def get_session(request):
@routes.post(config.url_prefix + '/set_session', name='set_session') @routes.post(config.url_prefix + '/set_session', name='set_session')
async def set_session(request): async def set_session(request):
"""Allows an application to set a user app session.""" """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(): async def init_app():

View File

@ -41,7 +41,7 @@ CREATE TABLE IF NOT EXISTS app_info (
CREATE TABLE IF NOT EXISTS app_user ( CREATE TABLE IF NOT EXISTS app_user (
user_id INTEGER references user_info(id) ON DELETE CASCADE, user_id INTEGER references user_info(id) ON DELETE CASCADE,
app_id INTEGER references app_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) 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 { main section {
background-color: whitesmoke; background-color: whitesmoke;
padding: 5%; padding: 2% 5%;
border-radius: 0.5em; border-radius: 0.5em;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 3px 1px -2px rgba(0, 0, 0, 0.2),
0 1px 5px 0 rgba(0, 0, 0, 0.12); 0 1px 5px 0 rgba(0, 0, 0, 0.12);
} }
h2 {
font-size: 16px;
margin: 0;
cursor: pointer;
}
#avail_sites { #avail_sites {
margin: 0; margin: 0;
padding-left: 1em; 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) { function perm_change(row) {
let user_perms = users_perms[row.children[0].textContent]; let user_perms = users_perms[row.children[0].textContent];
let row_perms = []; let row_perms = [];

View File

@ -2,28 +2,37 @@
<html lang="en"> <html lang="en">
<head> <head>
<title>Buckler</title> <title>Buckler</title>
<link rel="stylesheet" type="text/css" href="/static/buckler.css">
<script> <script>
var users_perms = {{ users_json|safe }}; var users_perms = {{ users_json|safe }};
</script> </script>
<script type="text/javascript" src="/static/buckler.js"></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> </head>
<body> <body>
<header> <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> <h1>Buckler</h1>
</header> </header>
<main> <main>
<section> <section>
Available sites <h2>Available Sites</h2>
<article>
<hr>
<ul id="avail_sites"> <ul id="avail_sites">
{% for site in avail_sites %} {% for site in avail_sites %}
<li><a href="{{ site['url'] }}">{{ site['name'] }}</a></li> <li><a href="{{ site['url'] }}">{{ site['name'] }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</article>
</section> </section>
{% if request['session']['admin'] %} {% if request['session']['admin'] %}
<section> <section>
<h2>User Permissions</h2>
<article style="display: none;">
<hr>
<table id="users"> <table id="users">
<thead> <thead>
<tr> <tr>
@ -46,9 +55,13 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</article>
</section> </section>
{% endif %} {% endif %}
<section> <section>
<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"> <form action="{{ request.app.router['change_password'].url_for() }}" method="post" enctype="application/x-www-form-urlencoded">
<label for="current_password">Current password</label> <label for="current_password">Current password</label>
<input id="current_password" name="current_password" type="password"><br> <input id="current_password" name="current_password" type="password"><br>
@ -58,8 +71,12 @@
<input id="verify_password" name="verify_password" type="password"><br> <input id="verify_password" name="verify_password" type="password"><br>
<input type="submit" value="Submit"> <input type="submit" value="Submit">
</form> </form>
</article>
</section> </section>
<section> <section>
<h2>Security Keys</h2>
<article style="display: none;">
<hr>
{% if fido2_keys %} {% if fido2_keys %}
<table id="security_keys"> <table id="security_keys">
<thead> <thead>
@ -81,6 +98,29 @@
<span>No registered keys.</span> <span>No registered keys.</span>
{% endif %} {% endif %}
<br><a href="./add_key">Add key</a> <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> </section>
</main> </main>
</body> </body>

View File

@ -2,12 +2,17 @@
<html lang="en"> <html lang="en">
<head> <head>
<title>Buckler - Login</title> <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> </head>
<body> <body>
<header> <header>
<object id="logo" title="Buckler logo" data="/static/buckler_icon.svg"></object>
<h1>Buckler Login</h1> <h1>Buckler Login</h1>
</header> </header>
<main> <main>
<section>
<form action="{{ request.app.router['login'].url_for() }}" method="post" enctype="application/x-www-form-urlencoded"> <form action="{{ request.app.router['login'].url_for() }}" method="post" enctype="application/x-www-form-urlencoded">
<label for="username">Username</label> <label for="username">Username</label>
<input id="username" name="username" type="text"><br> <input id="username" name="username" type="text"><br>
@ -18,6 +23,7 @@
{% endif %} {% endif %}
<input type="submit" value="Login"> <input type="submit" value="Login">
</form> </form>
</section>
</main> </main>
</body> </body>
</html> </html>

View File

@ -7,9 +7,12 @@
<script type="text/javascript" src="/static/buckler-auth.js"></script> <script type="text/javascript" src="/static/buckler-auth.js"></script>
<script>const url_prefix = '{{ url_prefix }}';</script> <script>const url_prefix = '{{ url_prefix }}';</script>
<script>window.onload = login;</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> </head>
<body> <body>
<header> <header>
<object id="logo" title="Buckler logo" data="/static/buckler_icon.svg"></object>
<h1>Buckler Login</h1> <h1>Buckler Login</h1>
</header> </header>
<main> <main>

View File

@ -2,9 +2,17 @@
<html lang="en"> <html lang="en">
<head> <head>
<title>Buckler - Register</title> <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> </head>
<body> <body>
<header>
<object id="logo" title="Buckler logo" data="/static/buckler_icon.svg"></object>
<h1>Buckler Register</h1> <h1>Buckler Register</h1>
</header>
<main>
<section>
<form method="POST" enctype="application/x-www-form-urlencoded"> <form method="POST" enctype="application/x-www-form-urlencoded">
<label for="username">Username</label> <label for="username">Username</label>
<input id="username" name="username" type="text" minlength="3" maxlength="20"><br> <input id="username" name="username" type="text" minlength="3" maxlength="20"><br>
@ -37,5 +45,7 @@
{% endif %} {% endif %}
<input type="submit" value="Register"> <input type="submit" value="Register">
</form> </form>
</section>
</main>
</body> </body>
</html> </html>

View File

@ -6,17 +6,21 @@
<script type="text/javascript" src="/static/cbor.js"></script> <script type="text/javascript" src="/static/cbor.js"></script>
<script type="text/javascript" src="/static/buckler-auth.js"></script> <script type="text/javascript" src="/static/buckler-auth.js"></script>
<script>const url_prefix = '{{ url_prefix }}';</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> </head>
<body> <body>
<main> <header>
<div id="devices"> <object id="logo" title="Buckler logo" data="/static/buckler_icon.svg"></object>
<div class="device">
<h1>Register Security Key</h1> <h1>Register Security Key</h1>
</header>
<main>
<section>
<p>Upon clicking submit, your security key will begin flashing. Have it ready. <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><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()"> <p><input type="button" value="Submit" onclick="register()">
</div> </section>
</div>
</main> </main>
</body> </body>
</html> </html>

View File

@ -2,8 +2,19 @@
<html lang="en"> <html lang="en">
<head> <head>
<title>Buckler - Register</title> <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> </head>
<body> <body>
<header>
<object id="logo" title="Buckler logo" data="/static/buckler_icon.svg"></object>
<h1>Buckler Register</h1>
</header>
<main>
<section>
{{ message }} {{ message }}
</section>
</main>
</body> </body>
</html> </html>