From cc0c271a22b33241c268e25a12a1ef373ed5d45d Mon Sep 17 00:00:00 2001 From: mcbloch Date: Wed, 20 Apr 2022 01:27:52 +0200 Subject: [PATCH 1/8] Add working microsoft login flow --- .tool-versions | 1 + app/app.py | 14 ++++--- app/{ => auth}/login.py | 18 +++------ app/auth/microsoft.py | 77 +++++++++++++++++++++++++++++++++++++++ app/{ => auth}/zeus.py | 26 ++++++++----- app/config.example.py | 4 +- app/create_database.py | 4 +- app/models/user.py | 11 ++++-- app/templates/layout.html | 3 +- first-setup.sh | 2 +- requirements.in | 1 + requirements.txt | 34 +++++++++-------- 12 files changed, 145 insertions(+), 50 deletions(-) create mode 100644 .tool-versions rename app/{ => auth}/login.py (67%) create mode 100644 app/auth/microsoft.py rename app/{ => auth}/zeus.py (78%) diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..6826aa8 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +python 3.9.2 diff --git a/app/app.py b/app/app.py index 8a7cc64..72415f6 100755 --- a/app/app.py +++ b/app/app.py @@ -13,14 +13,13 @@ from flask_bootstrap import Bootstrap, StaticCDN from flask_debugtoolbar import DebugToolbarExtension from flask_login import LoginManager from flask_migrate import Migrate, MigrateCommand -from flask_oauthlib.client import OAuth, OAuthException from flask_script import Manager, Server -from login import init_login +from auth.login import init_login from markupsafe import Markup from models import db from models.anonymous_user import AnonymouseUser from utils import euro_string, price_range_string -from zeus import init_oauth +from auth.zeus import init_oauth def register_plugins(app: Flask) -> Manager: @@ -97,18 +96,21 @@ def add_routes(application: Flask) -> None: # import views # TODO convert to blueprint # import views.stats # TODO convert to blueprint - from login import auth_bp + from auth.login import auth_bp + from auth.microsoft import auth_microsoft_bp + from auth.zeus import auth_zeus_bp from views.debug import debug_bp from views.general import general_bp from views.order import order_bp from views.stats import stats_blueprint - from zeus import oauth_bp application.register_blueprint(general_bp, url_prefix="/") application.register_blueprint(order_bp, url_prefix="/order") application.register_blueprint(stats_blueprint, url_prefix="/stats") application.register_blueprint(auth_bp, url_prefix="/") - application.register_blueprint(oauth_bp, url_prefix="/") + application.register_blueprint(auth_microsoft_bp, + url_prefix="/users/auth/microsoft_graph_auth") # "/auth/microsoft") + application.register_blueprint(auth_zeus_bp, url_prefix="/auth/zeus") if application.debug: application.register_blueprint(debug_bp, url_prefix="/debug") diff --git a/app/login.py b/app/auth/login.py similarity index 67% rename from app/login.py rename to app/auth/login.py index a7382fa..0cc621f 100644 --- a/app/login.py +++ b/app/auth/login.py @@ -1,31 +1,25 @@ -"Script for everything related to logging in and out" +"""Script for everything related to logging in and out""" from flask import Blueprint, abort, redirect, session, url_for from flask_login import current_user, logout_user from models import User from werkzeug.wrappers import Response -from zeus import zeus_login auth_bp = Blueprint("auth_bp", __name__) def init_login(app) -> None: - "Initialize the login" + """Initialize the login""" + # pylint: disable=W0612 @app.login_manager.user_loader def load_user(userid) -> User: - "Load the user" + """Load the user""" return User.query.filter_by(id=userid).first() -@auth_bp.route("/login") -def login(): - "Function to handle a user trying to log in" - return zeus_login() - - @auth_bp.route("/logout") def logout() -> Response: - "Function to handle a user trying to log out" + """Function to handle a user trying to log out""" if "zeus_token" in session: session.pop("zeus_token", None) logout_user() @@ -33,6 +27,6 @@ def logout() -> Response: def before_request() -> None: - "Function for what has to be done before a request" + """Function for what has to be done before a request""" if current_user.is_anonymous() or not current_user.is_allowed(): abort(401) diff --git a/app/auth/microsoft.py b/app/auth/microsoft.py new file mode 100644 index 0000000..d754a34 --- /dev/null +++ b/app/auth/microsoft.py @@ -0,0 +1,77 @@ +import typing + +from flask import Blueprint, url_for, request, redirect, flash, Response +from flask_login import login_user +from microsoftgraph.client import Client + +from config import Configuration +from models import User, db + +auth_microsoft_bp = Blueprint("auth_microsoft_bp", __name__) + +client = Client(Configuration.MICROSOFT_AUTH_ID, + Configuration.MICROSOFT_AUTH_SECRET, + account_type='common') # by default common, thus account_type is optional parameter. + + +def microsoft_login(): + """Log in using Microsoft""" + scope = ["openid", "profile", "User.Read", "User.Read.All"] + url = client.authorization_url(url_for("auth_microsoft_bp.authorized", _external=True), scope, state=None) + return redirect(url) + + +@auth_microsoft_bp.route("/login") +def login(): + """Function to handle a user trying to log in""" + return microsoft_login() + + +@auth_microsoft_bp.route("callback") # "/authorized") +def authorized() -> typing.Any: + # type is 'typing.Union[str, Response]', but this errors due to + # https://github.com/python/mypy/issues/7187 + """Check authorized status""" + + oauth_code = request.args['code'] + + resp = client.exchange_code(url_for("auth_microsoft_bp.authorized", _external=True), oauth_code) + + # access_token = resp.data['access_token'] + # id_token = resp.data['id_token'] + # expires_in = resp.data['expires_in'] + + client.set_token(resp.data) + + resp = client.users.get_me() + print(resp.data) + + username = resp.data['userPrincipalName'] + microsoft_uuid = resp.data['id'] + + user = User.query.filter_by(username=username).first() + + if username and user: + return login_and_redirect_user(user) + elif username: + # TODO Save 'ugent_username' or something similar + user = create_user(username, microsoft_uuid) + return login_and_redirect_user(user) + + flash("You're not allowed to enter, please contact a system administrator") + return redirect(url_for("general_bp.home")) + + +def login_and_redirect_user(user) -> Response: + """Log in the user and then redirect them""" + login_user(user) + return redirect(url_for("general_bp.home")) + + +def create_user(username, microsoft_uuid) -> User: + """Create a temporary user if it is needed""" + user = User() + user.configure(username, False, 1, microsoft_uuid) + db.session.add(user) + db.session.commit() + return user diff --git a/app/zeus.py b/app/auth/zeus.py similarity index 78% rename from app/zeus.py rename to app/auth/zeus.py index ebc16ab..35f6ec0 100644 --- a/app/zeus.py +++ b/app/auth/zeus.py @@ -4,24 +4,30 @@ import typing from flask import (Blueprint, current_app, flash, redirect, request, session, url_for) from flask_login import login_user -from flask_oauthlib.client import OAuth, OAuthException +from flask_oauthlib.client import OAuth, OAuthException, OAuthRemoteApp from models import User, db from werkzeug.wrappers import Response -oauth_bp = Blueprint("oauth_bp", __name__) +auth_zeus_bp = Blueprint("auth_zeus_bp", __name__) def zeus_login(): - "Log in using ZeusWPI" + """Log in using ZeusWPI""" return current_app.zeus.authorize( - callback=url_for("oauth_bp.authorized", _external=True)) + callback=url_for("auth_zeus_bp.authorized", _external=True)) -@oauth_bp.route("/login/zeus/authorized") +@auth_zeus_bp.route("/login") +def login(): + """Function to handle a user trying to log in""" + return zeus_login() + + +@auth_zeus_bp.route("/authorized") def authorized() -> typing.Any: # type is 'typing.Union[str, Response]', but this errors due to # https://github.com/python/mypy/issues/7187 - "Check authorized status" + """Check authorized status""" resp = current_app.zeus.authorized_response() if resp is None: # pylint: disable=C0301 @@ -45,8 +51,8 @@ def authorized() -> typing.Any: return redirect(url_for("general_bp.home")) -def init_oauth(app): - "Initialize the OAuth for ZeusWPI" +def init_oauth(app) -> OAuthRemoteApp: + """Initialize the OAuth for ZeusWPI""" oauth = OAuth(app) zeus = oauth.remote_app( @@ -69,13 +75,13 @@ def init_oauth(app): def login_and_redirect_user(user) -> Response: - "Log in the user and then redirect them" + """Log in the user and then redirect them""" login_user(user) return redirect(url_for("general_bp.home")) def create_user(username) -> User: - "Create a temporary user if it is needed" + """Create a temporary user if it is needed""" user = User() user.configure(username, False, 1) db.session.add(user) diff --git a/app/config.example.py b/app/config.example.py index daac1ab..5c1110a 100644 --- a/app/config.example.py +++ b/app/config.example.py @@ -1,4 +1,4 @@ -"An example for a Haldis config" +"""An example for a Haldis config""" # config @@ -14,3 +14,5 @@ class Configuration: LOGFILE = "haldis.log" ZEUS_KEY = "tomtest" ZEUS_SECRET = "blargh" + MICROSOFT_AUTH_ID = "" + MICROSOFT_AUTH_SECRET = "" diff --git a/app/create_database.py b/app/create_database.py index df07727..58ed446 100644 --- a/app/create_database.py +++ b/app/create_database.py @@ -2,7 +2,9 @@ import add_admins -from app import app_manager, db +from app import create_app, db + +app_manager = create_app() entry_sets = { "admins": add_admins.add, diff --git a/app/models/user.py b/app/models/user.py index 8e78b2f..8467048 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -3,11 +3,15 @@ from models import db class User(db.Model): - "Class used for configuring the User model in the database" + """Class used for configuring the User model in the database""" id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) admin = db.Column(db.Boolean) bias = db.Column(db.Integer) + # Microsoft OAUTH info + microsoft_uuid = db.Column(db.String(120), unique=True) + ugent_username = db.Column(db.String(80), unique=True) + # Relations runs = db.relation( "Order", backref="courier", @@ -16,11 +20,12 @@ class User(db.Model): ) orderItems = db.relationship("OrderItem", backref="user", lazy="dynamic") - def configure(self, username: str, admin: bool, bias: int) -> None: - "Configure the User" + def configure(self, username: str, admin: bool, bias: int, microsoft_uuid: str = None) -> None: + """Configure the User""" self.username = username self.admin = admin self.bias = bias + self.microsoft_uuid = microsoft_uuid # pylint: disable=C0111, R0201 def is_authenticated(self) -> bool: diff --git a/app/templates/layout.html b/app/templates/layout.html index 72b85b7..9ba62c9 100644 --- a/app/templates/layout.html +++ b/app/templates/layout.html @@ -81,7 +81,8 @@