From 7106d1cf18148304aa120f1186ba72d5dcce3a1c Mon Sep 17 00:00:00 2001 From: Maxim Stewart Date: Fri, 30 Oct 2020 18:25:34 -0500 Subject: [PATCH] Added OIDC IAM logic for Keycloak --- create_venv.sh | 40 +++++++++++++ linux-requirements.txt | 12 +--- src/core/__init__.py | 44 +++++++++++--- src/core/client_secrets.json | 14 +++++ src/core/forms.py | 1 - src/core/models.py | 1 + src/core/routes/Routes.py | 4 +- src/core/routes/__init__.py | 6 +- .../routes/pages/{Login.py => Flask_Login.py} | 10 ++-- .../pages/{Register.py => Flask_Register.py} | 12 ++-- src/core/routes/pages/LoginManager.py | 43 ++++++++++++++ src/core/routes/pages/OIDC_Login.py | 29 +++++++++ src/core/routes/pages/OIDC_Register.py | 32 ++++++++++ src/core/static/db/database.db | Bin 32768 -> 0 bytes src/core/utils/Logger.py | 56 ++++++++++++++++++ src/core/{ => utils}/MessageHandler.py | 0 src/core/utils/__init__.py | 2 + src/linux-start.sh | 5 +- src/socket_run.sh | 2 +- src/windows-start.sh | 5 +- src/wsgi.py | 2 +- windows-requirements.txt | 16 ++--- 22 files changed, 284 insertions(+), 52 deletions(-) create mode 100755 create_venv.sh create mode 100644 src/core/client_secrets.json rename src/core/routes/pages/{Login.py => Flask_Login.py} (84%) rename src/core/routes/pages/{Register.py => Flask_Register.py} (68%) create mode 100644 src/core/routes/pages/LoginManager.py create mode 100644 src/core/routes/pages/OIDC_Login.py create mode 100644 src/core/routes/pages/OIDC_Register.py delete mode 100644 src/core/static/db/database.db create mode 100644 src/core/utils/Logger.py rename src/core/{ => utils}/MessageHandler.py (100%) create mode 100644 src/core/utils/__init__.py diff --git a/create_venv.sh b/create_venv.sh new file mode 100755 index 0000000..e39ae87 --- /dev/null +++ b/create_venv.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +. CONFIG.sh + +# set -o xtrace ## To debug scripts +# set -o errexit ## To exit on error +# set -o errunset ## To exit if a variable is referenced but not set + + +function main() { + rm -rf venv/ + + clear + python -m venv venv/ + sleep 2 + source "./venv/bin/activate" + + ANSR="-1" + while [[ $ANSR != "0" ]] && [[ $ANSR != "1" ]] && [[ $ANSR != "2" ]]; do + clear + menu_mesage + read -p "--> : " ANSR + done + case $ANSR in + "1" ) pip install -r linux-requirements.txt;; + "2" ) pip install -r windows-requirements.txt;; + "0" ) exit;; + * ) echo "Don't know how you got here but that's a bad sign...";; + esac +} + +function menu_mesage() { + echo "NOTE: Make sure to have Python 3 installed!" + echo -e "\nWhat do you want to do?" + echo -e "\t1) Generate Linux/Mac supported venv. (Installs Repuirements)" + echo -e "\t2) Generate Windows supported venv. (Installs Repuirements)" + echo -e "\t0) EXIT" +} + +main $@; diff --git a/linux-requirements.txt b/linux-requirements.txt index 8e94e11..50c5c8b 100644 --- a/linux-requirements.txt +++ b/linux-requirements.txt @@ -1,18 +1,10 @@ -bcrypt==3.1.7 -cffi==1.14.0 -Click==7.0 Flask==1.1.1 -Flask-Bcrypt==0.7.1 +flask-oidc==1.4.0 Flask-Login==0.5.0 +Flask-Bcrypt==0.7.1 Flask-SQLAlchemy==2.4.1 Flask-WTF==0.14.3 gunicorn==19.9.0 -itsdangerous==1.1.0 -Jinja2==2.10.3 -MarkupSafe==1.1.1 -pkg-resources==0.0.0 -pycparser==2.20 -six==1.14.0 SQLAlchemy==1.3.11 Werkzeug==0.16.0 WTForms==2.2.1 diff --git a/src/core/__init__.py b/src/core/__init__.py index 6a26a51..dd2fcef 100644 --- a/src/core/__init__.py +++ b/src/core/__init__.py @@ -1,9 +1,13 @@ # Python imports -import secrets +import os, secrets +from datetime import timedelta # Lib imports from flask import Flask + #OIDC Login path +from flask_oidc import OpenIDConnect + # Flask Login Path from flask_bcrypt import Bcrypt from flask_login import current_user, login_user, logout_user, LoginManager @@ -11,21 +15,43 @@ from flask_login import current_user, login_user, logout_user, LoginManager # Apoplication imports + # Configs and 'init' +ROOT_FILE_PTH = os.path.dirname(os.path.realpath(__file__)) +# This path is submitted as the redirect URI in certain code flows. +# Change localhost%3A6969 to different port accordingly or change to your domain. +REDIRECT_LINK = "http%3A%2F%2Flocalhost%3A6969%2F" + app = Flask(__name__) -app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///static/db/database.db" -app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False -app.config['TITLE'] = ':::APP TITLE:::' - -# For csrf and some other stuff... -app.config['SECRET_KEY'] = secrets.token_hex(32) - +app.config.update({ + "TITLE": ':::APP TITLE:::', + 'DEBUG': False, + 'LOGIN_PATH': "FLASK_LOGIN", # Value can be OIDC or FLASK_LOGIN + 'SECRET_KEY': secrets.token_hex(32), # For csrf and some other stuff... + 'PERMANENT_SESSION_LIFETIME': timedelta(days = 7).total_seconds(), + 'SQLALCHEMY_DATABASE_URI': "sqlite:///static/db/database.db", + 'SQLALCHEMY_TRACK_MODIFICATIONS': False, + 'APP_REDIRECT_URI': REDIRECT_LINK, + 'OIDC_CLIENT_SECRETS': ROOT_FILE_PTH + '/client_secrets.json', + 'OIDC_ID_TOKEN_COOKIE_SECURE': True, # Only set false in development setups... + 'OIDC_REQUIRE_VERIFIED_EMAIL': False, + 'OIDC_USER_INFO_ENABLED': True, + 'OIDC_VALID_ISSUERS': [ + 'http://localhost:8080/auth/realms/apps', + 'https://localhost:443/auth/realms/apps' + ], + 'OIDC_TOKEN_TYPE_HINT': 'access_token' + }) +oidc = OpenIDConnect(app) login_manager = LoginManager(app) bcrypt = Bcrypt(app) + from core.models import db, User -db.init_app(app) +with app.app_context(): + db.create_all() + from core.forms import RegisterForm, LoginForm from core import routes diff --git a/src/core/client_secrets.json b/src/core/client_secrets.json new file mode 100644 index 0000000..2493f47 --- /dev/null +++ b/src/core/client_secrets.json @@ -0,0 +1,14 @@ +{ + "web": { + "auth_uri": "http://localhost:8080/auth/realms/apps/protocol/openid-connect/auth", + "client_id": "apps", + "issuer": "http://localhost:8080/auth/realms/apps", + "client_secret": "[ADD YOUR SECRET FROM THE REALM>CLIENTS>apps>Credentials Tab]", + "redirect_uris": [ + "http://localhost:6969/" + ], + "userinfo_uri": "http://localhost:8080/auth/realms/apps/protocol/openid-connect/userinfo", + "token_uri": "http://localhost:8080/auth/realms/apps/protocol/openid-connect/token", + "token_introspection_uri": "http://localhost:8080/auth/realms/apps/protocol/openid-connect/token/introspect" + } +} diff --git a/src/core/forms.py b/src/core/forms.py index 7b568c3..12ba62b 100644 --- a/src/core/forms.py +++ b/src/core/forms.py @@ -4,7 +4,6 @@ from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationE from core import User - class RegisterForm(FlaskForm): username = StringField('Username', validators=[DataRequired(), Length(min=4, max=24)]) email = StringField('Email', validators=[DataRequired(), Email()]) diff --git a/src/core/models.py b/src/core/models.py index 23795a1..e8e86f4 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -1,3 +1,4 @@ + from flask_sqlalchemy import SQLAlchemy from core import app, login_manager from flask_login import UserMixin diff --git a/src/core/routes/Routes.py b/src/core/routes/Routes.py index 00a1ef8..dc19763 100644 --- a/src/core/routes/Routes.py +++ b/src/core/routes/Routes.py @@ -4,8 +4,8 @@ from flask import request, render_template # App imports -from core import app, db # Get from __init__ -from core.MessageHandler import MessageHandler # Get simple message processor +from core import app, db # Get from __init__ +from core.utils import MessageHandler # Get simple message processor msgHandler = MessageHandler() diff --git a/src/core/routes/__init__.py b/src/core/routes/__init__.py index 57043cb..058bca0 100644 --- a/src/core/routes/__init__.py +++ b/src/core/routes/__init__.py @@ -1,2 +1,6 @@ from . import Routes -from .pages import Login, Register +from .pages import Flask_Login +from .pages import Flask_Register +from .pages import OIDC_Login +from .pages import OIDC_Register +from .pages import LoginManager diff --git a/src/core/routes/pages/Login.py b/src/core/routes/pages/Flask_Login.py similarity index 84% rename from src/core/routes/pages/Login.py rename to src/core/routes/pages/Flask_Login.py index a21e859..8ca8b78 100644 --- a/src/core/routes/pages/Login.py +++ b/src/core/routes/pages/Flask_Login.py @@ -6,14 +6,14 @@ from flask_login import current_user, login_user, logout_user # App imports from core import app, bcrypt, db, User, LoginForm -from core.MessageHandler import MessageHandler # Get simple message processor +from core.utils import MessageHandler # Get simple message processor msgHandler = MessageHandler() TITLE = app.config['TITLE'] -@app.route('/login', methods=['GET', 'POST']) -def login(): +@app.route('/app-login', methods=['GET', 'POST']) +def app_login(): if current_user.is_authenticated: return redirect(url_for("home")) @@ -31,8 +31,8 @@ def login(): return render_template('login.html', title=TITLE, form=_form) -@app.route('/logout') -def logout(): +@app.route('/app-logout') +def app_logout(): logout_user() flash("Logged out successfully!", "success") return redirect(url_for("home")) diff --git a/src/core/routes/pages/Register.py b/src/core/routes/pages/Flask_Register.py similarity index 68% rename from src/core/routes/pages/Register.py rename to src/core/routes/pages/Flask_Register.py index c6ebb50..39f9c12 100644 --- a/src/core/routes/pages/Register.py +++ b/src/core/routes/pages/Flask_Register.py @@ -6,26 +6,26 @@ from flask import request, render_template, url_for, redirect, flash # App imports from core import app, bcrypt, db, current_user, RegisterForm # Get from __init__ from core.models import User -from core.MessageHandler import MessageHandler # Get simple message processor +from core.utils import MessageHandler # Get simple message processor msgHandler = MessageHandler() TITLE = app.config['TITLE'] -@app.route('/register', methods=['GET', 'POST']) -def register(): +@app.route('/app-register', methods=['GET', 'POST']) +def app_register(): if current_user.is_authenticated: return redirect(url_for("home")) _form = RegisterForm() if _form.validate_on_submit(): hashed_password = bcrypt.generate_password_hash(_form.password.data).decode("utf-8") - user = User(username=_form.username.data, email=_form.email.data, password=hashed_password) + user = User(username = _form.username.data, email = _form.email.data, password = hashed_password) db.session.add(user) db.session.commit() flash("Account created successfully!", "success") return redirect(url_for("login")) return render_template('register.html', - title=TITLE, - form=_form) + title = TITLE, + form = _form) diff --git a/src/core/routes/pages/LoginManager.py b/src/core/routes/pages/LoginManager.py new file mode 100644 index 0000000..a8966fa --- /dev/null +++ b/src/core/routes/pages/LoginManager.py @@ -0,0 +1,43 @@ +# Python imports + +# Lib imports +from flask import redirect, url_for, flash + +# App imports +from core import app + + +ROUTE = app.config['LOGIN_PATH'] + + +@app.route('/login', methods=['GET', 'POST']) +def login(): + if ROUTE == "OIDC": + return redirect(url_for("oidc_login")) + if ROUTE == "FLASK_LOGIN": + return redirect(url_for("app_login")) + + flash("No Login Path Accessable! Please contact an Administrator!", "danger") + return redirect(url_for("home")) + + +@app.route('/logout') +def logout(): + if ROUTE == "OIDC": + return redirect(url_for("oidc_logout")) + if ROUTE == "FLASK_LOGIN": + return redirect(url_for("app_logout")) + + flash("No Logout Path Accessable! Please contact an Administrator!", "danger") + return redirect(url_for("home")) + + +@app.route('/register', methods=['GET', 'POST']) +def register(): + if ROUTE == "OIDC": + return redirect(url_for("oidc_register")) + if ROUTE == "FLASK_LOGIN": + return redirect(url_for("app_register")) + + flash("No Register Path Accessable! Please contact an Administrator!", "danger") + return redirect(url_for("home")) diff --git a/src/core/routes/pages/OIDC_Login.py b/src/core/routes/pages/OIDC_Login.py new file mode 100644 index 0000000..e580241 --- /dev/null +++ b/src/core/routes/pages/OIDC_Login.py @@ -0,0 +1,29 @@ +# Python imports + +# Lib imports +from flask import request, redirect, flash + + +# App imports +from ... import app, oidc + + +TITLE = app.config['TITLE'] + + +@app.route('/oidc-login', methods=['GET', 'POST']) +@oidc.require_login +def oidc_login(): + return redirect("/") + + +@app.route('/oidc-logout', methods=['GET', 'POST']) +@oidc.require_login +def oidc_logout(): + oidc.logout() + flash("Logged out successfully!", "success") + # NOTE: Need to redirect to logout on OIDC server to end session there too. + # If not, we can hit login url again and get same token until it expires. + return redirect( oidc.client_secrets.get('issuer') + + '/protocol/openid-connect/logout?redirect_uri=' + + app.config['APP_REDIRECT_URI']) diff --git a/src/core/routes/pages/OIDC_Register.py b/src/core/routes/pages/OIDC_Register.py new file mode 100644 index 0000000..9ac912b --- /dev/null +++ b/src/core/routes/pages/OIDC_Register.py @@ -0,0 +1,32 @@ +# Python imports + +# Lib imports +from flask import request, render_template, url_for, redirect, flash + +# App imports +from ... import app, oidc, db # Get from __init__ +from ...utils import MessageHandler # Get simple message processor + + +msgHandler = MessageHandler() +TITLE = app.config['TITLE'] + +@app.route('/oidc-register', methods=['GET', 'POST']) +def oidc_register(): + if oidc.user_loggedin: + return redirect("/home") + + _form = RegisterForm() + if _form.validate_on_submit(): + # NOTE: Do a requests api here maybe?? + + # hashed_password = bcrypt.generate_password_hash(_form.password.data).decode("utf-8") + # user = User(username=_form.username.data, password=hashed_password) + # db.session.add(user) + # db.session.commit() + flash("Account created successfully!", "success") + return redirect("/login") + + return render_template('register.html', + title = TITLE, + form = _form) diff --git a/src/core/static/db/database.db b/src/core/static/db/database.db deleted file mode 100644 index a720c244027f77664fe0660b924d56c1b450e91e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI)O=}ZD7{Kw_B<4+$Erm1}4?{0)X%V&Fq+nVGiKg2&?t(dOS2Kk`UTii&ulC-r zB7O}IUcC5Gdi9yo9ulPX5FyBa!;{R;%+5aZo71pl-=4I36RXcg<3TdfyJ|&wo_eXZ zQmPSl8t{N+UwHo`s{I8Xt^7($pRWu=h00Iag zfB*srAbWR(m;H9QY_SKHSgkJlDzNRD79l3nS-`zghq#r z{g%<|u4-LxmP@BoDGif>JuQXiU8sX2iKx}mQP4bz%#QT1gQVA=Yj*V?lQjJ_8h8KD z)RW3)5Smw}t&iKy!$!NK-y$5Z>IPymg$-*SEL5&UM1+d0)22aca+} zcKE^0o#tkyG1s{1=CLDJZpmuAUd)PYl=Jl$HBa@1UHkdwe#vw)x;~E`WBkm0KiDY! zwc}IQFr2q1s}0tg_0 z00IagfB*su5^&G|J^xDiSN@L$4aqPFAbBxSldToq z9{_m%e?KsKA%Fk^2q1s}0tg_000Iag@E-_dWDf3Kf9{=suFQb|0tg_000IagfB*sr zAb: IE : + # Note: Can replace 127.0.0.1 with 0.0.0.0 to make it 'network/internet' accessable... + # Note 2: Keycloak uses 8080. Change it or keep this as is. + gunicorn wsgi:app -b 127.0.0.1:6969 # : IE : } main $@; diff --git a/src/socket_run.sh b/src/socket_run.sh index e743565..bce0a05 100755 --- a/src/socket_run.sh +++ b/src/socket_run.sh @@ -9,7 +9,7 @@ function main() { SCRIPTPATH="$( cd "$(dirname "")" >/dev/null 2>&1 ; pwd -P )" cd "${SCRIPTPATH}" echo "Working Dir: " $(pwd) - source "../venv/Scripts/activate" + source "../venv/bin/activate" gunicorn --bind unix:/tmp/app.sock wsgi:app } main $@; diff --git a/src/windows-start.sh b/src/windows-start.sh index 4a409c8..c38a23e 100644 --- a/src/windows-start.sh +++ b/src/windows-start.sh @@ -7,7 +7,8 @@ function main() { source "../venv/Scripts/activate" - # Note can replace 127.0.0.1 with 0.0.0.0 to make it 'network/internet' accessable... - waitress-serve --listen=127.0.0.1:8080 wsgi:app # : IE : + # Note: Can replace 127.0.0.1 with 0.0.0.0 to make it 'network/internet' accessable... + # Note 2: Keycloak uses 8080. Change it or keep this as is. + waitress-serve --listen=127.0.0.1:6969 wsgi:app # : IE : } main $@; diff --git a/src/wsgi.py b/src/wsgi.py index 03ea9cb..5681760 100644 --- a/src/wsgi.py +++ b/src/wsgi.py @@ -1,4 +1,4 @@ from core import app if __name__ == '__main__': - app.run(debug=True) + app.run(debug=False) diff --git a/windows-requirements.txt b/windows-requirements.txt index 7c97465..165ff8f 100644 --- a/windows-requirements.txt +++ b/windows-requirements.txt @@ -1,18 +1,10 @@ -bcrypt==3.1.7 -cffi==1.14.0 -Click==7.0 Flask==1.1.1 -Flask-Bcrypt==0.7.1 +flask-oidc==1.4.0 Flask-Login==0.5.0 +Flask-Bcrypt==0.7.1 Flask-SQLAlchemy==2.4.1 Flask-WTF==0.14.3 -itsdangerous==1.1.0 -Jinja2==2.10.3 -MarkupSafe==1.1.1 -pkg-resources==0.0.0 -pycparser==2.20 -six==1.14.0 -SQLAlchemy==1.3.11 waitress==1.4.3 +SQLAlchemy==1.3.11 Werkzeug==0.16.0 -WTForms==2.2.1 +WTForms==2.2.1 \ No newline at end of file