Add working microsoft login flow

This commit is contained in:
mcbloch 2022-04-20 01:27:52 +02:00
parent 0ace54a8fd
commit cc0c271a22
12 changed files with 145 additions and 50 deletions

1
.tool-versions Normal file
View file

@ -0,0 +1 @@
python 3.9.2

View file

@ -13,14 +13,13 @@ from flask_bootstrap import Bootstrap, StaticCDN
from flask_debugtoolbar import DebugToolbarExtension from flask_debugtoolbar import DebugToolbarExtension
from flask_login import LoginManager from flask_login import LoginManager
from flask_migrate import Migrate, MigrateCommand from flask_migrate import Migrate, MigrateCommand
from flask_oauthlib.client import OAuth, OAuthException
from flask_script import Manager, Server from flask_script import Manager, Server
from login import init_login from auth.login import init_login
from markupsafe import Markup from markupsafe import Markup
from models import db from models import db
from models.anonymous_user import AnonymouseUser from models.anonymous_user import AnonymouseUser
from utils import euro_string, price_range_string 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: def register_plugins(app: Flask) -> Manager:
@ -97,18 +96,21 @@ def add_routes(application: Flask) -> None:
# import views # TODO convert to blueprint # import views # TODO convert to blueprint
# import views.stats # 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.debug import debug_bp
from views.general import general_bp from views.general import general_bp
from views.order import order_bp from views.order import order_bp
from views.stats import stats_blueprint from views.stats import stats_blueprint
from zeus import oauth_bp
application.register_blueprint(general_bp, url_prefix="/") application.register_blueprint(general_bp, url_prefix="/")
application.register_blueprint(order_bp, url_prefix="/order") application.register_blueprint(order_bp, url_prefix="/order")
application.register_blueprint(stats_blueprint, url_prefix="/stats") application.register_blueprint(stats_blueprint, url_prefix="/stats")
application.register_blueprint(auth_bp, url_prefix="/") 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: if application.debug:
application.register_blueprint(debug_bp, url_prefix="/debug") application.register_blueprint(debug_bp, url_prefix="/debug")

View file

@ -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 import Blueprint, abort, redirect, session, url_for
from flask_login import current_user, logout_user from flask_login import current_user, logout_user
from models import User from models import User
from werkzeug.wrappers import Response from werkzeug.wrappers import Response
from zeus import zeus_login
auth_bp = Blueprint("auth_bp", __name__) auth_bp = Blueprint("auth_bp", __name__)
def init_login(app) -> None: def init_login(app) -> None:
"Initialize the login" """Initialize the login"""
# pylint: disable=W0612 # pylint: disable=W0612
@app.login_manager.user_loader @app.login_manager.user_loader
def load_user(userid) -> User: def load_user(userid) -> User:
"Load the user" """Load the user"""
return User.query.filter_by(id=userid).first() 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") @auth_bp.route("/logout")
def logout() -> Response: 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: if "zeus_token" in session:
session.pop("zeus_token", None) session.pop("zeus_token", None)
logout_user() logout_user()
@ -33,6 +27,6 @@ def logout() -> Response:
def before_request() -> None: 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(): if current_user.is_anonymous() or not current_user.is_allowed():
abort(401) abort(401)

77
app/auth/microsoft.py Normal file
View file

@ -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

View file

