internal API plus brute force security
This commit is contained in:
parent
120b017b1a
commit
2f5d7ed712
9 changed files with 149 additions and 12 deletions
|
@ -2,27 +2,47 @@ package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.authentication.BadCredentialsException;
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
import org.springframework.security.authentication.LockedException;
|
import org.springframework.security.authentication.LockedException;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
@Component
|
||||||
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private final LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public CustomAuthenticationFailureHandler(LoginAttemptService loginAttemptService) {
|
||||||
|
this.loginAttemptService = loginAttemptService;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
|
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
|
||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
String ip = request.getRemoteAddr();
|
String ip = request.getRemoteAddr();
|
||||||
logger.error("Failed login attempt from IP: " + ip);
|
logger.error("Failed login attempt from IP: " + ip);
|
||||||
|
|
||||||
|
String username = request.getParameter("username");
|
||||||
|
if(loginAttemptService.loginAttemptCheck(username)) {
|
||||||
|
setDefaultFailureUrl("/login?error=locked");
|
||||||
|
System.out.println("test?");
|
||||||
|
|
||||||
|
} else {
|
||||||
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) {
|
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) {
|
||||||
setDefaultFailureUrl("/login?error=badcredentials");
|
setDefaultFailureUrl("/login?error=badcredentials");
|
||||||
} else if (exception.getClass().isAssignableFrom(LockedException.class)) {
|
} else if (exception.getClass().isAssignableFrom(LockedException.class)) {
|
||||||
setDefaultFailureUrl("/login?error=locked");
|
setDefaultFailureUrl("/login?error=locked");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
super.onAuthenticationFailure(request, response, exception);
|
super.onAuthenticationFailure(request, response, exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.authentication.LockedException;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
@ -22,12 +23,18 @@ public class CustomUserDetailsService implements UserDetailsService {
|
||||||
@Autowired
|
@Autowired
|
||||||
private UserRepository userRepository;
|
private UserRepository userRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||||
User user = userRepository.findByUsername(username)
|
User user = userRepository.findByUsername(username)
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("No user found with username: " + 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(
|
return new org.springframework.security.core.userdetails.User(
|
||||||
user.getUsername(),
|
user.getUsername(),
|
||||||
user.getPassword(),
|
user.getPassword(),
|
||||||
|
|
|
@ -38,7 +38,8 @@ public class InitialSecuritySetup {
|
||||||
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId(), true);
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -43,7 +43,11 @@ public class SecurityConfiguration {
|
||||||
private UserAuthenticationFilter userAuthenticationFilter;
|
private UserAuthenticationFilter userAuthenticationFilter;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private FirstLoginFilter firstLoginFilter;
|
private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
|
||||||
|
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
@ -57,9 +61,9 @@ public class SecurityConfiguration {
|
||||||
http
|
http
|
||||||
.formLogin(formLogin -> formLogin
|
.formLogin(formLogin -> formLogin
|
||||||
.loginPage("/login")
|
.loginPage("/login")
|
||||||
|
.successHandler(customAuthenticationSuccessHandler)
|
||||||
// .defaultSuccessUrl("/")
|
// .defaultSuccessUrl("/")
|
||||||
.successHandler(new SavedRequestAwareAuthenticationSuccessHandler())
|
.failureHandler(new CustomAuthenticationFailureHandler(loginAttemptService))
|
||||||
.failureHandler(new CustomAuthenticationFailureHandler())
|
|
||||||
.permitAll()
|
.permitAll()
|
||||||
)
|
)
|
||||||
.logout(logout -> logout
|
.logout(logout -> logout
|
||||||
|
@ -89,6 +93,8 @@ public class SecurityConfiguration {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public IPRateLimitingFilter rateLimitingFilter() {
|
public IPRateLimitingFilter rateLimitingFilter() {
|
||||||
int maxRequestsPerIp = 10000; // Example limit
|
int maxRequestsPerIp = 10000; // Example limit
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,10 @@ public enum Role {
|
||||||
EXTRA_LIMITED_API_USER("ROLE_EXTRA_LIMITED_API_USER", 20, 20),
|
EXTRA_LIMITED_API_USER("ROLE_EXTRA_LIMITED_API_USER", 20, 20),
|
||||||
|
|
||||||
// 0 API calls per day and 20 web calls
|
// 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 String roleId;
|
||||||
private final int apiCallsPerDay;
|
private final int apiCallsPerDay;
|
||||||
|
|
Loading…
Reference in a new issue