Added OIDC IAM logic for Keycloak
This commit is contained in:
parent
3fd6b0a8e4
commit
7106d1cf18
40
create_venv.sh
Executable file
40
create_venv.sh
Executable 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 $@;
|
@ -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
|
||||
|
@ -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
|
||||
|
14
src/core/client_secrets.json
Normal file
14
src/core/client_secrets.json
Normal 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"
|
||||
}
|
||||
}
|
@ -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()])
|
||||
|
@ -1,3 +1,4 @@
|
||||
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from core import app, login_manager
|
||||
from flask_login import UserMixin
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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"))
|
@ -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)
|
43
src/core/routes/pages/LoginManager.py
Normal file
43
src/core/routes/pages/LoginManager.py
Normal 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"))
|
29
src/core/routes/pages/OIDC_Login.py
Normal file
29
src/core/routes/pages/OIDC_Login.py
Normal 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'])
|
32
src/core/routes/pages/OIDC_Register.py
Normal file
32
src/core/routes/pages/OIDC_Register.py
Normal 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
56
src/core/utils/Logger.py
Normal 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
|
2
src/core/utils/__init__.py
Normal file
2
src/core/utils/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .Logger import Logger
|
||||
from .MessageHandler import MessageHandler
|
@ -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 $@;
|
||||
|
@ -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 $@;
|
||||
|
@ -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 $@;
|
||||
|
@ -1,4 +1,4 @@
|
||||
from core import app
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
||||
app.run(debug=False)
|
||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user