From 2f5d7ed712809b88289a5744578f5bd2d2d222b7 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sun, 24 Dec 2023 17:12:32 +0000 Subject: [PATCH] internal API plus brute force security --- .../CustomAuthenticationFailureHandler.java | 28 ++++++++++-- .../CustomAuthenticationSuccessHandler.java | 30 +++++++++++++ .../security/CustomUserDetailsService.java | 9 +++- .../config/security/InitialSecuritySetup.java | 3 +- .../config/security/LoginAttemptService.java | 43 +++++++++++++++++++ .../security/SecurityConfiguration.java | 14 ++++-- .../security/UserAuthenticationFilter.java | 2 +- .../software/SPDF/model/AttemptCounter.java | 27 ++++++++++++ .../stirling/software/SPDF/model/Role.java | 5 ++- 9 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationSuccessHandler.java create mode 100644 src/main/java/stirling/software/SPDF/config/security/LoginAttemptService.java create mode 100644 src/main/java/stirling/software/SPDF/model/AttemptCounter.java diff --git a/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationFailureHandler.java b/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationFailureHandler.java index f286f149..9d8ff07c 100644 --- a/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationFailureHandler.java +++ b/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationFailureHandler.java @@ -2,27 +2,47 @@ package stirling.software.SPDF.config.security; import java.io.IOException; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.LockedException; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.stereotype.Component; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; - +@Component public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { + + @Autowired + private final LoginAttemptService loginAttemptService; + @Autowired + public CustomAuthenticationFailureHandler(LoginAttemptService loginAttemptService) { + this.loginAttemptService = loginAttemptService; + } + @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { String ip = request.getRemoteAddr(); logger.error("Failed login attempt from IP: " + ip); - if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) { - setDefaultFailureUrl("/login?error=badcredentials"); - } else if (exception.getClass().isAssignableFrom(LockedException.class)) { + + String username = request.getParameter("username"); + if(loginAttemptService.loginAttemptCheck(username)) { setDefaultFailureUrl("/login?error=locked"); + System.out.println("test?"); + + } else { + 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/CustomAuthenticationSuccessHandler.java b/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationSuccessHandler.java new file mode 100644 index 00000000..db934f97 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationSuccessHandler.java @@ -0,0 +1,30 @@ +package stirling.software.SPDF.config.security; + +import java.io.IOException; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.LockedException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@Component +public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { + + @Autowired + private LoginAttemptService loginAttemptService; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { + String username = request.getParameter("username"); + loginAttemptService.loginSucceeded(username); + super.onAuthenticationSuccess(request, response, authentication); + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java b/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java index 7ae1680b..77db2cd4 100644 --- a/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java +++ b/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java @@ -5,6 +5,7 @@ import java.util.Set; import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.LockedException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; @@ -22,12 +23,18 @@ public class CustomUserDetailsService implements UserDetailsService { @Autowired private UserRepository userRepository; - + @Autowired + private LoginAttemptService loginAttemptService; + @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("No user found with username: " + username)); + if (loginAttemptService.isBlocked(username)) { + throw new LockedException("Your account has been locked due to too many failed login attempts."); + } + return new org.springframework.security.core.userdetails.User( user.getUsername(), user.getPassword(), diff --git a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java index 842375d8..fd89af30 100644 --- a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java +++ b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java @@ -38,7 +38,8 @@ public class InitialSecuritySetup { userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId(), true); } - + userService.saveUser(Role.INTERNAL_API_USER.getRoleId(), UUID.randomUUID().toString(), Role.USER.getRoleId()); + userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId()); } } diff --git a/src/main/java/stirling/software/SPDF/config/security/LoginAttemptService.java b/src/main/java/stirling/software/SPDF/config/security/LoginAttemptService.java new file mode 100644 index 00000000..ce38de1a --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/LoginAttemptService.java @@ -0,0 +1,43 @@ +package stirling.software.SPDF.config.security; +import org.springframework.stereotype.Service; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import stirling.software.SPDF.model.AttemptCounter; + +@Service +public class LoginAttemptService { + + private final int MAX_ATTEMPTS = 2; + private final long ATTEMPT_INCREMENT_TIME = TimeUnit.MINUTES.toMillis(1); + private final ConcurrentHashMap attemptsCache = new ConcurrentHashMap<>(); + + public void loginSucceeded(String key) { + System.out.println("here3 reset "); + attemptsCache.remove(key); + } + + public boolean loginAttemptCheck(String key) { + System.out.println("here"); + attemptsCache.compute(key, (k, attemptCounter) -> { + if (attemptCounter == null || attemptCounter.shouldReset(ATTEMPT_INCREMENT_TIME)) { + return new AttemptCounter(); + } else { + attemptCounter.increment(); + return attemptCounter; + } + }); + System.out.println("here2 = " + attemptsCache.get(key).getAttemptCount()); + return attemptsCache.get(key).getAttemptCount() >= MAX_ATTEMPTS; + } + + + public boolean isBlocked(String key) { + AttemptCounter attemptCounter = attemptsCache.get(key); + if (attemptCounter != null) { + return attemptCounter.getAttemptCount() >= MAX_ATTEMPTS; + } + return false; + } + +} diff --git a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java index 7a2541af..640a3987 100644 --- a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -41,9 +41,13 @@ public class SecurityConfiguration { @Autowired private UserAuthenticationFilter userAuthenticationFilter; - + @Autowired - private FirstLoginFilter firstLoginFilter; + private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler; + + + @Autowired + private LoginAttemptService loginAttemptService; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -57,9 +61,9 @@ public class SecurityConfiguration { http .formLogin(formLogin -> formLogin .loginPage("/login") + .successHandler(customAuthenticationSuccessHandler) // .defaultSuccessUrl("/") - .successHandler(new SavedRequestAwareAuthenticationSuccessHandler()) - .failureHandler(new CustomAuthenticationFailureHandler()) + .failureHandler(new CustomAuthenticationFailureHandler(loginAttemptService)) .permitAll() ) .logout(logout -> logout @@ -87,6 +91,8 @@ public class SecurityConfiguration { } return http.build(); } + + @Bean diff --git a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java index eca7f70e..d13cce7c 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java @@ -82,7 +82,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { response.getWriter().write("Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternativly you can disable authentication if this is unexpected"); return; } - } + } filterChain.doFilter(request, response); } diff --git a/src/main/java/stirling/software/SPDF/model/AttemptCounter.java b/src/main/java/stirling/software/SPDF/model/AttemptCounter.java new file mode 100644 index 00000000..94f8ef4a --- /dev/null +++ b/src/main/java/stirling/software/SPDF/model/AttemptCounter.java @@ -0,0 +1,27 @@ +package stirling.software.SPDF.model; +public class AttemptCounter { + private int attemptCount; + private long firstAttemptTime; + + public AttemptCounter() { + this.attemptCount = 1; + this.firstAttemptTime = System.currentTimeMillis(); + } + + public void increment() { + this.attemptCount++; + this.firstAttemptTime = System.currentTimeMillis(); + } + + public int getAttemptCount() { + return attemptCount; + } + + public long getFirstAttemptTime() { + return firstAttemptTime; + } + + public boolean shouldReset(long ATTEMPT_INCREMENT_TIME) { + return System.currentTimeMillis() - firstAttemptTime > ATTEMPT_INCREMENT_TIME; + } +} diff --git a/src/main/java/stirling/software/SPDF/model/Role.java b/src/main/java/stirling/software/SPDF/model/Role.java index 1b775de0..531f12fb 100644 --- a/src/main/java/stirling/software/SPDF/model/Role.java +++ b/src/main/java/stirling/software/SPDF/model/Role.java @@ -14,7 +14,10 @@ public enum Role { EXTRA_LIMITED_API_USER("ROLE_EXTRA_LIMITED_API_USER", 20, 20), // 0 API calls per day and 20 web calls - WEB_ONLY_USER("ROLE_WEB_ONLY_USER", 0, 20); + WEB_ONLY_USER("ROLE_WEB_ONLY_USER", 0, 20), + + + INTERNAL_API_USER("STIRLING-PDF-BACKEND-API-USER", Integer.MAX_VALUE, Integer.MAX_VALUE); private final String roleId; private final int apiCallsPerDay;