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,85 +2,125 @@
<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>
<ul id="avail_sites"> <article>
{% for site in avail_sites %} <hr>
<li><a href="{{ site['url'] }}">{{ site['name'] }}</a></li> <ul id="avail_sites">
{% endfor %} {% for site in avail_sites %}
</ul> <li><a href="{{ site['url'] }}">{{ site['name'] }}</a></li>
{% endfor %}
</ul>
</article>
</section> </section>
{% if request['session']['admin'] %} {% if request['session']['admin'] %}
<section> <section>
<table id="users"> <h2>User Permissions</h2>
<thead> <article style="display: none;">
<tr> <hr>
<th>User</th> <table id="users">
{% for app in apps %} <thead>
<th>{{ app }}</th> <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 %} {% endfor %}
<th></th> </tbody>
</tr> </table>
</thead> </article>
<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>
</section> </section>
{% endif %} {% endif %}
<section> <section>
<form action="{{ request.app.router['change_password'].url_for() }}" method="post" enctype="application/x-www-form-urlencoded"> <h2>Change Password</h2>
<label for="current_password">Current password</label> <article style="display: none;">
<input id="current_password" name="current_password" type="password"><br> <hr>
<label for="new_password">New password</label> <form action="{{ request.app.router['change_password'].url_for() }}" method="post" enctype="application/x-www-form-urlencoded">
<input id="new_password" name="new_password" type="password"><br> <label for="current_password">Current password</label>
<label for="verify_password">Verify password</label> <input id="current_password" name="current_password" type="password"><br>
<input id="verify_password" name="verify_password" type="password"><br> <label for="new_password">New password</label>
<input type="submit" value="Submit"> <input id="new_password" name="new_password" type="password"><br>
</form> <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>
<section> <section>
{% if fido2_keys %} <h2>Security Keys</h2>
<table id="security_keys"> <article style="display: none;">
<thead> <hr>
<tr> {% if fido2_keys %}
<th>Nick</th> <table id="security_keys">
<th>Delete</th> <thead>
</tr> <tr>
</thead> <th>Nick</th>
<tbody> <th>Delete</th>
{% for key in fido2_keys %} </tr>
<tr> </thead>
<td>{{ key['nick'] }}</td> <tbody>
<td><input type="checkbox"></td> {% for key in fido2_keys %}
</tr> <tr>
{% endfor %} <td>{{ key['nick'] }}</td>
</tbody> <td><input type="checkbox"></td>
</table> </tr>
{% else %} {% endfor %}
<span>No registered keys.</span> </tbody>
{% endif %} </table>
<br><a href="./add_key">Add key</a> {% 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> </section>
</main> </main>
</body> </body>

View File

@ -2,22 +2,28 @@
<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>
<form action="{{ request.app.router['login'].url_for() }}" method="post" enctype="application/x-www-form-urlencoded"> <section>
<label for="username">Username</label> <form action="{{ request.app.router['login'].url_for() }}" method="post" enctype="application/x-www-form-urlencoded">
<input id="username" name="username" type="text"><br> <label for="username">Username</label>
<label for="password">Password</label> <input id="username" name="username" type="text"><br>
<input id="password" name="password" type="password"><br> <label for="password">Password</label>
{% if login_failed %} <input id="password" name="password" type="password"><br>
<ul><li>Username and/or password incorrect</li></ul> {% if login_failed %}
{% endif %} <ul><li>Username and/or password incorrect</li></ul>
<input type="submit" value="Login"> {% endif %}
</form> <input type="submit" value="Login">
</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,40 +2,50 @@
<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>
<h1>Buckler Register</h1> <header>
<form method="POST" enctype="application/x-www-form-urlencoded"> <object id="logo" title="Buckler logo" data="/static/buckler_icon.svg"></object>
<label for="username">Username</label> <h1>Buckler Register</h1>
<input id="username" name="username" type="text" minlength="3" maxlength="20"><br> </header>
{% if errors['username'] %} <main>
<ul> <section>
{% for error in errors['username'] %} <form method="POST" enctype="application/x-www-form-urlencoded">
<li class="error">{{ error }}</li> <label for="username">Username</label>
{% endfor %} <input id="username" name="username" type="text" minlength="3" maxlength="20"><br>
</ul> {% if errors['username'] %}
{% endif %} <ul>
<label for="email">Email</label> {% for error in errors['username'] %}
<input id="email" name="email" type="email"><br> <li class="error">{{ error }}</li>
{% if errors['email'] %} {% endfor %}
<ul> </ul>
{% for error in errors['email'] %} {% endif %}
<li class="error">{{ error }}</li> <label for="email">Email</label>
{% endfor %} <input id="email" name="email" type="email"><br>
</ul> {% if errors['email'] %}
{% endif %} <ul>
<label for="password">Password</label> {% for error in errors['email'] %}
<input id="password" name="password" type="password" minlength="8" maxlength="10240"><br> <li class="error">{{ error }}</li>
<label for="password_verify">Verify Password</label> {% endfor %}
<input id="password_verify" name="password_verify" type="password" minlength="8" maxlength="10240"><br> </ul>
{% if errors['password'] %} {% endif %}
<ul> <label for="password">Password</label>
{% for error in errors['password'] %} <input id="password" name="password" type="password" minlength="8" maxlength="10240"><br>
<li class="error">{{ error }}</li> <label for="password_verify">Verify Password</label>
{% endfor %} <input id="password_verify" name="password_verify" type="password" minlength="8" maxlength="10240"><br>
</ul> {% if errors['password'] %}
{% endif %} <ul>
<input type="submit" value="Register"> {% for error in errors['password'] %}
</form> <li class="error">{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
<input type="submit" value="Register">
</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>
<header>
<object id="logo" title="Buckler logo" data="/static/buckler_icon.svg"></object>
<h1>Register Security Key</h1>
</header>
<main> <main>
<div id="devices"> <section>
<div class="device"> <p>Upon clicking submit, your security key will begin flashing. Have it ready.
<h1>Register Security Key</h1> <p><label for="security_key_nick">Security key nick</label>
<p>Upon clicking submit, your security key will begin flashing. Have it ready. <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()"> </section>
</div>
</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>
{{ 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> </body>
</html> </html>