feat(auth): reset a forgotten password by mail
"Passwort vergessen?" appears on the login screen when SMTP and BASE_URL are configured. The request always gets the same answer and sends the mail in the background (at most every 2 minutes per account). The link holds a random token valid once for one hour; only its SHA-256 is stored. Setting the new password invalidates all open links and ends every session. The app removes the token from the address bar and offers a way back for expired links. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
677fe0fc0a
commit
5e20710aa7
16 files changed
+489
-8
No files matched your search
@@ -1449,3 +1449,39 @@ func TestAPIKeysOnlyDeliverData(t *testing.T) {
|
||||
t.Fatalf("revoked token must be refused by the ingest, got %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordResetFlow(t *testing.T) {
|
||||
srv, store, _ := setupTestServer(t)
|
||||
router := srv.Router()
|
||||
post := func(path, body string) (int, string) {
|
||||
req := httptest.NewRequest("POST", path, strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
return rec.Code, rec.Body.String()
|
||||
}
|
||||
|
||||
// Without BASE_URL and SMTP the feature is off
|
||||
if code, _ := post("/api/v1/auth/forgot-password", `{"email":"x@example.com"}`); code != http.StatusServiceUnavailable {
|
||||
t.Errorf("forgot-password without setup: want 503, got %d", code)
|
||||
}
|
||||
|
||||
// The link from the mail sets a new password once and ends the old sessions
|
||||
u, _ := store.CreateUser("reset@example.com", "hash", "Reset", "member")
|
||||
raw, err := store.CreatePasswordResetToken(u.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("token: %v", err)
|
||||
}
|
||||
if code, _ := post("/api/v1/auth/reset-password", `{"token":"`+raw+`","password":"abc"}`); code != http.StatusBadRequest {
|
||||
t.Errorf("too short password must be refused, got %d", code)
|
||||
}
|
||||
if code, body := post("/api/v1/auth/reset-password", `{"token":"`+raw+`","password":"neues-passwort"}`); code != http.StatusOK {
|
||||
t.Fatalf("reset: %d %s", code, body)
|
||||
}
|
||||
if code, _ := post("/api/v1/auth/login", `{"login":"reset@example.com","password":"neues-passwort"}`); code != http.StatusOK {
|
||||
t.Errorf("login with the new password failed: %d", code)
|
||||
}
|
||||
if code, _ := post("/api/v1/auth/reset-password", `{"token":"`+raw+`","password":"nochmal1"}`); code != http.StatusBadRequest {
|
||||
t.Errorf("token must work only once, got %d", code)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user