internal API plus brute force security

This commit is contained in:
Anthony Stirling 2023-12-24 17:12:32 +00:00
parent 120b017b1a
commit 2f5d7ed712
9 changed files with 149 additions and 12 deletions

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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(),

View file

@ -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());
}
}

View file

@ -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<String, AttemptCounter> 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;
}
}

View file

@ -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

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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;