98 lines
3.8 KiB
Python
98 lines
3.8 KiB
Python
# 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)
|