fix(auth): new passwords need at least 8 characters (FLAW-09)

auth.ValidatePassword checks every way a password is set: setup,
registration, profile, admin create and reset, reset by mail. Same rule
in the browser before sending (lib/password.ts), with our own German
message instead of the browser popup. Existing shorter passwords still
log in. Profile and admin reset no longer trim the password, which made
passwords with surrounding spaces unusable for login.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-29 12:20:46 +01:00
1 parent f8989d0c44
commit b5f926bcf7
13 files changed
+142 -26

No files matched your search

+11 -3
View File
@@ -111,8 +111,12 @@ func (s *Server) handleAdminCreateUser(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
return
}
if req.Email == "" || req.Password == "" {
writeError(w, http.StatusBadRequest, "E-Mail und Passwort erforderlich")
if req.Email == "" {
writeError(w, http.StatusBadRequest, "E-Mail-Adresse erforderlich")
return
}
if err := auth.ValidatePassword(req.Password); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
// Only full server admin can create another server admin
@@ -200,10 +204,14 @@ func (s *Server) handleAdminResetPassword(w http.ResponseWriter, r *http.Request
var req struct {
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.Password) == "" {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Neues Passwort erforderlich")
return
}
if err := auth.ValidatePassword(req.Password); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
hash, err := auth.HashPassword(req.Password)
if err != nil {
writeError(w, http.StatusInternalServerError, "Passwort-Hash fehlgeschlagen")
+16 -4
View File
@@ -90,8 +90,12 @@ func (s *Server) handleFirstBootSetup(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "Ungültige Anfrage")
return
}
if strings.TrimSpace(req.Email) == "" || len(req.Password) < 4 {
writeError(w, http.StatusBadRequest, "E-Mail und Passwort (mind. 4 Zeichen) erforderlich")
if strings.TrimSpace(req.Email) == "" {
writeError(w, http.StatusBadRequest, "E-Mail-Adresse erforderlich")
return
}
if err := auth.ValidatePassword(req.Password); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
if strings.TrimSpace(req.DisplayName) == "" {
@@ -154,8 +158,12 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "Ungültige Anfrage")
return
}
if strings.TrimSpace(req.Email) == "" || len(req.Password) < 4 {
writeError(w, http.StatusBadRequest, "E-Mail und Passwort (mind. 4 Zeichen) erforderlich")
if strings.TrimSpace(req.Email) == "" {
writeError(w, http.StatusBadRequest, "E-Mail-Adresse erforderlich")
return
}
if err := auth.ValidatePassword(req.Password); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
if strings.TrimSpace(req.DisplayName) == "" {
@@ -415,6 +423,10 @@ func (s *Server) handleUpdateUserSettings(w http.ResponseWriter, r *http.Request
}
}
if req.Password != nil && strings.TrimSpace(*req.Password) != "" {
if err := auth.ValidatePassword(*req.Password); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
hash, err := auth.HashPassword(*req.Password)
if err != nil {
writeError(w, http.StatusInternalServerError, "Fehler beim Hashen des Passworts")
+2 -2
View File
@@ -68,8 +68,8 @@ func (s *Server) handleResetPassword(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, storage.ErrResetTokenInvalid.Error())
return
}
if len(req.Password) < 4 {
writeError(w, http.StatusBadRequest, "Das Passwort braucht mindestens 4 Zeichen")
if err := auth.ValidatePassword(req.Password); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
hash, err := auth.HashPassword(req.Password)
+10 -1
View File
@@ -1344,7 +1344,16 @@ func TestFirstBootSetupWithSampleData(t *testing.T) {
defer store.Close()
srv := NewServer(&config.Config{Port: "8080", DatabasePath: dbPath, JWTSecret: "test-jwt-secret-key"}, store, ws.NewHub(), nil)
body := []byte(`{"email":"me@example.com","password":"secret","displayName":"Ich","createSampleData":true}`)
// Too short passwords are refused (FLAW-09)
short := httptest.NewRequest(http.MethodPost, "/api/v1/auth/setup", strings.NewReader(`{"email":"me@example.com","password":"secret"}`))
short.Header.Set("Content-Type", "application/json")
shortRec := httptest.NewRecorder()
srv.Router().ServeHTTP(shortRec, short)
if shortRec.Code != http.StatusBadRequest || !strings.Contains(shortRec.Body.String(), "mindestens 8 Zeichen") {
t.Fatalf("setup with 6 characters: want 400, got %d %s", shortRec.Code, shortRec.Body.String())
}
body := []byte(`{"email":"me@example.com","password":"secret123","displayName":"Ich","createSampleData":true}`)
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/setup", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()