diff --git a/buckler_flask.py b/buckler_flask.py new file mode 100644 index 0000000..0ed5429 --- /dev/null +++ b/buckler_flask.py @@ -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