@ -4,24 +4,30 @@ import typing
from flask import (Blueprint, current_app, flash, redirect, request, session, from flask import (Blueprint, current_app, flash, redirect, request, session,
url_for) url_for)
from flask_login import login_user 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 models import User, db
from werkzeug.wrappers import Response from werkzeug.wrappers import Response
oauth_bp = Blueprint("oauth_bp", __name__) auth_zeus_bp = Blueprint("auth_zeus_bp", __name__)
def zeus_login(): def zeus_login():
"Log in using ZeusWPI" """Log in using ZeusWPI"""
return current_app.zeus.authorize( 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: def authorized() -> typing.Any:
# type is 'typing.Union[str, Response]', but this errors due to # type is 'typing.Union[str, Response]', but this errors due to
# https://github.com/python/mypy/issues/7187 # https://github.com/python/mypy/issues/7187
"Check authorized status" """Check authorized status"""
resp = current_app.zeus.authorized_response() resp = current_app.zeus.authorized_response()
if resp is None: if resp is None:
# pylint: disable=C0301 # pylint: disable=C0301
@ -45,8 +51,8 @@ def authorized() -> typing.Any:
return redirect(url_for("general_bp.home")) return redirect(url_for("general_bp.home"))
def init_oauth(app): def init_oauth(app) -> OAuthRemoteApp:
"Initialize the OAuth for ZeusWPI" """Initialize the OAuth for ZeusWPI"""
oauth = OAuth(app) oauth = OAuth(app)
zeus = oauth.remote_app( zeus = oauth.remote_app(
@ -69,13 +75,13 @@ def init_oauth(app):
def login_and_redirect_user(user) -> Response: 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) login_user(user)
return redirect(url_for("general_bp.home")) return redirect(url_for("general_bp.home"))
def create_user(username) -> User: def create_user(username) -> User:
"Create a temporary user if it is needed" """Create a temporary user if it is needed"""
user = User() user = User()
user.configure(username, False, 1) user.configure(username, False, 1)
db.session.add(user) db.session.add(user)

View file

@ -1,4 +1,4 @@
"An example for a Haldis config" """An example for a Haldis config"""
# config # config
@ -14,3 +14,5 @@ class Configuration:
LOGFILE = "haldis.log" LOGFILE = "haldis.log"
ZEUS_KEY = "tomtest" ZEUS_KEY = "tomtest"
ZEUS_SECRET = "blargh" ZEUS_SECRET = "blargh"
MICROSOFT_AUTH_ID = ""
MICROSOFT_AUTH_SECRET = ""

View file

@ -2,7 +2,9 @@
import add_admins import add_admins
from app import app_manager, db from app import create_app, db
app_manager = create_app()
entry_sets = { entry_sets = {
"admins": add_admins.add, "admins": add_admins.add,

View file

@ -3,11 +3,15 @@ from models import db
class User(db.Model): 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) id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False) username = db.Column(db.String(80), unique=True, nullable=False)
admin = db.Column(db.Boolean) admin = db.Column(db.Boolean)
bias = db.Column(db.Integer) 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( runs = db.relation(
"Order", "Order",
backref="courier", backref="courier",
@ -16,11 +20,12 @@ class User(db.Model):
) )
orderItems = db.relationship("OrderItem", backref="user", lazy="dynamic") orderItems = db.relationship("OrderItem", backref="user", lazy="dynamic")
def configure(self, username: str, admin: bool, bias: int) -> None: def configure(self, username: str, admin: bool, bias: int, microsoft_uuid: str = None) -> None:
"Configure the User" """Configure the User"""
self.username = username self.username = username
self.admin = admin self.admin = admin
self.bias = bias self.bias = bias
self.microsoft_uuid = microsoft_uuid
# pylint: disable=C0111, R0201 # pylint: disable=C0111, R0201
def is_authenticated(self) -> bool: def is_authenticated(self) -> bool:

View file

@ -81,7 +81,8 @@
</ul> </ul>
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
{% if current_user.is_anonymous() %} {% if current_user.is_anonymous() %}
<li><a href="{{ url_for('auth_bp.login') }}">Login</a></li> <li><a href="{{ url_for('auth_microsoft_bp.login') }}">Login with Microsoft</a></li>
<li><a href="{{ url_for('auth_zeus_bp.login') }}">Login with Zeus</a></li>
{% else %} {% else %}
<li><a href="{{ url_for('general_bp.profile') }}">{{ current_user.username }}</a></li> <li><a href="{{ url_for('general_bp.profile') }}">{{ current_user.username }}</a></li>
<li><a href="{{ url_for('auth_bp.logout') }}">Logout</a></li> <li><a href="{{ url_for('auth_bp.logout') }}">Logout</a></li>

View file

@ -12,7 +12,7 @@ E="${normal}"
if [ ! -d "venv" ]; then if [ ! -d "venv" ]; then
PYTHON_VERSION=$(cat .python-version) PYTHON_VERSION=$(cat .python-version)
echo -e "${B} No venv found, creating a new one with version ${PYTHON_VERSION} ${E}" echo -e "${B} No venv found, creating a new one with version ${PYTHON_VERSION} ${E}"
python3 -m virtualenv -p $PYTHON_VERSION venv python3 -m virtualenv -p "$PYTHON_VERSION" venv
fi fi
source venv/bin/activate source venv/bin/activate

View file

@ -12,3 +12,4 @@ black
pymysql pymysql
pyyaml pyyaml
tatsu<5.6 # >=5.6 needs Python >=3.8 tatsu<5.6 # >=5.6 needs Python >=3.8
microsoftgraph-python

View file

@ -1,5 +1,5 @@
# #
# This file is autogenerated by pip-compile # This file is autogenerated by pip-compile with python 3.9
# To update, run: # To update, run:
# #
# pip-compile # pip-compile
@ -24,6 +24,18 @@ click==7.1.2
# flask # flask
dominate==2.6.0 dominate==2.6.0
# via flask-bootstrap # via flask-bootstrap
flask==1.1.4
# via
# -r requirements.in
# flask-admin
# flask-bootstrap
# flask-debugtoolbar
# flask-login
# flask-migrate
# flask-oauthlib
# flask-script
# flask-sqlalchemy
# flask-wtf
flask-admin==1.5.8 flask-admin==1.5.8
# via -r requirements.in # via -r requirements.in
flask-bootstrap==3.3.7.1 flask-bootstrap==3.3.7.1
@ -44,18 +56,6 @@ flask-sqlalchemy==2.5.1
# flask-migrate # flask-migrate
flask-wtf==0.15.1 flask-wtf==0.15.1
# via -r requirements.in # via -r requirements.in
flask==1.1.4
# via
# -r requirements.in
# flask-admin
# flask-bootstrap
# flask-debugtoolbar
# flask-login
# flask-migrate
# flask-oauthlib
# flask-script
# flask-sqlalchemy
# flask-wtf
greenlet==1.1.0 greenlet==1.1.0
# via sqlalchemy # via sqlalchemy
idna==2.10 idna==2.10
@ -74,6 +74,8 @@ markupsafe==2.0.1
# jinja2 # jinja2
# mako # mako
# wtforms # wtforms
microsoftgraph-python==1.1.3
# via -r requirements.in
mypy-extensions==0.4.3 mypy-extensions==0.4.3
# via black # via black
oauthlib==2.1.0 oauthlib==2.1.0
@ -92,10 +94,12 @@ pyyaml==5.4.1
# via -r requirements.in # via -r requirements.in
regex==2021.4.4 regex==2021.4.4
# via black # via black
requests==2.25.1
# via
# microsoftgraph-python
# requests-oauthlib
requests-oauthlib==1.1.0 requests-oauthlib==1.1.0
# via flask-oauthlib # via flask-oauthlib
requests==2.25.1
# via requests-oauthlib
six==1.16.0 six==1.16.0
# via python-dateutil # via python-dateutil
sqlalchemy==1.4.18 sqlalchemy==1.4.18