Tenant-User (tenant_id IS NOT NULL) melden sich künftig per E-Mail an statt per Username — behebt Verwechslungen wie im Support-Fall vom 2026-06-13 (Login schlug trotz Passwort-Reset fehl, weil E-Mail statt Username verwendet wurde). Nicht-Tenant-User (Superadmin/System) können weiterhin Username ODER E-Mail nutzen. Neue Store.VerifyLogin() prüft erst per E-Mail (alle User), fällt dann auf Username zurück (nur tenant_id IS NULL). VerifyPassword() bleibt für den IMAP-Server-Login-Pfad (PROJ-26) unverändert. Bewusster Breaking Change für Tenant-User, Datenqualität vorab geprüft (0 Kollisionen). Security-Nachtrag: bcrypt-Dummy-Compare im "user not found"-Pfad ergänzt, um Timing-basierte Identifier-Enumeration zu verhindern. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
101 lines
7.6 KiB
Markdown
101 lines
7.6 KiB
Markdown
# PROJ-46: E-Mail als primärer Login-Identifier für Tenant-User
|
|
|
|
## Status: In Review
|
|
**Created:** 2026-06-13
|
|
**Last Updated:** 2026-07-03
|
|
|
|
## Implementation Notes (2026-07-03, Backend)
|
|
- **`internal/userstore/userstore.go`:** Neue Funktion `VerifyLogin(ctx, identifier, password) (*User, error)`.
|
|
Ablauf: (1) Lookup per `email = $1` (matcht alle User). (2) Falls kein Treffer,
|
|
Lookup per `username = $1 AND tenant_id IS NULL` — Tenant-User können sich damit
|
|
NICHT mehr per Username anmelden. Danach `active`-Check + `bcrypt.CompareHashAndPassword`.
|
|
Neuer Helper `scanUserWithHash` (reicht `pgx.ErrNoRows` unverfälscht durch, damit der
|
|
Email→Username-Fallback greift). `VerifyPassword` blieb unangetastet (IMAP-Pfad PROJ-26).
|
|
- **`internal/auth/auth.go`:** `Manager.Login()` ruft nun `VerifyLogin(context.Background(), ...)`
|
|
statt `VerifyPassword(...)`. `extractDomain(identifier)` musste NICHT angepasst werden:
|
|
Tenant-User senden jetzt die E-Mail (`user@domain`) als Identifier, woraus die bestehende
|
|
`strings.LastIndex(..., "@")`-Logik die Domain korrekt extrahiert — für den per-Tenant-/
|
|
Global-LDAP-Fallback ist das sogar zuverlässiger als der frühere reine Username.
|
|
- **`internal/userstore/userstore_test.go`:** `TestVerifyLogin` deckt die volle Matrix ab
|
|
(Tenant per E-Mail = Erfolg, Tenant per Username = Reject, Non-Tenant per Username = Erfolg,
|
|
Non-Tenant per E-Mail = Erfolg, unbekannter Identifier = Reject, falsches Passwort = Reject).
|
|
DB-backed (Skip ohne `TEST_DATABASE_URL`, wie die übrigen userstore-Tests).
|
|
- **Kein lokaler `go build`/`go test` möglich** (kein Toolchain im Arbeitsverzeichnis) —
|
|
Build-/Testverifikation erfolgt separat auf dem Testserver.
|
|
- Frontend (`src/app/page.tsx`, `src/app/admin/login/page.tsx`) wird separat vom Frontend-Agent umgesetzt.
|
|
|
|
## Implementation Notes (2026-07-03, Frontend)
|
|
- **`src/app/page.tsx` (Tenant-User-Login):** Label "Benutzername" → "E-Mail-Adresse",
|
|
Input `type="text"` → `type="email"`, `autoComplete="username"` → `autoComplete="email"`,
|
|
Placeholder + `aria-label` entsprechend angepasst. API-Client (`login()`) unverändert —
|
|
der eingegebene String geht weiterhin als `username`-Feld ins JSON-Body.
|
|
- **`src/app/admin/login/page.tsx` (Admin/Superadmin-Login):** Label + Placeholder →
|
|
"Benutzername oder E-Mail-Adresse". Input bleibt bewusst `type="text"` (beide Formate möglich).
|
|
- **`npx tsc --noEmit`:** sauber durchgelaufen (Exit 0), keine Typfehler.
|
|
- Kein API-Client-/Backend-Code angefasst. QA gegen Acceptance Criteria steht noch aus.
|
|
|
|
## Problem Statement
|
|
Tenant-User melden sich aktuell mit `username` an, nicht mit ihrer E-Mail-Adresse. Das führt in der Praxis zu Verwechslungen: Ein User versucht sich mit seiner E-Mail-Adresse einzuloggen (die einzige Kennung, die er sich merkt), das Login schlägt mit "invalid_password" fehl, und nach 5 Fehlversuchen innerhalb von 15 Minuten greift das Rate-Limit (429 "too many failed login attempts") — selbst nachdem ein Admin das Passwort zurückgesetzt hat.
|
|
|
|
Konkreter Support-Fall (2026-06-13): patrick@perlbach24.de konnte sich trotz Passwort-Reset durch den Superadmin nicht einloggen, weil er `patrick@perlbach24.de` statt `patrick` (seinem tatsächlichen `username`) als Login-Identifier verwendete.
|
|
|
|
## Dependencies
|
|
- Betrifft: PROJ-1 (Authentifizierung & Rollen), PROJ-21 (Multi-Tenancy)
|
|
- Berührt NICHT: PROJ-26 (IMAP-Server-Schnittstelle) — IMAP-Login bleibt unverändert per `username`
|
|
|
|
## User Stories
|
|
- Als **Tenant-User** möchte ich mich mit meiner **E-Mail-Adresse** anmelden, weil das die Kennung ist, die ich kenne und die mir in Einladungs-/Reset-Mails genannt wird.
|
|
- Als **Superadmin/System-User** (ohne Tenant-Zugehörigkeit) möchte ich mich weiterhin mit meinem **Benutzernamen ODER meiner E-Mail-Adresse** anmelden können.
|
|
- Als **Tenant-Admin** möchte ich, dass sich Tenant-User NICHT mehr per Benutzername einloggen können, um Verwechslungen wie im Support-Fall vom 2026-06-13 zukünftig zu vermeiden.
|
|
|
|
## Acceptance Criteria
|
|
|
|
### Login-Logik (Backend)
|
|
- [ ] Neue Lookup-Funktion `Store.VerifyLogin(ctx, identifier, password) (*User, error)` in `internal/userstore/userstore.go`
|
|
- [ ] Login per E-Mail-Adresse (`email = $1`) funktioniert für alle User (Tenant-User UND Nicht-Tenant-User)
|
|
- [ ] Login per `username` funktioniert NUR für User mit `tenant_id IS NULL` (Superadmin/System-User)
|
|
- [ ] Tenant-User (`tenant_id IS NOT NULL`), die ihren `username` als Login-Identifier verwenden, erhalten `invalid_credentials` (kein Login)
|
|
- [ ] Bestehende `VerifyPassword(username, password)` bleibt unverändert erhalten für den IMAP-Server-Login-Pfad (PROJ-26)
|
|
- [ ] `internal/auth/auth.go` `Manager.Login()` ruft `VerifyLogin()` statt `VerifyPassword()`
|
|
|
|
### Frontend
|
|
- [ ] Tenant-User-Login (`src/app/page.tsx`): Label "Benutzername" → "E-Mail-Adresse", Input-Type `email`, `autoComplete="email"`
|
|
- [ ] Admin/Superadmin-Login (`src/app/admin/login/page.tsx`): Label → "Benutzername oder E-Mail-Adresse"
|
|
- [ ] API-Request-Format bleibt unverändert (`{"username": "<identifier>", "password": "..."}`)
|
|
|
|
### Rate-Limiting & Audit
|
|
- [ ] `login_attempts.username` Spalte auf VARCHAR(255) erweitert (bereits erledigt, siehe Migration unten)
|
|
- [ ] Rate-Limiting-Logik (`CountRecentFailures`, `RecordLoginAttempt`) funktioniert unverändert mit E-Mail-Strings als Schlüssel
|
|
- [ ] Audit-Log protokolliert bei Fehlversuchen weiterhin den eingegebenen Identifier (E-Mail oder Username)
|
|
|
|
### Tests
|
|
- [ ] Unit-Tests für `VerifyLogin` decken die vollständige Matrix ab:
|
|
- Tenant-User per E-Mail → Erfolg
|
|
- Tenant-User per Username (≠ E-Mail) → `invalid_credentials`
|
|
- Nicht-Tenant-User per Username → Erfolg
|
|
- Nicht-Tenant-User per E-Mail → Erfolg
|
|
- Unbekannter Identifier → `invalid_credentials`
|
|
|
|
## Migration (bereits durchgeführt am 2026-06-13)
|
|
- Datenqualitäts-Check auf 192.168.1.131: 0 Tenant-User mit fehlender/ungültiger E-Mail, 0 Username↔E-Mail-Kollisionen
|
|
- `login_attempts.username` von VARCHAR(100) → VARCHAR(255) erweitert (idempotenter `initSchema`-Eintrag in `internal/userstore/userstore.go` ergänzt)
|
|
- Hinweis: `superadmin@localhost` und `auditor@archivmail.local` (beide `tenant_id IS NULL`) sind als E-Mail-Format ungewöhnlich, aber kein Blocker — diese User können weiterhin per `username` einloggen
|
|
|
|
## Edge Cases
|
|
- **Kollision `username` (User A) == `email` (User B):** Mit `email UNIQUE` selten, aber falls vorhanden gewinnt der `email`-Treffer (User B) immer — User A kann sich mit diesem String dann nicht mehr einloggen, auch wenn `tenant_id IS NULL`. Aktuell 0 solcher Fälle (siehe Migration).
|
|
- **LDAP-User (PROJ-16/23):** `extractDomain(identifier)` in `internal/auth/auth.go` muss bei E-Mail-Eingabe weiterhin korrekt funktionieren (E-Mail-Format `user@domain` ist kompatibel zum bisherigen Format).
|
|
|
|
## Non-Goals
|
|
- IMAP-Server-Login (PROJ-26) bleibt unverändert per `username`
|
|
- Kein einheitlicher Login-Screen für alle Usertypen (bleibt bei zwei separaten Routen: `/` und `/admin/login`)
|
|
- Keine Änderung am API-Request-Wire-Format (`username`-Feld bleibt im JSON-Body, nur die Bedeutung ändert sich)
|
|
|
|
## Technical Requirements
|
|
- **Breaking Change (bewusst):** Tenant-User, die sich bisher per `username` einloggten, müssen künftig die E-Mail-Adresse verwenden. REST-API-Clients (PROJ-13), die `username` für Tenant-User senden, müssen auf `email` umgestellt werden.
|
|
- **Betroffene Dateien:**
|
|
- `internal/userstore/userstore.go` (neue `VerifyLogin`, initSchema-Erweiterung — bereits erledigt)
|
|
- `internal/auth/auth.go` (`Login()` ruft `VerifyLogin()`)
|
|
- `src/app/page.tsx` (Label/Input-Type)
|
|
- `src/app/admin/login/page.tsx` (Label)
|
|
- `internal/auth/auth_test.go` (neue Testfälle)
|