From bc709d1aaf1d4ce752394be9d575414de0c66307 Mon Sep 17 00:00:00 2001 From: Frederick Muriuki Muriithi Date: Wed, 28 Dec 2022 05:57:13 +0300 Subject: Implement "login" via OAuth2 --- etc/default_settings.py | 9 +++++ wqflask/utility/startup_config.py | 2 +- wqflask/utility/tools.py | 3 ++ wqflask/wqflask/__init__.py | 34 ++++++++++++++++--- wqflask/wqflask/oauth2/__init__.py | 0 wqflask/wqflask/oauth2/routes.py | 51 +++++++++++++++++++++++++++++ wqflask/wqflask/templates/base.html | 4 +-- wqflask/wqflask/templates/oauth2/login.html | 41 +++++++++++++++++++++++ 8 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 wqflask/wqflask/oauth2/__init__.py create mode 100644 wqflask/wqflask/oauth2/routes.py create mode 100644 wqflask/wqflask/templates/oauth2/login.html diff --git a/etc/default_settings.py b/etc/default_settings.py index 6d7ac063..38c0a110 100644 --- a/etc/default_settings.py +++ b/etc/default_settings.py @@ -27,6 +27,8 @@ import sys with open("../etc/VERSION", "r") as version_file: GN_VERSION = version_file.read() +SECRET_KEY = "pleaseChangeThisToSomethingSecretInAnExternalConfigFileOrEnvvars" + # Redis REDIS_URL = "redis://:@localhost:6379/0" @@ -115,3 +117,10 @@ JS_GN_PATH = os.environ['HOME'] + "/genenetwork/javascript" # GEMMA_COMMAND = str.strip(os.popen("which gemma").read()) REAPER_COMMAND = os.environ['GN2_PROFILE'] + "/bin/qtlreaper" # GEMMA_WRAPPER_COMMAND = str.strip(os.popen("which gemma-wrapper").read()) + +OAUTH2_CLIENT_ID="0bbfca82-d73f-4bd4-a140-5ae7abb4a64d" +OAUTH2_CLIENT_SECRET="yadabadaboo" + +SESSION_TYPE = "redis" +SESSION_PERMANENT = True +SESSION_USE_SIGNER = True diff --git a/wqflask/utility/startup_config.py b/wqflask/utility/startup_config.py index 59923fa1..69cac124 100644 --- a/wqflask/utility/startup_config.py +++ b/wqflask/utility/startup_config.py @@ -15,7 +15,7 @@ ENDC = '\033[0m' def app_config(): - app.config['SESSION_TYPE'] = 'filesystem' + app.config['SESSION_TYPE'] = app.config.get('SESSION_TYPE', 'filesystem') if not app.config.get('SECRET_KEY'): import os app.config['SECRET_KEY'] = str(os.urandom(24)) diff --git a/wqflask/utility/tools.py b/wqflask/utility/tools.py index d4c83302..5b3e9413 100644 --- a/wqflask/utility/tools.py +++ b/wqflask/utility/tools.py @@ -335,3 +335,6 @@ assert_dir(JS_CYTOSCAPE_PATH) assert_file(JS_CYTOSCAPE_PATH + '/cytoscape.min.js') # assert_file(PHEWAS_FILES+"/auwerx/PheWAS_pval_EMMA_norm.RData") + +OAUTH2_CLIENT_ID = get_setting('OAUTH2_CLIENT_ID') +OAUTH2_CLIENT_SECRET = get_setting('OAUTH2_CLIENT_SECRET') diff --git a/wqflask/wqflask/__init__.py b/wqflask/wqflask/__init__.py index ada73867..66ed0e91 100644 --- a/wqflask/wqflask/__init__.py +++ b/wqflask/wqflask/__init__.py @@ -1,12 +1,16 @@ """Entry point for flask app""" # pylint: disable=C0413,E0611 import time +from typing import Tuple +from urllib.parse import urljoin, urlparse + +import redis import jinja2 +from flask_session import Session +from authlib.integrations.requests_client import OAuth2Session +from flask import g, Flask, flash, session, url_for, redirect, current_app + -from flask import g -from flask import Flask -from typing import Tuple -from urllib.parse import urlparse from utility import formatting from gn3.authentication import DataRole, AdminRole @@ -26,6 +30,7 @@ from wqflask.api.markdown import facilities_blueprint from wqflask.api.markdown import blogs_blueprint from wqflask.api.markdown import news_blueprint from wqflask.api.jobs import jobs as jobs_bp +from wqflask.oauth2.routes import oauth2 from wqflask.jupyter_notebooks import jupyter_notebooks @@ -47,6 +52,8 @@ app.jinja_env.globals.update( undefined=jinja2.StrictUndefined, numify=formatting.numify) +app.config["SESSION_REDIS"] = redis.from_url(app.config["REDIS_URL"]) + # Registering blueprints app.register_blueprint(glossary_blueprint, url_prefix="/glossary") app.register_blueprint(references_blueprint, url_prefix="/references") @@ -62,12 +69,31 @@ app.register_blueprint(resource_management, url_prefix="/resource-management") app.register_blueprint(metadata_edit, url_prefix="/datasets/") app.register_blueprint(group_management, url_prefix="/group-management") app.register_blueprint(jobs_bp, url_prefix="/jobs") +app.register_blueprint(oauth2, url_prefix="/oauth2") + +server_session = Session(app) @app.before_request def before_request(): g.request_start_time = time.time() g.request_time = lambda: "%.5fs" % (time.time() - g.request_start_time) + token = session.get("oauth2_token", False) + if token and not bool(session.get("user_details", False)): + config = current_app.config + client = OAuth2Session( + config["OAUTH2_CLIENT_ID"], config["OAUTH2_CLIENT_SECRET"], + token=token) + resp = client.get( + urljoin(config["GN_SERVER_URL"], "oauth2/user")) + user_details = resp.json() + session["user_details"] = user_details + + if user_details.get("error") == "invalid_token": + flash(user_details["error_description"], "alert-danger") + flash("You are now logged out.", "alert-info") + session.pop("user_details", None) + session.pop("oauth2_token", None) @app.context_processor def include_admin_role_class(): diff --git a/wqflask/wqflask/oauth2/__init__.py b/wqflask/wqflask/oauth2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/wqflask/wqflask/oauth2/routes.py b/wqflask/wqflask/oauth2/routes.py new file mode 100644 index 00000000..931b8b61 --- /dev/null +++ b/wqflask/wqflask/oauth2/routes.py @@ -0,0 +1,51 @@ +"""Routes for the OAuth2 auth system in GN3""" +import uuid +from urllib.parse import urljoin + +import redis +from authlib.integrations.requests_client import OAuth2Session +from authlib.integrations.base_client.errors import OAuthError +from flask import ( + flash, request, session, redirect, Blueprint, render_template, + current_app as app) + +oauth2 = Blueprint("oauth2", __name__) + +def user_logged_in(): + """Check whether the user has logged in.""" + return bool(session.get("oauth2_token", False)) + +@oauth2.route("/login", methods=["GET", "POST"]) +def login(): + """Route to allow users to sign up.""" + if request.method == "POST": + config = app.config + form = request.form + scope = "profile resource" + client = OAuth2Session( + config["OAUTH2_CLIENT_ID"], config["OAUTH2_CLIENT_SECRET"], + scope=scope, token_endpoint_auth_method="client_secret_post") + try: + token = client.fetch_token( + urljoin(config["GN_SERVER_URL"], "oauth2/token"), + username=form.get("email_address"), + password=form.get("password"), + grant_type="password") + session["oauth2_token"] = token + except OAuthError as _oaerr: + flash(_oaerr.args[0], "alert-danger") + return render_template("oauth2/login.html") + + if user_logged_in(): + return redirect("/") + + return render_template("oauth2/login.html") + + +@oauth2.route("/logout", methods=["GET", "POST"]) +def logout(): + keys = tuple(key for key in session.keys() if not key.startswith("_")) + for key in keys: + session.pop(key, default=None) + + return redirect("/") diff --git a/wqflask/wqflask/templates/base.html b/wqflask/wqflask/templates/base.html index fcd154c1..7145cd7a 100644 --- a/wqflask/wqflask/templates/base.html +++ b/wqflask/wqflask/templates/base.html @@ -116,8 +116,8 @@ {% endif %}