Add WebAuthn passkey auth (dormant behind AUTH_ENABLED)

- auth.py: passkey credential store (public keys only) + env-driven RP config.
- auth_routes.py: /login, /enroll, /logout + /auth/* ceremony endpoints +
  before_request gate (no-op unless AUTH_ENABLED=1).
- login/enroll pages (SimpleWebAuthn browser); settings page lists + removes
  enrolled devices. requirements: webauthn==2.7.1.
- Deploys OFF: no behavior change until enabled after the hostname/cert exist.
  Closes groundwork for #5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 18:03:09 -05:00
parent 3a48021260
commit b5a136ed41
7 changed files with 447 additions and 1 deletions

9
app.py
View File

@@ -8,6 +8,7 @@ import calendar as cal_module
from storage import load_json, save_json
from settings import load_settings, save_settings, SECRET_KEYS
import txns_db
import auth
from version import __version__ as APP_VERSION, CHANGELOG
from categories import CATEGORIES, CATEGORY_COLORS
from bills import load_bills, save_bills, bills_for_month, calendar_grid, get_week_num
@@ -35,6 +36,10 @@ app.secret_key = os.environ.get("SECRET_KEY", "fullmoonbakehouse")
from suggestions.routes_flask import suggestions_bp
app.register_blueprint(suggestions_bp)
# Passkey (WebAuthn) auth — blueprint + login gate. No-op until AUTH_ENABLED=1.
from auth_routes import init_auth
init_auth(app)
from flask import send_from_directory
@app.route("/uploads/<path:p>")
@@ -2881,7 +2886,9 @@ def settings_view():
secrets_status = {k: bool(current.get(k)) for k in SECRET_KEYS}
return render_template("settings.html", settings=safe,
secrets_status=secrets_status,
saved=request.args.get("saved"))
saved=request.args.get("saved"),
passkeys=auth.list_credentials(),
auth_enabled=auth.auth_enabled())
@app.route("/changelog")