oidc example auth

This commit is contained in:
Arian Nasr
2026-02-25 21:27:34 -05:00
parent b7844558ac
commit ea66a750c9
7 changed files with 199 additions and 7 deletions

13
.env.example Normal file
View File

@@ -0,0 +1,13 @@
# Flask secret key — generate with: python -c "import secrets; print(secrets.token_hex(32))"
SECRET_KEY=replace_with_random_secret
# Authentik base URL (no trailing slash)
AUTHENTIK_URL=https://auth.example.com
# The slug shown in Authentik under Applications → <your app> → Slug
AUTHENTIK_APP_SLUG=my-app
# OAuth2 credentials from Authentik → Providers → <your provider>
AUTHENTIK_CLIENT_ID=your_client_id_here
AUTHENTIK_CLIENT_SECRET=your_client_secret_here

8
.gitignore vendored
View File

@@ -1 +1,9 @@
/.idea/ /.idea/
.env
__pycache__/
*.pyc
*.pyo
.venv/
venv/
instance/
INFO.md

97
app.py
View File

@@ -1,12 +1,97 @@
from flask import Flask # 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__) 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
@app.route('/') # ── Authentik OIDC config ─────────────────────────────────────────────────────
def hello_world(): # put application's code here AUTHENTIK_URL = os.environ.get("AUTHENTIK_URL", "https://auth.example.com")
return 'Hello World!' 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"},
)
if __name__ == '__main__': # ── Helpers ───────────────────────────────────────────────────────────────────
app.run() 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)

4
requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
Flask==3.1.3
Authlib==1.6.8
requests==2.32.5
python-dotenv==1.0.1

37
templates/base.html Normal file
View File

@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}BoxDrop{% endblock %}</title>
<style>
body { font-family: sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
nav { display: flex; justify-content: space-between; align-items: center;
border-bottom: 1px solid #ddd; padding-bottom: 0.75rem; margin-bottom: 2rem; }
a { color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
.btn { display: inline-block; padding: 0.4rem 1rem; border-radius: 4px;
background: #0066cc; color: #fff; }
.btn:hover { background: #0052a3; text-decoration: none; }
.btn-outline { background: transparent; border: 1px solid #0066cc; color: #0066cc; }
.btn-outline:hover { background: #e8f0fe; }
</style>
</head>
<body>
<nav>
<strong><a href="{{ url_for('index') }}">BoxDrop</a></strong>
<span>
{% if user %}
Signed in as <strong>{{ user.get('name') or user.get('email') }}</strong>
&nbsp;
<a class="btn btn-outline" href="{{ url_for('logout') }}">Log out</a>
{% else %}
<a class="btn" href="{{ url_for('login') }}">Log in with Authentik</a>
{% endif %}
</span>
</nav>
{% block content %}{% endblock %}
</body>
</html>

25
templates/dashboard.html Normal file
View File

@@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block title %}Dashboard BoxDrop{% endblock %}
{% block content %}
<h1>Dashboard</h1>
<p>This page is only visible to authenticated users.</p>
<table style="border-collapse:collapse;width:100%">
<thead>
<tr style="background:#f4f4f4">
<th style="text-align:left;padding:0.5rem 1rem;border:1px solid #ddd">Claim</th>
<th style="text-align:left;padding:0.5rem 1rem;border:1px solid #ddd">Value</th>
</tr>
</thead>
<tbody>
{% for key, value in user.items() %}
<tr>
<td style="padding:0.5rem 1rem;border:1px solid #ddd"><code>{{ key }}</code></td>
<td style="padding:0.5rem 1rem;border:1px solid #ddd">{{ value }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

20
templates/index.html Normal file
View File

@@ -0,0 +1,20 @@
{% extends "base.html" %}
{% block title %}Home BoxDrop{% endblock %}
{% block content %}
{% if user %}
<h1>Welcome back, {{ user.get('name') or user.get('email') }}! 👋</h1>
<p>You are authenticated via Authentik.</p>
<p><a class="btn" href="{{ url_for('dashboard') }}">Go to Dashboard</a></p>
<details style="margin-top:2rem">
<summary>Raw token claims</summary>
<pre style="background:#f4f4f4;padding:1rem;border-radius:4px;overflow:auto">{{ user | tojson(indent=2) }}</pre>
</details>
{% else %}
<h1>BoxDrop</h1>
<p>Please log in to continue.</p>
<p><a class="btn" href="{{ url_for('login') }}">Log in with Authentik</a></p>
{% endif %}
{% endblock %}