feat(2-Schritt3): nginx-Cutover — App per Token-Link, Admin hinter Authelia

Sicherheits-Architektur umgestellt (live verifiziert):
- App-Routen (/, /ws/) ohne Authelia -> Auth via Capability-Token-Cookie
  (Senioren: Ein-Klick-Link, kein 2FA). Remote-User wird in nginx HART
  geleert (Anti-Spoofing: App vertraut Remote-User von der Proxy-IP).
- /api/admin/* (und /api/admin/log) zusätzlich hinter Authelia (Doppelschutz).
- gate_web_ui: statt SSO-Redirect (würde nach Cutover schleifen) eine
  freundliche "Persönlicher Zugang nötig"-Hinweisseite (401).

Endabnahme durch nginx: Link->303+Cookie, /api/me->200 (kein 2FA),
Admin->302 Authelia, Header-Spoofing->401. DEPLOYMENT.md 2.8 aktualisiert.
Test auf neue Hinweisseite angepasst. 219 passed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-25 04:45:11 +02:00
commit 6e824d8f9b
3 changed files with 43 additions and 22 deletions

View file

@ -5,7 +5,7 @@ from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from starlette.responses import RedirectResponse, JSONResponse
from starlette.responses import RedirectResponse, JSONResponse, HTMLResponse
from app.config import settings
from urllib.parse import urlencode
@ -55,6 +55,19 @@ async def record_metrics(request: Request, call_next):
# Pfade mit eigener Auth, Health-Checks und Favicons: nicht gaten.
_NO_ACCESS_PAGE = """<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"><title>Zugang</title>
<style>body{font-family:system-ui,-apple-system,sans-serif;background:#f8fafc;color:#1e293b;
display:grid;place-items:center;min-height:100vh;margin:0;padding:1.5rem}
.box{max-width:30rem;text-align:center}h1{font-size:1.3rem;margin:0 0 .75rem}
p{color:#475569;line-height:1.6;margin:.5rem 0}.emoji{font-size:2.5rem}</style></head>
<body><div class="box"><div class="emoji">🔒</div>
<h1>Persönlicher Zugang nötig</h1>
<p>Bitte öffne die App über deinen persönlichen Zugangslink.</p>
<p>Wenn du keinen Link hast, wende dich an deine Betreuungsperson.</p>
</div></body></html>"""
_PUBLIC_PREFIXES = ("/api", "/ws", "/health", "/favicon", "/apple-touch-icon")
@ -83,10 +96,9 @@ async def gate_web_ui(request: Request, call_next):
token = (_bearer_token(request.headers.get("authorization"))
or k or request.cookies.get(CAPABILITY_COOKIE))
if authenticate(request.headers, client_host, token) is None:
login = settings.sso_login_url.strip()
if login:
return RedirectResponse(login, status_code=302)
return JSONResponse({"detail": "Authentication required"}, status_code=401)
# Kein gültiger Zugang -> freundliche Hinweisseite (KEIN SSO-Redirect, der
# nach dem nginx-Cutover in eine Schleife liefe). Senioren nutzen ihren Link.
return HTMLResponse(_NO_ACCESS_PAGE, status_code=401)
return await call_next(request)