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( 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]
@ -233,8 +229,7 @@ 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 LEFT JOIN app_user " "FROM user_info, 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'])
@ -249,14 +244,11 @@ async def get_session(request):
await conn.close() await conn.close()
data_meta = dict(data) data_meta = dict(data)
data_meta['last_used'] = session['last_used'].isoformat() data_meta.update(
data_meta['user_sid'] = user_sid {'last_used': session['last_used'].isoformat()})
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(session_data) 'session_data': json.loads(data_meta.pop('session_data'))}
}
return web.json_response(data) return web.json_response(data)
else: else:
@ -272,44 +264,7 @@ 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."""
# TODO: only allow LAN IPs pass
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 DEFAULT '{}', session_data JSON,
PRIMARY KEY (user_id, app_id) 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 { main section {
background-color: whitesmoke; background-color: whitesmoke;
padding: 2% 5%; padding: 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,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) { 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,125 +2,85 @@
<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>
<script>window.onload = load;</script> <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> <object id="logo" data="/static/buckler_icon.svg"></object>
<h1>Buckler</h1> <h1>Buckler</h1>
</header> </header>
<main> <main>
<section> <section>
<h2>Available Sites</h2> Available sites
<article> <ul id="avail_sites">
<hr> {% for site in avail_sites %}
<ul id="avail_sites"> <li><a href="{{ site['url'] }}">{{ site['name'] }}</a></li>
{% for site in avail_sites %} {% endfor %}
<li><a href="{{ site['url'] }}">{{ site['name'] }}</a></li> </ul>
{% endfor %}
</ul>
</article>
</section> </section>
{% if request['session']['admin'] %} {% if request['session']['admin'] %}
<section> <section>
<h2>User Permissions</h2> <table id="users">
<article style="display: none;"> <thead>
<hr> <tr>
<table id="users"> <th>User</th>
<thead> {% for app in apps %}
<tr> <th>{{ app }}</th>
<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 %}
</tbody> <th></th>
</table> </tr>
</article> </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>
</section> </section>
{% endif %} {% endif %}
<section> <section>
<h2>Change Password</h2> <form action="{{ request.app.router['change_password'].url_for() }}" method="post" enctype="application/x-www-form-urlencoded">
<article style="display: none;"> <label for="current_password">Current password</label>
<hr> <input id="current_password" name="current_password" type="password"><br>
<form action="{{ request.app.router['change_password'].url_for() }}" method="post" enctype="application/x-www-form-urlencoded"> <label for="new_password">New password</label>
<label for="current_password">Current password</label> <input id="new_password" name="new_password" type="password"><br>
<input id="current_password" name="current_password" type="password"><br> <label for="verify_password">Verify password</label>
<label for="new_password">New password</label> <input id="verify_password" name="verify_password" type="password"><br>
<input id="new_password" name="new_password" type="password"><br> <input type="submit" value="Submit">
<label for="verify_password">Verify password</label> </form>
<input id="verify_password" name="verify_password" type="password"><br>
<input type="submit" value="Submit">
</form>
</article>
</section> </section>
<section> <section>
<h2>Security Keys</h2> {% if fido2_keys %}
<article style="display: none;"> <table id="security_keys">
<hr> <thead>
{% if fido2_keys %} <tr>
<table id="security_keys"> <th>Nick</th>
<thead> <th>Delete</th>
<tr> </tr>
<th>Nick</th> </thead>
<th>Delete</th> <tbody>
</tr> {% for key in fido2_keys %}
</thead> <tr>
<tbody> <td>{{ key['nick'] }}</td>
{% for key in fido2_keys %} <td><input type="checkbox"></td>
<tr> </tr>
<td>{{ key['nick'] }}</td> {% endfor %}
<td><input type="checkbox"></td> </tbody>
</tr> </table>
{% endfor %} {% else %}
</tbody> <span>No registered keys.</span>
</table> {% endif %}
{% else %} <br><a href="./add_key">Add key</a>
<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,28 +2,22 @@
<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> <label for="password">Password</label>
<label for="password">Password</label> <input id="password" name="password" type="password"><br>
<input id="password" name="password" type="password"><br> {% if login_failed %}
{% if login_failed %} <ul><li>Username and/or password incorrect</li></ul>
<ul><li>Username and/or password incorrect</li></ul> {% endif %}
{% endif %} <input type="submit" value="Login">
<input type="submit" value="Login"> </form>
</form>
</section>
</main> </main>
</body> </body>
</html> </html>

View File

@ -7,12 +7,9 @@
<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,50 +2,40 @@
<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> <h1>Buckler Register</h1>
<object id="logo" title="Buckler logo" data="/static/buckler_icon.svg"></object> <form method="POST" enctype="application/x-www-form-urlencoded">
<h1>Buckler Register</h1> <label for="username">Username</label>
</header> <input id="username" name="username" type="text" minlength="3" maxlength="20"><br>
<main> {% if errors['username'] %}
<section> <ul>
<form method="POST" enctype="application/x-www-form-urlencoded"> {% for error in errors['username'] %}
<label for="username">Username</label> <li class="error">{{ error }}</li>
<input id="username" name="username" type="text" minlength="3" maxlength="20"><br> {% endfor %}
{% if errors['username'] %} </ul>
<ul> {% endif %}
{% for error in errors['username'] %} <label for="email">Email</label>
<li class="error">{{ error }}</li> <input id="email" name="email" type="email"><br>
{% endfor %} {% if errors['email'] %}
</ul> <ul>
{% endif %} {% for error in errors['email'] %}
<label for="email">Email</label> <li class="error">{{ error }}</li>
<input id="email" name="email" type="email"><br> {% endfor %}
{% if errors['email'] %} </ul>
<ul> {% endif %}
{% for error in errors['email'] %} <label for="password">Password</label>
<li class="error">{{ error }}</li> <input id="password" name="password" type="password" minlength="8" maxlength="10240"><br>
{% endfor %} <label for="password_verify">Verify Password</label>
</ul> <input id="password_verify" name="password_verify" type="password" minlength="8" maxlength="10240"><br>
{% endif %} {% if errors['password'] %}
<label for="password">Password</label> <ul>
<input id="password" name="password" type="password" minlength="8" maxlength="10240"><br> {% for error in errors['password'] %}
<label for="password_verify">Verify Password</label> <li class="error">{{ error }}</li>
<input id="password_verify" name="password_verify" type="password" minlength="8" maxlength="10240"><br> {% endfor %}
{% if errors['password'] %} </ul>
<ul> {% endif %}
{% for error in errors['password'] %} <input type="submit" value="Register">
<li class="error">{{ error }}</li> </form>
{% endfor %}
</ul>
{% endif %}
<input type="submit" value="Register">
</form>
</section>
</main>
</body> </body>
</html> </html>

View File

@ -6,21 +6,17 @@
<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>
<section> <div id="devices">
<p>Upon clicking submit, your security key will begin flashing. Have it ready. <div class="device">
<p><label for="security_key_nick">Security key nick</label> <h1>Register Security Key</h1>
<input id="security_key_nick" type="text" minlength="1" maxlength="64" required> <p>Upon clicking submit, your security key will begin flashing. Have it ready.
<p><input type="button" value="Submit" onclick="register()"> <p><label for="security_key_nick">Security key nick</label> <input id="security_key_nick" type="text" minlength="1" maxlength="64" required>
</section> <p><input type="button" value="Submit" onclick="register()">
</div>
</div>
</main> </main>
</body> </body>
</html> </html>

View File

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