forked from jappel/leistungsbilanz-ts
99 lines
5.8 KiB
Markdown
99 lines
5.8 KiB
Markdown
# 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 **Config → Benutzer**
|
||
- **user** – volle Nutzung der Leistungsbilanz (Projekte, Stromkreislisten,
|
||
Grobbilanz, …) und Zugriff auf das eigene Konto, keine Benutzerverwaltung
|
||
|
||
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.
|
||
**Konto** ist für jede angemeldete Person erreichbar; **Benutzer** nur für
|
||
Administratoren.
|
||
|
||
## 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`: jeder Schreibzugriff braucht `X-Leistungsbilanz-Request: 1`; fremde HTML-Formulare können den Header nicht setzen und ein fremder Fetch scheitert an der notwendigen CORS-Preflight-Anfrage. Ein vorhandener `Sec-Fetch-Site`-Header wird zusätzlich geprüft. |
|
||
| CSRF über reines HTTP | Der eigene Anwendungsheader gilt auch dann, wenn Browser im HTTP-LAN keinen `Sec-Fetch-Site`-Header senden. Skripte und manuelle API-Aufrufe müssen ihn bei `POST`, `PUT`, `PATCH` und `DELETE` ausdrücklich mitsenden. |
|
||
| 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; Prüfung und Änderung laufen atomar in einer SQLite-Transaktion. Der eigene 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.
|
||
|
||
```dotenv
|
||
AUTH_BOOTSTRAP_USERNAME=admin
|
||
AUTH_BOOTSTRAP_PASSWORT=<eigenes Startpasswort mit mindestens 12 Zeichen>
|
||
AUTH_SESSION_HOURS=12
|
||
AUTH_SESSION_MAX_DAYS=7
|
||
COOKIE_SECURE=false
|
||
```
|
||
|
||
Bei Zugriff über HTTPS muss `COOKIE_SECURE=true` gesetzt werden. Bei direktem
|
||
HTTP-Zugriff im LAN bleibt es `false`, da der Browser das Session-Cookie sonst
|
||
nicht speichert.
|
||
|
||
## 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
|