diff --git a/roac/__init__.py b/roac/__init__.py index 525114ce179ad480ecc15a7cd7371258cdc2a872..d48f9c36c179a9a1b14ac9fb17980ae707daf057 100644 --- a/roac/__init__.py +++ b/roac/__init__.py @@ -1,10 +1,12 @@ import secrets import os +import datetime +import functools -from flask import Flask +from flask import Flask, session, request, redirect, abort, Response, url_for, render_template +from requests_oauthlib import OAuth2Session from .models import * -from .base import bp as base def create_app(test_config=None): app = Flask(__name__) @@ -15,9 +17,66 @@ def create_app(test_config=None): app.config.from_pyfile('config.py', silent=True) else: app.config.from_mapping(test_config) - + # OAuth2Session.fetch_token verifies that the passed URIs scheme (the scheme + # of request.url) is HTTPS. The way we deploy this app, request.url does not + # reflect the actual request url, so we disable this check. + if app.debug: + os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' db.init_app(app) + def user_required(func): + @functools.wraps(func) + def decorator(*args, **kwargs): + timestamp = datetime.datetime.fromtimestamp(session.get('timestamp', 0)) + if datetime.datetime.utcnow() - timestamp > datetime.timedelta(minutes=30): + return redirect(url_for('oauth_login')) + if 'userinfo' not in session: + return redirect(url_for('oauth_login')) + user = User.query.filter_by(loginname=session['userinfo']['nickname']).one_or_none() + if user is None: + return render_template('error_unknown_user.html') + return func(*args, **kwargs) + return decorator + + @app.route('/') + @user_required + def index(): + return render_template('index.html') + + @app.route('/create') + @user_required + def create_account(): + return render_template('index.html') + + @app.route('/oauth/login') + def oauth_login(): + client = OAuth2Session(app.config['OAUTH2_CLIENT_ID'], + redirect_uri=url_for('oauth_callback', _external=True)) + url, state = client.authorization_url(app.config['OAUTH2_AUTH_URL']) + session.clear() + session['timestamp'] = datetime.datetime.utcnow().timestamp() + session['oauth-state'] = state + return redirect(url) + + @app.route('/oauth/callback') + def oauth_callback(): + if 'oauth-state' not in session: + return redirect(url_for('oauth_login')) + timestamp = datetime.datetime.fromtimestamp(session.get('timestamp', 0)) + if datetime.datetime.utcnow() - timestamp > datetime.timedelta(minutes=30): + return redirect(url_for('oauth_login')) + client = OAuth2Session(app.config['OAUTH2_CLIENT_ID'], + redirect_uri=url_for('oauth_callback', _external=True), + state=session['oauth-state']) + client.fetch_token(app.config['OAUTH2_TOKEN_URL'], + client_secret=app.config['OAUTH2_CLIENT_SECRET'], + authorization_response=request.url, verify=(not app.debug)) + userinfo = client.get(app.config['OAUTH2_USERINFO_URL']).json() + session.clear() + session['timestamp'] = datetime.datetime.utcnow().timestamp() + session['userinfo'] = userinfo + return redirect(url_for('index')) + os.makedirs(app.instance_path, exist_ok=True) with app.app_context(): db.create_all() diff --git a/roac/default_config.py b/roac/default_config.py index 903cfeb172394720b221b1f866fb78b50b65110e..de889fee1cf8e158a80a38b7bb92eb322e014edb 100644 --- a/roac/default_config.py +++ b/roac/default_config.py @@ -1 +1,11 @@ -SQLALCHEMY_TRACK_MODIFICATIONS=False +# URLs of the OAuth2-based identity provider +OAUTH2_AUTH_URL = 'http://localhost:5000/oauth2/authorize' +OAUTH2_TOKEN_URL = 'http://localhost:5000/oauth2/token' +OAUTH2_USERINFO_URL = 'http://localhost:5000/oauth2/userinfo' +OAUTH2_CLIENT_ID = 'roac' +OAUTH2_CLIENT_SECRET = 'testsecret' + +# CSRF protection +SESSION_COOKIE_SECURE=True +SESSION_COOKIE_HTTPONLY=True +SESSION_COOKIE_SAMESITE='Strict' diff --git a/roac/models.py b/roac/models.py new file mode 100644 index 0000000000000000000000000000000000000000..0c73f1a31e8f83370cb999de4329bd30de841b3b --- /dev/null +++ b/roac/models.py @@ -0,0 +1,9 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + +class User(db.Model): + __tablename__ = 'user' + id = db.Column(db.Integer(), primary_key=True, autoincrement=True) + loginname = db.Column(db.String, nullable=False) + rocketchat_id = db.Column(db.String, nullable=True)