Simplify auth to HTTP Basic Auth

This commit is contained in:
Randy 2026-03-30 08:50:31 -04:00
parent 38cfa587a5
commit c2064bd943
3 changed files with 53 additions and 31 deletions

81
app.py
View File

@ -1,50 +1,73 @@
import base64
import hashlib
import hmac
import os import os
from dotenv import load_dotenv from flask import Flask, Response, render_template, request
from flask import Flask, redirect, render_template, request, session, url_for
load_dotenv()
app = Flask(__name__) app = Flask(__name__)
app.secret_key = os.environ["SECRET_KEY"]
SITE_PASSWORD = os.environ["SITE_PASSWORD"] PASSWORD_SALT = "fn-resi-v1"
PASSWORD_HASH = "18194f5e151146895ddd7ff59fcac4f324b2832201df108d1d89cc0ef9d2ebe9"
AUTH_REALM = "FBN Residential"
def _password_matches(candidate):
expected = os.environ.get("SITE_PASSWORD")
if expected is not None:
return hmac.compare_digest(candidate, expected)
derived = hashlib.pbkdf2_hmac(
"sha256",
candidate.encode("utf-8"),
PASSWORD_SALT.encode("utf-8"),
390000,
).hex()
return hmac.compare_digest(derived, PASSWORD_HASH)
def _unauthorized():
return Response(
"Authentication required.\n",
401,
{
"WWW-Authenticate": f'Basic realm="{AUTH_REALM}"',
"Content-Type": "text/plain; charset=utf-8",
},
)
def _authorized(req):
header = req.headers.get("Authorization", "")
if not header.startswith("Basic "):
return False
try:
decoded = base64.b64decode(header.split(None, 1)[1]).decode("utf-8")
except Exception:
return False
_, _, password = decoded.partition(":")
if not password:
return False
return _password_matches(password)
@app.after_request @app.after_request
def add_noindex_header(response): def add_noindex_header(response):
response.headers["X-Robots-Tag"] = "noindex, nofollow" response.headers["X-Robots-Tag"] = "noindex, nofollow"
response.headers["Cache-Control"] = "no-store"
return response return response
@app.route("/") @app.route("/")
def index(): def index():
if not session.get("authenticated"): if not _authorized(request):
return redirect(url_for("login")) return _unauthorized()
return render_template("dashboard.html") return render_template("dashboard.html")
@app.route("/login", methods=["GET", "POST"])
def login():
if session.get("authenticated"):
return redirect(url_for("index"))
error = None
if request.method == "POST":
if request.form.get("password") == SITE_PASSWORD:
session["authenticated"] = True
return redirect(url_for("index"))
error = "Wrong password."
return render_template("login.html", error=error)
@app.route("/logout")
def logout():
session.clear()
return redirect(url_for("login"))
@app.route("/robots.txt") @app.route("/robots.txt")
def robots(): def robots():
return "User-agent: *\nDisallow: /\n", 200, {"Content-Type": "text/plain"} return "User-agent: *\nDisallow: /\n", 200, {"Content-Type": "text/plain"}

View File

@ -1,3 +1,2 @@
flask flask
gunicorn gunicorn
python-dotenv

View File

@ -97,7 +97,7 @@
<div style="display:flex;justify-content:space-between;align-items:baseline;"> <div style="display:flex;justify-content:space-between;align-items:baseline;">
<h1>FBN Residential &mdash; Executive Summary</h1> <h1>FBN Residential &mdash; Executive Summary</h1>
<a href="/logout" style="color:var(--muted);font-size:12px;text-decoration:none;">Logout</a> <span style="color:var(--muted);font-size:12px;">Protected</span>
</div> </div>
<p class="subtitle">As of March 27, 2026 &middot; FY 2023&ndash;2025 &middot; Source: GP</p> <p class="subtitle">As of March 27, 2026 &middot; FY 2023&ndash;2025 &middot; Source: GP</p>