leistungsbilanz-ts/docs/authentication.md
Grovy311 5073d9342a Harden authentication and put filter toggle and fields in one row
Security: per-username login throttling, constant-time unknown-user check,
Sec-Fetch-Site CSRF guard for writes, session rotation on login and
password change, absolute session lifetime, sessions ended on reset and
deactivation, last-admin protection, input length caps, ASCII usernames,
clickjacking headers. Covered by tests/authentication.test.ts.

Filter panel: toggle, fields and count share one row to save space.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:30:30 +02:00

4.9 KiB
Raw Blame History

Anmeldung und Benutzerverwaltung

Die Leistungsbilanz verlangt eine Anmeldung. Es gibt kein Self-Signup: jeder Zugang wird von einem Administrator über Config → Benutzer angelegt.

Rollen

Es gibt genau zwei Rollen (src/shared/constants/user-roles.ts):

  • admin – wie user, zusätzlich Zugriff auf den Config-Bereich (Benutzerverwaltung, eigenes Konto)
  • user – volle Nutzung der Leistungsbilanz (Projekte, Stromkreislisten, Grobbilanz, …), kein Zugriff auf Config

Es gibt (bewusst, siehe unten) keine konfigurierbare Rechte-Matrix und keine Verknüpfung mit einer Mitarbeiter-/Personen-Stammdatentabelle.

Ablauf beim Anlegen eines Zugangs

  1. Ein Administrator öffnet Config → Benutzer und legt einen neuen Zugang mit Anmeldename, Name, Rolle und einem Startpasswort an, das er selbst eintippt.
  2. Der Zugang startet mit mustChangePassword = true.
  3. Beim ersten Anmelden zeigt die Oberfläche statt der App ein Formular, das ein eigenes Passwort verlangt, bevor es weitergeht.
  4. "Passwort zurücksetzen" (ebenfalls in Config → Benutzer) setzt ein neues Startpasswort und mustChangePassword wieder auf true – z. B. wenn jemand sein Passwort vergessen hat.

Technischer Aufbau

  • Datenmodell: Tabelle users (src/db/schema/users.ts) – username, passwordHash (scrypt, src/server/auth/password.ts), name, role, active, mustChangePassword.
  • Sessions: In-Memory-Store (src/server/auth/sessions.ts), Cookie lb_session, sliding expiry (AUTH_SESSION_HOURS, Standard 12h) mit absoluter Obergrenze (AUTH_SESSION_MAX_DAYS, Standard 7). Ein Neustart der API beendet alle Sessions – für einen Single-Container-Dienst ein akzeptabler Kompromiss gegenüber einer persistierten Session-Tabelle.
  • Middleware: src/server/auth/auth.middleware.ts schützt /api/* (außer /api/auth/login, /api/auth/status, /api/auth/logout) und erzwingt den Passwortwechsel, solange mustChangePassword gesetzt ist. /api/users/* verlangt zusätzlich die Rolle admin.
  • Frontend: AuthProvider lädt /api/auth/me; LoginGate (src/app/layout.tsx) zeigt Login- bzw. Passwortformular, solange nicht angemeldet bzw. mustChangePassword gesetzt ist, sonst die App.

Schutzmechanismen

Angriff Schutz
Passwort-Raten (online) LoginThrottle: 5 Fehlversuche je Anmeldename in 15 min → HTTP 429 bis das Fenster abläuft. Schlüssel ist der Name, nicht die IP (alle Anfragen kommen über den Next-Rewrite mit derselben Quell-IP).
Passwort-Raten (offline, DB-Diebstahl) scrypt (N=16384, r=8, p=1), eigenes Salt je Hash
Nutzernamen ausspähen (Timing) Unbekannte Namen werden gegen einen festen Dummy-Hash geprüft – immer genau ein scrypt-Lauf; einheitliche Fehlermeldung
CSRF SameSite=Lax plus requireSameOriginForWrites: schreibende Anfragen nur mit Sec-Fetch-Site: same-origin. Nötig, weil andere Dienste auf demselben Host (andere Ports) als „same-site“ gelten
Session-Fixation Neue Session-ID bei jedem Login und nach jedem Passwortwechsel
Gestohlenes Cookie httpOnly; gleitende Laufzeit (AUTH_SESSION_HOURS, 12 h) und harte Obergrenze (AUTH_SESSION_MAX_DAYS, 7 Tage)
Kompromittiertes Konto Passwort-Reset und Deaktivieren beenden sofort alle Sessions des Kontos; Passwortwechsel beendet alle anderen
Aussperren Der letzte aktive Admin kann weder herabgestuft noch deaktiviert werden; eigener Zugang ist in der Liste gesperrt
Rechteausweitung / Mass Assignment Rolle kommt aus der DB, nie aus der Anfrage; zod-Schemas mit .strict()
DoS über riesige Passwörter Längengrenzen (Name 100, Passwort 200 Zeichen) vor scrypt
Look-alike-Namen Anmeldename nur a-z 0-9 . _ @ -, klein geschrieben
Clickjacking X-Frame-Options: SAMEORIGIN, frame-ancestors 'self' (next.config.mjs)
Speicherwachstum Max. 20 Sessions je Nutzer, periodisches Aufräumen abgelaufener Sessions, Throttle-Tabelle begrenzt

Nicht durch Code lösbar: Ohne TLS gehen Passwort und Session-Cookie im Klartext durchs Netz. Für Zugriff außerhalb des LANs HTTPS über einen Reverse-Proxy vorschalten und COOKIE_SECURE=true setzen.

Bootstrap-Admin

AUTH_BOOTSTRAP_USERNAME / AUTH_BOOTSTRAP_PASSWORT (Env-Variablen, siehe compose.yaml) legen beim ersten Start einen Admin an – nur falls die users-Tabelle noch komplett leer ist. Danach werden diese Variablen ignoriert; jeder weitere Zugang entsteht über Config → Benutzer. Die Werte gehören in eine lokale, nicht versionierte .env auf dem Deploy-Host, nicht in compose.yaml selbst.

Bewusst nicht umgesetzt (Stand jetzt)

  • Konfigurierbare Rechte-Matrix statt fixer admin/user-Rollen
  • Dauerhafte Konten-Sperre nach Fehlversuchen (es gibt nur die zeitlich begrenzte Drosselung, siehe oben)
  • E-Mail-/Einmal-Link-Passwortvergabe, OIDC/Entra-Anmeldung
  • Verknüpfung von Zugängen mit einer Mitarbeiter-Stammdatentabelle