oidc example auth
This commit is contained in:
13
.env.example
Normal file
13
.env.example
Normal 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
|
||||
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1 +1,9 @@
|
||||
/.idea/
|
||||
/.idea/
|
||||
.env
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.venv/
|
||||
venv/
|
||||
instance/
|
||||
INFO.md
|
||||
97
app.py
97
app.py
@@ -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__)
|
||||
|
||||
# ── 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('/')
|
||||
def hello_world(): # put application's code here
|
||||
return 'Hello World!'
|
||||
# ── 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"},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
||||
# ── 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)
|
||||
|
||||
4
requirements.txt
Normal file
4
requirements.txt
Normal 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
37
templates/base.html
Normal 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>
|
||||
|
||||
<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
25
templates/dashboard.html
Normal 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
20
templates/index.html
Normal 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 %}
|
||||
|
||||
Reference in New Issue
Block a user