Added OIDC IAM logic for Keycloak

This commit is contained in:
Maxim Stewart 2020-10-30 18:25:34 -05:00
parent 3fd6b0a8e4
commit 7106d1cf18
22 changed files with 284 additions and 52 deletions

40
create_venv.sh Executable file
View File

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

View File

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

View File

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

View File

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

View File

@ -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()])

View File

@ -1,3 +1,4 @@
from flask_sqlalchemy import SQLAlchemy
from core import app, login_manager
from flask_login import UserMixin

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'])

View File

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

Binary file not shown.

56
src/core/utils/Logger.py Normal file
View File

@ -0,0 +1,56 @@
# Python imports
import os, logging
# Application imports
class Logger:
def __init__(self):
pass
def get_logger(self, loggerName = "NO_LOGGER_NAME_PASSED", createFile = True):
"""
Create a new logging object and return it.
:note:
NOSET # Don't know the actual log level of this... (defaulting or literally none?)
Log Levels (From least to most)
Type Value
CRITICAL 50
ERROR 40
WARNING 30
INFO 20
DEBUG 10
:param loggerName: Sets the name of the logger object. (Used in log lines)
:param createFile: Whether we create a log file or just pump to terminal
:return: the logging object we created
"""
globalLogLvl = logging.DEBUG # Keep this at highest so that handlers can filter to their desired levels
chLogLevel = logging.CRITICAL # Prety musch the only one we change ever
fhLogLevel = logging.DEBUG
log = logging.getLogger(loggerName)
log.setLevel(globalLogLvl)
# Set our log output styles
fFormatter = logging.Formatter('[%(asctime)s] %(pathname)s:%(lineno)d %(levelname)s - %(message)s', '%m-%d %H:%M:%S')
cFormatter = logging.Formatter('%(pathname)s:%(lineno)d] %(levelname)s - %(message)s')
ch = logging.StreamHandler()
ch.setLevel(level=chLogLevel)
ch.setFormatter(cFormatter)
log.addHandler(ch)
if createFile:
folder = "logs"
file = folder + "/flask-application.log"
if not os.path.exists(folder):
os.mkdir(folder)
fh = logging.FileHandler(file)
fh.setLevel(level=fhLogLevel)
fh.setFormatter(fFormatter)
log.addHandler(fh)
return log

View File

@ -0,0 +1,2 @@
from .Logger import Logger
from .MessageHandler import MessageHandler

View File

@ -10,7 +10,8 @@ function main() {
cd "${SCRIPTPATH}"
echo "Working Dir: " $(pwd)
source "../venv/bin/activate"
# Note can replace 127.0.0.1 with 0.0.0.0 to make it 'network/internet' accessable...
gunicorn wsgi:app -b 127.0.0.1:8080 # <module>:<app> IE <file>:<flask app variable>
# 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 # <module>:<app> IE <file>:<flask app variable>
}
main $@;

View File

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

View File

@ -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 # <module>:<app> IE <file>:<flask app variable>
# 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 # <module>:<app> IE <file>:<flask app variable>
}
main $@;

View File

@ -1,4 +1,4 @@
from core import app
if __name__ == '__main__':
app.run(debug=True)
app.run(debug=False)

View File

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