ChangePassword handles PUT /auth/change-password.
(w http.ResponseWriter, r *http.Request)
| 1050 | |
| 1051 | // ChangePassword handles PUT /auth/change-password. |
| 1052 | func (h *AuthHandler) ChangePassword(w http.ResponseWriter, r *http.Request) { |
| 1053 | userID, _ := r.Context().Value(middleware.UserIDKey).(string) |
| 1054 | if userID == "" { |
| 1055 | Error(w, http.StatusUnauthorized, "Unauthorized") |
| 1056 | return |
| 1057 | } |
| 1058 | var req struct { |
| 1059 | CurrentPassword string `json:"currentPassword"` |
| 1060 | NewPassword string `json:"newPassword"` |
| 1061 | } |
| 1062 | if err := decodeJSON(r, &req); err != nil { |
| 1063 | Error(w, http.StatusBadRequest, "Invalid request body") |
| 1064 | return |
| 1065 | } |
| 1066 | if req.CurrentPassword == "" { |
| 1067 | Error(w, http.StatusBadRequest, "Current password is required") |
| 1068 | return |
| 1069 | } |
| 1070 | if err := ValidatePasswordPolicy(h.resolved, req.NewPassword); err != nil { |
| 1071 | Error(w, http.StatusBadRequest, err.Error()) |
| 1072 | return |
| 1073 | } |
| 1074 | user, err := h.users.GetByID(r.Context(), userID) |
| 1075 | if err != nil || user == nil { |
| 1076 | Error(w, http.StatusNotFound, "User not found") |
| 1077 | return |
| 1078 | } |
| 1079 | if user.PasswordHash == nil { |
| 1080 | Error(w, http.StatusBadRequest, "Cannot change password for OIDC-only accounts") |
| 1081 | return |
| 1082 | } |
| 1083 | hash := strings.TrimSpace(*user.PasswordHash) |
| 1084 | if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(req.CurrentPassword)); err != nil { |
| 1085 | Error(w, http.StatusUnauthorized, "Current password is incorrect") |
| 1086 | return |
| 1087 | } |
| 1088 | newHash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), 12) |
| 1089 | if err != nil { |
| 1090 | Error(w, http.StatusInternalServerError, "Failed to hash password") |
| 1091 | return |
| 1092 | } |
| 1093 | if err := h.users.UpdatePassword(r.Context(), userID, string(newHash)); err != nil { |
| 1094 | Error(w, http.StatusInternalServerError, "Failed to change password") |
| 1095 | return |
| 1096 | } |
| 1097 | // Security baseline: invalidate everything that grants access without |
| 1098 | // re-entering the new password on another browser. |
| 1099 | // * Trusted-device rows → attacker with a trust cookie cannot skip MFA. |
| 1100 | // * Other sessions → attacker with a refresh_token cannot keep a live session. |
| 1101 | // We keep the caller's own session alive so the UI doesn't log them out |
| 1102 | // mid-action after a successful password change. |
| 1103 | if h.trustedDevices != nil { |
| 1104 | if err := h.trustedDevices.RevokeAllForUser(r.Context(), userID); err != nil && h.log != nil { |
| 1105 | h.log.Error("change password revoke trusted devices failed", "user_id", userID, "error", err) |
| 1106 | } |
| 1107 | } |
| 1108 | currentSessionID, _ := r.Context().Value(middleware.SessionIDKey).(string) |
| 1109 | if h.sessions != nil { |
nothing calls this directly
no test coverage detected