This is a repo for the codes and scripts used for the project
your-project/
├── .venv/ # Python virtual environment (created by you)
├── app.py # Your main Flask application
├── requirements.txt # Dependencies file
├── templates/
│ ├── base.html
│ ├── index.html
│ ├── dashboard.html
│ ├── admin.html
│ └── error.html
└── static/
├── css/
│ └── style.css
└── js/
└── main.js
creating an isolated virtual environment for the app
# Navigate to your project directory
cd /path/to/your-project
# Create virtual environment
python3 -m venv .venv
# Activate virtual environment
source .venv/bin/activate
Create a file named requirements.txt in your project root
Flask==3.0.0
PyJWT==2.8.0
requests==2.31.0
Werkzeug==3.0.1
cryptography==41.0.7
install the dependencies that were placed in the requirements file
# Make sure your virtual environment is activated (you should see (.venv) in your terminal)
pip install -r requirements.txt
import os
import jwt
import requests
from functools import wraps
import urllib.parse
from flask import Flask, render_template, request, redirect, session, jsonify, url_for, abort
from werkzeug.middleware.proxy_fix import ProxyFix
# --- Configuration Constants ---
KEYCLOAK_DOMAIN = "<KEYLOAK-IP>:8080"
REALM_NAME = "your-realm-name"
CLIENT_ID = "your-client-name"
PUBLIC_APP_HOST = "your-public-app-host"
# --- Flask App Initialization ---
app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
# Secret key for session management
app.config['SECRET_KEY'] = os.environ.get('FLASK_SECRET', 'a-very-secret-and-random-key')
# --- Helper Functions ---
def get_user_email():
"""Extracts the authenticated user's email."""
return request.headers.get("X-Email")
_jwks = None
def get_jwks():
global _jwks
if not _jwks:
jwks_url = f"http://{KEYCLOAK_DOMAIN}/realms/{REALM_NAME}/protocol/openid-connect/certs"
_jwks = requests.get(jwks_url).json()
return _jwks
def verify_access_token(token):
jwks = get_jwks()
header = jwt.get_unverified_header(token)
key = next(k for k in jwks["keys"] if k["kid"] == header["kid"])
public_key = jwt.algorithms.RSAAlgorithm.from_jwk(key)
decoded = jwt.decode(
token,
public_key,
algorithms=["RS256"],
issuer=f"http://{KEYCLOAK_DOMAIN}/realms/{REALM_NAME}",
options={"verify_aud": False},
)
return decoded
## Authorization Dceorator ##
def require_role(role_name):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
token = request.headers.get("X-Access-Token")
if not token:
abort(401)
claims = verify_access_token(token)
roles = (
claims
.get("resource_access", {})
.get(CLIENT_ID, {})
.get("roles", [])
)
if role_name not in roles:
abort(403)
return fn(*args, **kwargs)
return wrapper
return decorator
# --- Routes ---
@app.route("/")
def index():
return render_template("index.html")
@app.route("/protected")
def protected_dashboard():
"""
Protected endpoint. If the proxy authenticates the user, the headers
will be present, and the user's email will be displayed.
If the user is unauthorized, the proxy redirects them to login.
"""
user_email = get_user_email()
if not user_email:
return redirect(f"http://{request.host}/oauth2/sign_in")
return render_template(
"dashboard.html",
email=user_email,
logout_url=url_for('logout')
)
@app.route("/admin")
@require_role("app1-admin")
def admin():
token = request.headers.get("X-Access-Token")
claims = verify_access_token(token)
roles = claims['resource_access'][CLIENT_ID]['roles']
user_email = claims.get("email")
return render_template(
"admin.html",
email=user_email,
roles=roles,
logout_url=url_for('logout')
)
# error handlers
@app.errorhandler(401)
def unauthorized(e):
return render_template("error.html",
error_code="401",
error_title="Unauthorized",
error_message="You need to be authenticated to access this resource."), 401
@app.errorhandler(403)
def forbidden(e):
return render_template("error.html",
error_code="403",
error_title="Forbidden",
error_message="You don't have permission to access this resource."), 403
@app.errorhandler(404)
def not_found(e):
return render_template("error.html",
error_code="404",
error_title="Not Found",
error_message="The page you're looking for doesn't exist."), 404
@app.route("/logout", methods=["GET", "POST"])
def logout():
session.clear()
current_app_host = PUBLIC_APP_HOST
keycloak_domain = KEYCLOAK_DOMAIN
realm_name = REALM_NAME
# 1. Build the Return-To-App URL (Where Keycloak sends user after logout)
post_logout_redirect = f"http://{current_app_host}/"
# 2. Build the Keycloak Logout URL
keycloak_logout_url = f"http://{keycloak_domain}/realms/{realm_name}/protocol/openid-connect/logout"
# Add the redirect param to Keycloak URL
keycloak_params = urllib.parse.urlencode({
"post_logout_redirect_uri": post_logout_redirect,
"client_id": CLIENT_ID
})
full_keycloak_url = f"{keycloak_logout_url}?{keycloak_params}"
# 3. Build the Proxy Logout URL
# We send the user to /oauth2/sign_out on the CURRENT app host
oauth2_proxy_logout_url = f"http://{current_app_host}/oauth2/sign_out"
# We encode the Keycloak URL and attach it as the 'rd' (redirect) parameter
final_logout_url = f"{oauth2_proxy_logout_url}?rd={urllib.parse.quote(full_keycloak_url)}"
return redirect(final_logout_url)
#######################################
@app.route("/health")
def health():
return jsonify({"status": "ok"}), 200
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000)
templates/base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Expadox Labs{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<link rel="preconnect" href="<https://fonts.googleapis.com>">
<link rel="preconnect" href="<https://fonts.gstatic.com>" crossorigin>
<link href="<https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Playfair+Display:wght@700;900&display=swap>" rel="stylesheet">
{% block extra_css %}{% endblock %}
</head>
<body>
{% block content %}{% endblock %}
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
templates/index.html