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>
4.9 KiB
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
- Ein Administrator öffnet Config → Benutzer und legt einen neuen Zugang mit Anmeldename, Name, Rolle und einem Startpasswort an, das er selbst eintippt.
- Der Zugang startet mit
mustChangePassword = true. - Beim ersten Anmelden zeigt die Oberfläche statt der App ein Formular, das ein eigenes Passwort verlangt, bevor es weitergeht.
- "Passwort zurücksetzen" (ebenfalls in Config → Benutzer) setzt ein neues
Startpasswort und
mustChangePasswordwieder auftrue– 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), Cookielb_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.tsschützt/api/*(außer/api/auth/login,/api/auth/status,/api/auth/logout) und erzwingt den Passwortwechsel, solangemustChangePasswordgesetzt ist./api/users/*verlangt zusätzlich die Rolleadmin. - Frontend:
AuthProviderlädt/api/auth/me;LoginGate(src/app/layout.tsx) zeigt Login- bzw. Passwortformular, solange nicht angemeldet bzw.mustChangePasswordgesetzt 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