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 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 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(),
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
||||
// 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;
|
||||
|
|
Loading…
Reference in a new issue