# Arian Nasr # 2026-02-25 import os import functools from flask import Flask, render_template, request, redirect, url_for, session from authlib.integrations.flask_client import OAuth # noqa from dotenv import load_dotenv load_dotenv() app = Flask(__name__) # ── Security ────────────────────────────────────────────────────────────────── app.secret_key = os.environ.get("SECRET_KEY", "CHANGE_ME_IN_PRODUCTION") app.config["SESSION_COOKIE_HTTPONLY"] = True app.config["SESSION_COOKIE_SAMESITE"] = "Lax" # Set SESSION_COOKIE_SECURE = True when serving over HTTPS # ── Authentik OIDC config ───────────────────────────────────────────────────── AUTHENTIK_URL = os.environ.get("AUTHENTIK_URL", "https://auth.example.com") AUTHENTIK_APP_SLUG = os.environ.get("AUTHENTIK_APP_SLUG", "my-app") oauth = OAuth(app) oauth.register( name="authentik", client_id=os.environ.get("AUTHENTIK_CLIENT_ID"), client_secret=os.environ.get("AUTHENTIK_CLIENT_SECRET"), # Authentik exposes a per-application OIDC discovery document server_metadata_url=( f"{AUTHENTIK_URL}/application/o/{AUTHENTIK_APP_SLUG}/.well-known/openid-configuration" ), client_kwargs={"scope": "openid email profile"}, ) # ── Helpers ─────────────────────────────────────────────────────────────────── def login_required(f): """Decorator that redirects unauthenticated users to /login.""" @functools.wraps(f) def decorated(*args, **kwargs): if not session.get("user"): return redirect(url_for("login", next=request.url)) return f(*args, **kwargs) return decorated # ── Routes ──────────────────────────────────────────────────────────────────── @app.route("/") def index(): user = session.get("user") return render_template("index.html", user=user) @app.route("/login") def login(): redirect_uri = url_for("callback", _external=True) return oauth.authentik.authorize_redirect(redirect_uri) @app.route("/callback") def callback(): token = oauth.authentik.authorize_access_token() # authlib parses and verifies the ID token automatically; # userinfo claims are available directly on the token dict. user = token.get("userinfo") if user is None: # Fall back to the userinfo endpoint if claims aren't in the token user = oauth.authentik.userinfo() session["user"] = dict(user) # Honour the 'next' redirect set by login_required, defaulting to home next_url = request.args.get("next") or url_for("index") return redirect(next_url) @app.route("/logout") def logout(): session.clear() # Redirect to Authentik's end-session endpoint so the SSO session is # also terminated. The post_logout_redirect_uri must be registered in # the Authentik provider settings. end_session_url = ( f"{AUTHENTIK_URL}/application/o/{AUTHENTIK_APP_SLUG}/end-session/" ) return redirect(end_session_url) # ── Protected example ───────────────────────────────────────────────────────── @app.route("/dashboard") @login_required def dashboard(): """Example of a route that requires authentication.""" return render_template("dashboard.html", user=session["user"]) if __name__ == "__main__": app.run(debug=True)