Auth.js mit Zugangsdaten-Provider: E-Mail, Passwort und TOTP-Code werden
in einem Formular abgefragt. Ein zweistufiger Ablauf muesste zwischen den
Schritten einen Zwischenzustand halten - genau das ist die uebliche
Schwachstelle, und am Handy ist ein Formular ohnehin schneller.
Anmeldung:
- Sperre wird VOR dem Passwortvergleich geprueft, damit ein gesperrter
Zugang nicht einmal Argon2-Rechenzeit kostet.
- Bei unbekannter Adresse wird gegen einen Dummy-Hash gerechnet und die
Antwort auf eine Mindestdauer gestreckt. Sonst verraet die Antwortzeit,
welche Adressen existieren.
- Wiederherstellungscodes werden nach Gebrauch entfernt.
- Veraltete Argon2-Parameter werden beim erfolgreichen Login nachgezogen.
Rate-Limiting in der Datenbank statt Redis - ein Server, wenige Benutzer,
kein Grund fuer eine weitere Komponente. Gezaehlt wird nach E-Mail und
nach Anschluss, damit weder viele Anschluesse noch viele Adressen helfen.
Zugriffsschutz (sitzung.ts):
- Die Sitzung allein entscheidet nichts. Bei jedem Aufruf wird der
Benutzer frisch geladen und die Sitzungsgeneration verglichen - so
lassen sich alle Anmeldungen auf einen Schlag beenden.
- erfordereZugang() prueft immer gegen die betreuteId, nie nur im UI.
- Erneute Bestaetigung vor sensiblen Aktionen ueber ein signiertes,
kurzlebiges Cookie statt ueber Serverzustand.
Audit-Log: aenderungErmitteln() protokolliert nur die tatsaechlich
geaenderten Felder; Passwoerter und TOTP-Geheimnisse werden ersetzt, nie
gespeichert. Ein Fehler im Protokoll bricht die Fachaktion nicht ab -
ausser die Aktion laeuft in einer Transaktion, dann faellt beides.
CSP mit Nonce pro Anfrage in der Middleware: keine Fremdquellen, kein
CDN, keine Fonts von aussen. Fachseiten werden nicht zwischengespeichert.
Etappe: 1
Phase: Fundament
Die Sicherheitsschicht, auf der alles Weitere aufsetzt.
Feldverschluesselung (AES-256-GCM):
- Umschlagformat bc1.<version>.<iv>.<tag>.<ct>. Die Schluesselversion
erlaubt Rotation, ohne alte Daten unlesbar zu machen.
- Der Feldname wird als AAD mitauthentifiziert. Ein Geheimtext laesst
sich damit nicht von diagnosen nach notizen umhaengen.
- Leere Werte werden zu NULL statt zu einem Umschlag - sonst verriete
die Datenbank, dass ueberhaupt etwas eingetragen war.
- VERSCHLUESSELTE_FELDER ist der einzige Ort, an dem steht, was
verschluesselt gehoert. sealen()/oeffnen() arbeiten darauf.
Argon2id nach OWASP-Empfehlung (19 MiB, t=2, p=1), mit hashVeraltet()
fuer spaetere Parametererhoehung ohne Migration.
Blindindex per HMAC-SHA256 mit eigenem Schluessel: erlaubt die Frage
"kenne ich diese IBAN schon?", ohne sie im Klartext zu halten.
Derselbe Mechanismus pseudonymisiert IP-Adressen fuers Audit-Log.
TOTP mit otpauth, QR-Code serverseitig als Data-URI - kein externer
QR-Dienst, kein CDN. Wiederherstellungscodes aus einem Alphabet ohne
0/O und 1/I, einzeln Argon2id-gehasht, Pruefung ohne fruehen Abbruch.
37 Tests decken Rundlauf, Manipulation, Feldbindung, Rotation,
Salzung und Normalisierung ab.
Etappe: 1
Phase: Fundament
Alle Fachentitaeten der Betreuung in einem Schema - bewusst komplett,
damit spaetere Etappen keine Migration am Fundament brauchen.
Kernentscheidungen:
- Geldbetraege durchgaengig als BigInt in Cent. Eine Rechnungslegung nach
§ 1865 BGB darf keine Rundungsfehler erben.
- Felder mit Suffix Verschluesselt tragen einen AES-256-GCM-Umschlag.
Wo trotzdem gesucht werden muss (IBAN), steht ein HMAC-Blindindex daneben.
- Fristen und Stichtage sind @db.Date, nicht DateTime - sonst verschiebt
die Zeitzone eine Frist um einen Tag.
- Frist.systemSchluessel macht die aus dem Bestellungsdatum abgeleiteten
Pflichtfristen idempotent.
- Genehmigung.beschlussDokumentId ist die technische Voraussetzung dafuer,
dass ein Vorgang ohne Beschluss nicht auf VOLLZOGEN gesetzt werden kann.
- DokumentVerknuepfung mit echten Fremdschluesseln statt loser Typ/ID-
Spalte: eine Akte muss referenzsicher sein.
- Wertstand und Saldostand sind Historien - Werte werden nie ueberschrieben.
Dazu zwei SQL-Skripte, die Prisma nicht abbilden kann:
- volltextsuche.sql: tsvector-Trigger (deutsch) mit Gewichtung A/B/C,
plus Trigram-Indizes fuer unscharfe Absendersuche.
- audit-schutz.sql: Trigger, die UPDATE und DELETE auf audit_log
verweigern. Das Log ist Nachweis gegenueber Gericht und Miterben,
also nur anhaengend - auch gegen psql.
Etappe: 1
Phase: Fundament
Grundgeruest der Anwendung: Next.js 15 (App Router), TypeScript im
strict-Modus, Tailwind mit shadcn/ui-Farbtokens.
Bewusste Festlegungen:
- Nur Systemschriften, keine externen Fonts, kein CDN.
- Grundschrift eine Stufe groesser, Fokusring immer sichtbar,
Touchziele mindestens 44px.
- Ampelfarben fuer Fristen als eigene Design-Tokens.
- Betraege werden als Cent-Ganzzahl gefuehrt, damit die Rechnungslegung
keine Rundungsfehler erbt.
- .env.example dokumentiert alle Geheimnisse; npm run schluessel erzeugt sie.
Etappe: 1
Phase: Fundament