package api import ( "encoding/json" "io" "net/http" "time" "golang.org/x/crypto/bcrypt" ) func (a *API) changePassword(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-store") if r.Method != http.MethodPost { fail(w, 405, "method not allowed") return } if _, _, err := a.user(r); err != nil { fail(w, 401, "authentication required") return } var in struct { CurrentPassword string `json:"current_password"` Password string `json:"password"` } decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096)) decoder.DisallowUnknownFields() if decoder.Decode(&in) != nil || decoder.Decode(new(any)) != io.EOF || in.CurrentPassword == "" || len(in.Password) < 12 || len(in.Password) > 72 { fail(w, 400, "current password and new password 12–72 bytes required") return } // Recheck credentials after waiting for concurrent logins or password resets. a.authLoginMu.Lock() defer a.authLoginMu.Unlock() u, _, err := a.user(r) if err != nil { fail(w, 401, "authentication required") return } locked, err := a.accountLocked(u.Username) if err != nil { fail(w, 503, "authentication unavailable") return } if locked { fail(w, 429, "account temporarily locked; retry later") return } currentHash, err := a.s.Password(u.Username) if err != nil { fail(w, 503, "authentication unavailable") return } if len(in.CurrentPassword) > 72 || bcrypt.CompareHashAndPassword([]byte(currentHash), []byte(in.CurrentPassword)) != nil { if err = a.failedLogin(u.Username); err != nil { fail(w, 503, "authentication unavailable") return } fail(w, 400, "invalid current password") return } hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost) if err != nil { fail(w, 500, "password error") return } tx, err := a.s.DB.BeginTx(r.Context(), nil) if err != nil { fail(w, 500, "password change failed") return } defer tx.Rollback() _, err = tx.ExecContext(r.Context(), "UPDATE users SET pass_hash=? WHERE id=?", string(hash), u.ID) if err == nil { _, err = tx.ExecContext(r.Context(), "DELETE FROM auth_refresh WHERE sid IN (SELECT id FROM sessions WHERE uid=?)", u.ID) } if err == nil { _, err = tx.ExecContext(r.Context(), "DELETE FROM sessions WHERE uid=?", u.ID) } if err == nil { _, err = tx.ExecContext(r.Context(), "DELETE FROM auth_failures WHERE username=?", u.Username) } if err == nil { _, err = tx.ExecContext(r.Context(), "INSERT INTO audit(uid,action,target,detail,at) VALUES(?,?,?,?,?)", u.ID, "user.change_password", u.ID, "", time.Now().Unix()) } if err == nil { err = tx.Commit() } if err != nil { fail(w, 500, "password change failed") return } writeJSON(w, map[string]bool{"ok": true, "reauthenticate": true}) }