95 lines
2.7 KiB
Go
95 lines
2.7 KiB
Go
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})
|
||
}
|