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
|
||||||
|
|
||||||
8
.gitignore
vendored
8
.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__)
|
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
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