diff --git a/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationFailureHandler.java b/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationFailureHandler.java new file mode 100644 index 00000000..6b300920 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationFailureHandler.java @@ -0,0 +1,26 @@ +package stirling.software.SPDF.config.security; + +import java.io.IOException; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.LockedException; + +public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) + throws IOException, ServletException { + if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) { + setDefaultFailureUrl("/login?error=badcredentials"); + } else if (exception.getClass().isAssignableFrom(LockedException.class)) { + setDefaultFailureUrl("/login?error=locked"); + } + super.onAuthenticationFailure(request, response, exception); + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java b/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java new file mode 100644 index 00000000..21d6d678 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java @@ -0,0 +1,45 @@ +package stirling.software.SPDF.config.security; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import stirling.software.SPDF.model.Authority; +import stirling.software.SPDF.model.User; +import stirling.software.SPDF.repository.UserRepository; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + + @Autowired + private UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("No user found with username: " + username)); + + return new org.springframework.security.core.userdetails.User( + user.getUsername(), + user.getPassword(), + user.isEnabled(), + true, true, true, + getAuthorities(user.getAuthorities()) + ); + } + + private Collection getAuthorities(Set authorities) { + return authorities.stream() + .map(authority -> new SimpleGrantedAuthority(authority.getAuthority())) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java new file mode 100644 index 00000000..90cc2ca0 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -0,0 +1,78 @@ +package stirling.software.SPDF.config.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +@Configuration +public class SecurityConfiguration { + + @Autowired + private UserDetailsService userDetailsService; + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Autowired + @Qualifier("loginEnabled") + public boolean loginEnabledValue; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + if(loginEnabledValue) { + http.csrf().disable(); + http + .formLogin(formLogin -> formLogin + .loginPage("/login") + .defaultSuccessUrl("/") + .failureHandler(new CustomAuthenticationFailureHandler()) + .permitAll() + ) + .logout(logout -> logout + .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) + .logoutSuccessUrl("/login?logout=true") + .invalidateHttpSession(true) // Invalidate session + .deleteCookies("JSESSIONID") + ) + .authorizeHttpRequests(authz -> authz + .requestMatchers(req -> req.getRequestURI().startsWith("/login") || req.getRequestURI().endsWith(".svg") || req.getRequestURI().startsWith("/register") || req.getRequestURI().startsWith("/error") || req.getRequestURI().startsWith("/images/") || req.getRequestURI().startsWith("/public/") || req.getRequestURI().startsWith("/css/") || req.getRequestURI().startsWith("/js/")) + .permitAll() + .anyRequest().authenticated() + ) + .userDetailsService(userDetailsService) + .authenticationProvider(authenticationProvider()); + } else { + http + .csrf().disable() + .authorizeHttpRequests(authz -> authz + .anyRequest().permitAll() + ); + } + return http.build(); + } + + + + @Bean + public DaoAuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userDetailsService); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + +} + diff --git a/src/main/java/stirling/software/SPDF/config/security/UserService.java b/src/main/java/stirling/software/SPDF/config/security/UserService.java new file mode 100644 index 00000000..4965f52f --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/UserService.java @@ -0,0 +1,74 @@ +package stirling.software.SPDF.config.security; + +import java.util.Map; +import java.util.Optional; +import java.util.HashMap; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import stirling.software.SPDF.repository.UserRepository; +import stirling.software.SPDF.model.Authority; +import stirling.software.SPDF.model.Role; +import stirling.software.SPDF.model.User; +@Service +public class UserService { + + @Autowired + private UserRepository userRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + public void saveUser(String username, String password) { + User user = new User(); + user.setUsername(username); + user.setPassword(passwordEncoder.encode(password)); + user.setEnabled(true); + userRepository.save(user); + } + + public void saveUser(String username, String password, String role) { + User user = new User(); + user.setUsername(username); + user.setPassword(passwordEncoder.encode(password)); + user.addAuthority(new Authority(role, user)); + user.setEnabled(true); + userRepository.save(user); + } + + public void deleteUser(String username) { + Optional userOpt = userRepository.findByUsername(username); + if (userOpt.isPresent()) { + userRepository.delete(userOpt.get()); + } + } + + public boolean usernameExists(String username) { + return userRepository.findByUsername(username) != null; + } + + public boolean hasUsers() { + return userRepository.count() > 0; + } + + public void updateUserSettings(String username, Map updates) { + Optional userOpt = userRepository.findByUsername(username); + if (userOpt.isPresent()) { + User user = userOpt.get(); + Map settingsMap = user.getSettings(); + + if(settingsMap == null) { + settingsMap = new HashMap(); + } + settingsMap.clear(); + settingsMap.putAll(updates); + user.setSettings(settingsMap); + + userRepository.save(user); + } + } + + +}