Disable handles POST /tfa/disable.
(w http.ResponseWriter, r *http.Request)
| 155 | |
| 156 | // Disable handles POST /tfa/disable. |
| 157 | func (h *TfaHandler) Disable(w http.ResponseWriter, r *http.Request) { |
| 158 | userID, _ := r.Context().Value(middleware.UserIDKey).(string) |
| 159 | if userID == "" { |
| 160 | Error(w, http.StatusUnauthorized, "Unauthorized") |
| 161 | return |
| 162 | } |
| 163 | |
| 164 | var req DisableRequest |
| 165 | if err := decodeJSON(r, &req); err != nil { |
| 166 | Error(w, http.StatusBadRequest, "Invalid request body") |
| 167 | return |
| 168 | } |
| 169 | if req.Password == "" { |
| 170 | Error(w, http.StatusBadRequest, "Password is required to disable TFA") |
| 171 | return |
| 172 | } |
| 173 | |
| 174 | user, err := h.users.GetByID(r.Context(), userID) |
| 175 | if err != nil || user == nil { |
| 176 | Error(w, http.StatusNotFound, "User not found") |
| 177 | return |
| 178 | } |
| 179 | if !user.TfaEnabled { |
| 180 | Error(w, http.StatusBadRequest, "Two-factor authentication is not enabled for this account") |
| 181 | return |
| 182 | } |
| 183 | if user.PasswordHash == nil { |
| 184 | Error(w, http.StatusBadRequest, "Cannot disable TFA for accounts without a password (e.g., OIDC-only accounts)") |
| 185 | return |
| 186 | } |
| 187 | |
| 188 | hash := strings.TrimSpace(*user.PasswordHash) |
| 189 | if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(req.Password)); err != nil { |
| 190 | Error(w, http.StatusUnauthorized, "Invalid password") |
| 191 | return |
| 192 | } |
| 193 | |
| 194 | if err := h.users.DisableTfa(r.Context(), userID); err != nil { |
| 195 | if h.log != nil { |
| 196 | h.log.Error("tfa disable", "error", err) |
| 197 | } |
| 198 | Error(w, http.StatusInternalServerError, "Failed to disable two-factor authentication") |
| 199 | return |
| 200 | } |
| 201 | // All trusted-device records exist solely to skip MFA. With MFA disabled they |
| 202 | // have no purpose and should not linger if the user re-enables MFA later. |
| 203 | if h.trustedDevices != nil { |
| 204 | if err := h.trustedDevices.RevokeAllForUser(r.Context(), userID); err != nil && h.log != nil { |
| 205 | h.log.Error("tfa disable revoke trusted devices failed", "user_id", userID, "error", err) |
| 206 | } |
| 207 | } |
| 208 | clearDeviceTrustCookie(w, r) |
| 209 | |
| 210 | // Emit user_tfa_disabled event |
| 211 | if h.notify != nil { |
| 212 | if d := h.db.DB(r.Context()); d != nil { |
| 213 | h.notify.EmitEvent(r.Context(), d, hostctx.TenantHostKey(r.Context()), notifications.Event{ |
| 214 | Type: "user_tfa_disabled", |
nothing calls this directly
no test coverage detected