Compare commits

..

No commits in common. "9d1a720ab73fb87aecd6a9f79323782cf479b679" and "2356c50fba0e3b6fd59004fa886a3dee894504ed" have entirely different histories.

11 changed files with 118 additions and 364 deletions

View File

@ -44,10 +44,6 @@ 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]
@ -233,8 +229,7 @@ async def get_session(request):
session = session[0]
data = await conn.fetchrow(
"SELECT user_info.username, app_user.session_data "
"FROM user_info LEFT JOIN app_user "
"ON (user_info.id = app_user.user_id) "
"FROM user_info, app_user "
"WHERE user_info.id = $1 AND app_user.app_id = $2",
session['user_id'], app['id'])
@ -249,14 +244,11 @@ async def get_session(request):
await conn.close()
data_meta = dict(data)
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.update(
{'last_used': session['last_used'].isoformat()})
data = {
'meta': data_meta,
'session_data': json.loads(session_data)
}
'session_data': json.loads(data_meta.pop('session_data'))}
return web.json_response(data)
else:
@ -272,44 +264,7 @@ 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."""
# 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)
pass
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 DEFAULT '{}',
session_data JSON,
PRIMARY KEY (user_id, app_id)
);

View File

@ -1,107 +0,0 @@
#!/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,19 +22,13 @@ main {
main section {
background-color: whitesmoke;
padding: 2% 5%;
padding: 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,17 +1,3 @@
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,37 +2,28 @@
<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>
<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.">
<link rel="stylesheet" type="text/css" href="/static/buckler.css">
</head>
<body>
<header>
<object id="logo" title="Buckler logo" data="/static/buckler_icon.svg"></object>
<object id="logo" data="/static/buckler_icon.svg"></object>
<h1>Buckler</h1>
</header>
<main>
<section>
<h2>Available Sites</h2>
<article>
<hr>
Available sites
<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>
<h2>User Permissions</h2>
<article style="display: none;">
<hr>
<table id="users">
<thead>
<tr>
@ -55,13 +46,9 @@
{% endfor %}
</tbody>
</table>
</article>
</section>
{% endif %}
<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">
<label for="current_password">Current password</label>
<input id="current_password" name="current_password" type="password"><br>
@ -71,12 +58,8 @@
<input id="verify_password" name="verify_password" type="password"><br>
<input type="submit" value="Submit">
</form>
</article>
</section>
<section>
<h2>Security Keys</h2>
<article style="display: none;">
<hr>
{% if fido2_keys %}
<table id="security_keys">
<thead>
@ -98,29 +81,6 @@
<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,17 +2,12 @@
<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>
<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>
@ -23,7 +18,6 @@
{% endif %}
<input type="submit" value="Login">
</form>
</section>
</main>
</body>
</html>

View File

@ -7,12 +7,9 @@
<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,17 +2,9 @@
<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>
<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>
@ -45,7 +37,5 @@
{% endif %}
<input type="submit" value="Register">
</form>
</section>
</main>
</body>
</html>

View File

@ -6,21 +6,17 @@
<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>
<section>
<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><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>
</div>
</div>
</main>
</body>
</html>

View File

@ -2,19 +2,8 @@
<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>
<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>