ChangePasswordController.java

package com.taxonomy.security.controller;

import com.taxonomy.security.model.AppUser;
import com.taxonomy.security.repository.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.Optional;

/**
 * Allows authenticated users to change their password.
 * <p>
 * When {@code taxonomy.security.require-password-change=true} and the user still
 * has the default password, Spring Security redirects all GUI requests here.
 * <p>
 * Only active when local user management is enabled (without Keycloak).
 * In the Keycloak profile, password changes are handled by the identity provider.
 */
@Controller
@ConditionalOnProperty(name = "taxonomy.security.change-password-enabled",
        havingValue = "true", matchIfMissing = true)
public class ChangePasswordController {

    private static final Logger log = LoggerFactory.getLogger(ChangePasswordController.class);

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public ChangePasswordController(UserRepository userRepository, PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    @GetMapping("/change-password")
    public String showChangePasswordForm(Model model) {
        return "change-password";
    }

    @PostMapping("/change-password")
    public String changePassword(Authentication authentication,
                                 @RequestParam String currentPassword,
                                 @RequestParam String newPassword,
                                 @RequestParam String confirmPassword,
                                 Model model) {
        if (authentication == null) {
            return "redirect:/login";
        }

        String username = authentication.getName();
        Optional<AppUser> optionalUser = userRepository.findByUsername(username);
        if (optionalUser.isEmpty()) {
            model.addAttribute("error", "User not found.");
            return "change-password";
        }

        AppUser user = optionalUser.get();

        // Validate current password
        if (!passwordEncoder.matches(currentPassword, user.getPasswordHash())) {
            model.addAttribute("error", "Current password is incorrect.");
            return "change-password";
        }

        // Validate new password
        if (newPassword == null || newPassword.length() < 8) {
            model.addAttribute("error", "New password must be at least 8 characters.");
            return "change-password";
        }

        if (!newPassword.equals(confirmPassword)) {
            model.addAttribute("error", "New passwords do not match.");
            return "change-password";
        }

        if (newPassword.equals(currentPassword)) {
            model.addAttribute("error", "New password must be different from the current password.");
            return "change-password";
        }

        // Update password
        user.setPasswordHash(passwordEncoder.encode(newPassword));
        userRepository.save(user);
        log.info("PASSWORD_CHANGED user={}", username);

        model.addAttribute("success", "Password changed successfully.");
        return "change-password";
    }
}