Merge branch 'main' into changes

This commit is contained in:
Anthony Stirling
2023-12-31 13:33:10 +00:00
committed by GitHub
174 changed files with 5505 additions and 4723 deletions

View File

@@ -12,36 +12,38 @@ 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 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();
public void onAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {
String ip = request.getRemoteAddr();
logger.error("Failed login attempt from IP: " + ip);
String username = request.getParameter("username");
if(loginAttemptService.loginAttemptCheck(username)) {
setDefaultFailureUrl("/login?error=locked");
if (loginAttemptService.loginAttemptCheck(username)) {
setDefaultFailureUrl("/login?error=locked");
} else {
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) {
setDefaultFailureUrl("/login?error=badcredentials");
} else if (exception.getClass().isAssignableFrom(LockedException.class)) {
setDefaultFailureUrl("/login?error=locked");
}
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

@@ -15,30 +15,33 @@ import jakarta.servlet.http.HttpSession;
import stirling.software.SPDF.utils.RequestUriUtils;
@Component
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
public class CustomAuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private LoginAttemptService loginAttemptService;
@Autowired private LoginAttemptService loginAttemptService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
String username = request.getParameter("username");
public void onAuthenticationSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
String username = request.getParameter("username");
loginAttemptService.loginSucceeded(username);
// Get the saved request
HttpSession session = request.getSession(false);
SavedRequest savedRequest = session != null ? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST") : null;
if (savedRequest != null && !RequestUriUtils.isStaticResource(savedRequest.getRedirectUrl())) {
SavedRequest savedRequest =
session != null
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
: null;
if (savedRequest != null
&& !RequestUriUtils.isStaticResource(savedRequest.getRedirectUrl())) {
// Redirect to the original destination
super.onAuthenticationSuccess(request, response, authentication);
} else {
// Redirect to the root URL (considering context path)
getRedirectStrategy().sendRedirect(request, response, "/");
}
//super.onAuthenticationSuccess(request, response, authentication);
}
// super.onAuthenticationSuccess(request, response, authentication);
}
}

View File

@@ -20,33 +20,38 @@ import stirling.software.SPDF.repository.UserRepository;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Autowired private UserRepository userRepository;
@Autowired private LoginAttemptService loginAttemptService;
@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));
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.");
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(),
user.isEnabled(),
true, true, true,
getAuthorities(user.getAuthorities())
);
user.getUsername(),
user.getPassword(),
user.isEnabled(),
true,
true,
true,
getAuthorities(user.getAuthorities()));
}
private Collection<? extends GrantedAuthority> getAuthorities(Set<Authority> authorities) {
return authorities.stream()
.map(authority -> new SimpleGrantedAuthority(authority.getAuthority()))
.collect(Collectors.toList());
.map(authority -> new SimpleGrantedAuthority(authority.getAuthority()))
.collect(Collectors.toList());
}
}

View File

@@ -19,16 +19,16 @@ import stirling.software.SPDF.utils.RequestUriUtils;
@Component
public class FirstLoginFilter extends OncePerRequestFilter {
@Autowired
@Lazy
private UserService userService;
@Autowired @Lazy private UserService userService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String method = request.getMethod();
String requestURI = request.getRequestURI();
// Check if the request is for static resources
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String method = request.getMethod();
String requestURI = request.getRequestURI();
// Check if the request is for static resources
boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI);
// If it's a static resource, just continue the filter chain and skip the logic below
@@ -36,11 +36,14 @@ public class FirstLoginFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response);
return;
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
Optional<User> user = userService.findByUsername(authentication.getName());
if ("GET".equalsIgnoreCase(method) && user.isPresent() && user.get().isFirstLogin() && !"/change-creds".equals(requestURI)) {
if ("GET".equalsIgnoreCase(method)
&& user.isPresent()
&& user.get().isFirstLogin()
&& !"/change-creds".equals(requestURI)) {
response.sendRedirect("/change-creds");
return;
}

View File

@@ -1,4 +1,5 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@@ -13,51 +14,53 @@ import stirling.software.SPDF.utils.RequestUriUtils;
public class IPRateLimitingFilter implements Filter {
private final ConcurrentHashMap<String, AtomicInteger> requestCounts = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, AtomicInteger> requestCounts =
new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, AtomicInteger> getCounts = new ConcurrentHashMap<>();
private final int maxRequests;
private final int maxGetRequests;
public IPRateLimitingFilter(int maxRequests, int maxGetRequests) {
this.maxRequests = maxRequests;
this.maxGetRequests = maxGetRequests;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String method = httpRequest.getMethod();
String requestURI = httpRequest.getRequestURI();
// Check if the request is for static resources
boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI);
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (request instanceof HttpServletRequest) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String method = httpRequest.getMethod();
String requestURI = httpRequest.getRequestURI();
// Check if the request is for static resources
boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI);
// If it's a static resource, just continue the filter chain and skip the logic below
if (isStaticResource) {
chain.doFilter(request, response);
return;
}
String clientIp = request.getRemoteAddr();
requestCounts.computeIfAbsent(clientIp, k -> new AtomicInteger(0));
if (!"GET".equalsIgnoreCase(method)) {
if (requestCounts.get(clientIp).incrementAndGet() > maxRequests) {
// Handle limit exceeded (e.g., send error response)
response.getWriter().write("Rate limit exceeded");
return;
}
} else {
if (requestCounts.get(clientIp).incrementAndGet() > maxGetRequests) {
// Handle limit exceeded (e.g., send error response)
response.getWriter().write("GET Rate limit exceeded");
return;
}
}
}
chain.doFilter(request, response);
// If it's a static resource, just continue the filter chain and skip the logic below
if (isStaticResource) {
chain.doFilter(request, response);
return;
}
String clientIp = request.getRemoteAddr();
requestCounts.computeIfAbsent(clientIp, k -> new AtomicInteger(0));
if (!"GET".equalsIgnoreCase(method)) {
if (requestCounts.get(clientIp).incrementAndGet() > maxRequests) {
// Handle limit exceeded (e.g., send error response)
response.getWriter().write("Rate limit exceeded");
return;
}
} else {
if (requestCounts.get(clientIp).incrementAndGet() > maxGetRequests) {
// Handle limit exceeded (e.g., send error response)
response.getWriter().write("GET Rate limit exceeded");
return;
}
}
}
chain.doFilter(request, response);
}
public void resetRequestCounts() {
requestCounts.clear();
getCounts.clear();

View File

@@ -13,75 +13,76 @@ import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.Role;
@Component
public class InitialSecuritySetup {
@Autowired
private UserService userService;
@Autowired private UserService userService;
@Autowired ApplicationProperties applicationProperties;
@Autowired
ApplicationProperties applicationProperties;
@PostConstruct
public void init() {
if (!userService.hasUsers()) {
String initialUsername = applicationProperties.getSecurity().getInitialLogin().getUsername();
String initialPassword = applicationProperties.getSecurity().getInitialLogin().getPassword();
if (initialUsername != null && initialPassword != null) {
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
} else {
initialUsername = "admin";
initialPassword = "stirling";
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId(), true);
}
}
if(!userService.usernameExists(Role.INTERNAL_API_USER.getRoleId())) {
userService.saveUser(Role.INTERNAL_API_USER.getRoleId(), UUID.randomUUID().toString(), Role.INTERNAL_API_USER.getRoleId());
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
}
}
@PostConstruct
public void init() {
if (!userService.hasUsers()) {
String initialUsername =
applicationProperties.getSecurity().getInitialLogin().getUsername();
String initialPassword =
applicationProperties.getSecurity().getInitialLogin().getPassword();
if (initialUsername != null && initialPassword != null) {
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
} else {
initialUsername = "admin";
initialPassword = "stirling";
userService.saveUser(
initialUsername, initialPassword, Role.ADMIN.getRoleId(), true);
}
}
if (!userService.usernameExists(Role.INTERNAL_API_USER.getRoleId())) {
userService.saveUser(
Role.INTERNAL_API_USER.getRoleId(),
UUID.randomUUID().toString(),
Role.INTERNAL_API_USER.getRoleId());
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
}
}
@PostConstruct
public void initSecretKey() throws IOException {
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
if (secretKey == null || secretKey.isEmpty()) {
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
saveKeyToConfig(secretKey);
}
}
@PostConstruct
public void initSecretKey() throws IOException {
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
if (secretKey == null || secretKey.isEmpty()) {
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
saveKeyToConfig(secretKey);
}
}
private void saveKeyToConfig(String key) throws IOException {
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
List<String> lines = Files.readAllLines(path);
boolean keyFound = false;
private void saveKeyToConfig(String key) throws IOException {
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
List<String> lines = Files.readAllLines(path);
boolean keyFound = false;
// Search for the existing key to replace it or place to add it
for (int i = 0; i < lines.size(); i++) {
if (lines.get(i).startsWith("AutomaticallyGenerated:")) {
keyFound = true;
if (i + 1 < lines.size() && lines.get(i + 1).trim().startsWith("key:")) {
lines.set(i + 1, " key: " + key);
break;
} else {
lines.add(i + 1, " key: " + key);
break;
}
}
}
// Search for the existing key to replace it or place to add it
for (int i = 0; i < lines.size(); i++) {
if (lines.get(i).startsWith("AutomaticallyGenerated:")) {
keyFound = true;
if (i + 1 < lines.size() && lines.get(i + 1).trim().startsWith("key:")) {
lines.set(i + 1, " key: " + key);
break;
} else {
lines.add(i + 1, " key: " + key);
break;
}
}
}
// If the section doesn't exist, append it
if (!keyFound) {
lines.add("# Automatically Generated Settings (Do Not Edit Directly)");
lines.add("AutomaticallyGenerated:");
lines.add(" key: " + key);
}
// If the section doesn't exist, append it
if (!keyFound) {
lines.add("# Automatically Generated Settings (Do Not Edit Directly)");
lines.add("AutomaticallyGenerated:");
lines.add(" key: " + key);
}
// Write back to the file
Files.write(path, lines);
}
}
// Write back to the file
Files.write(path, lines);
}
}

View File

@@ -1,4 +1,5 @@
package stirling.software.SPDF.config.security;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@@ -12,39 +13,41 @@ import stirling.software.SPDF.model.AttemptCounter;
@Service
public class LoginAttemptService {
@Autowired
ApplicationProperties applicationProperties;
@Autowired ApplicationProperties applicationProperties;
private int MAX_ATTEMPTS;
private long ATTEMPT_INCREMENT_TIME;
@PostConstruct
public void init() {
MAX_ATTEMPTS = applicationProperties.getSecurity().getLoginAttemptCount();
ATTEMPT_INCREMENT_TIME = TimeUnit.MINUTES.toMillis(applicationProperties.getSecurity().getLoginResetTimeMinutes());
MAX_ATTEMPTS = applicationProperties.getSecurity().getLoginAttemptCount();
ATTEMPT_INCREMENT_TIME =
TimeUnit.MINUTES.toMillis(
applicationProperties.getSecurity().getLoginResetTimeMinutes());
}
private final ConcurrentHashMap<String, AttemptCounter> attemptsCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, AttemptCounter> attemptsCache =
new ConcurrentHashMap<>();
public void loginSucceeded(String key) {
attemptsCache.remove(key);
}
public boolean loginAttemptCheck(String key) {
attemptsCache.compute(key, (k, attemptCounter) -> {
if (attemptCounter == null || attemptCounter.shouldReset(ATTEMPT_INCREMENT_TIME)) {
return new AttemptCounter();
} else {
attemptCounter.increment();
return attemptCounter;
}
});
attemptsCache.compute(
key,
(k, attemptCounter) -> {
if (attemptCounter == null
|| attemptCounter.shouldReset(ATTEMPT_INCREMENT_TIME)) {
return new AttemptCounter();
} else {
attemptCounter.increment();
return attemptCounter;
}
});
return attemptsCache.get(key).getAttemptCount() >= MAX_ATTEMPTS;
}
public boolean isBlocked(String key) {
AttemptCounter attemptCounter = attemptsCache.get(key);
if (attemptCounter != null) {
@@ -52,5 +55,4 @@ public class LoginAttemptService {
}
return false;
}
}

View File

@@ -1,4 +1,5 @@
package stirling.software.SPDF.config.security;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@@ -11,7 +12,7 @@ public class RateLimitResetScheduler {
this.rateLimitingFilter = rateLimitingFilter;
}
@Scheduled(cron = "0 0 0 * * MON") // At 00:00 every Monday TODO: configurable
@Scheduled(cron = "0 0 0 * * MON") // At 00:00 every Monday TODO: configurable
public void resetRateLimit() {
rateLimitingFilter.resetRequestCounts();
}

View File

@@ -19,105 +19,111 @@ import org.springframework.security.web.savedrequest.NullRequestCache;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
@Configuration
@EnableWebSecurity()
@EnableMethodSecurity
public class SecurityConfiguration {
@Autowired
private UserDetailsService userDetailsService;
@Autowired private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
@Lazy
private UserService userService;
@Autowired @Lazy private UserService userService;
@Autowired
@Qualifier("loginEnabled")
public boolean loginEnabledValue;
@Autowired
private UserAuthenticationFilter userAuthenticationFilter;
@Autowired private UserAuthenticationFilter userAuthenticationFilter;
@Autowired private LoginAttemptService loginAttemptService;
@Autowired private FirstLoginFilter firstLoginFilter;
@Autowired
private LoginAttemptService loginAttemptService;
@Autowired
private FirstLoginFilter firstLoginFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
if(loginEnabledValue) {
http.csrf(csrf -> csrf.disable());
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
http
.formLogin(formLogin -> formLogin
.loginPage("/login")
.successHandler(new CustomAuthenticationSuccessHandler())
.defaultSuccessUrl("/")
.failureHandler(new CustomAuthenticationFailureHandler(loginAttemptService))
.permitAll()
).requestCache(requestCache -> requestCache
.requestCache(new NullRequestCache())
)
.logout(logout -> logout
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/login?logout=true")
.invalidateHttpSession(true) // Invalidate session
.deleteCookies("JSESSIONID", "remember-me")
).rememberMe(rememberMeConfigurer -> rememberMeConfigurer // Use the configurator directly
.key("uniqueAndSecret")
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(1209600) // 2 weeks
)
.authorizeHttpRequests(authz -> authz
.requestMatchers(req -> {
String uri = req.getRequestURI();
String contextPath = req.getContextPath();
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
// Remove the context path from the URI
String trimmedUri = uri.startsWith(contextPath) ? uri.substring(contextPath.length()) : uri;
if (loginEnabledValue) {
http.csrf(csrf -> csrf.disable());
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
http.formLogin(
formLogin ->
formLogin
.loginPage("/login")
.successHandler(
new CustomAuthenticationSuccessHandler())
.defaultSuccessUrl("/")
.failureHandler(
new CustomAuthenticationFailureHandler(
loginAttemptService))
.permitAll())
.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()))
.logout(
logout ->
logout.logoutRequestMatcher(
new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/login?logout=true")
.invalidateHttpSession(true) // Invalidate session
.deleteCookies("JSESSIONID", "remember-me"))
.rememberMe(
rememberMeConfigurer ->
rememberMeConfigurer // Use the configurator directly
.key("uniqueAndSecret")
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(1209600) // 2 weeks
)
.authorizeHttpRequests(
authz ->
authz.requestMatchers(
req -> {
String uri = req.getRequestURI();
String contextPath = req.getContextPath();
// Remove the context path from the URI
String trimmedUri =
uri.startsWith(contextPath)
? uri.substring(
contextPath
.length())
: uri;
return trimmedUri.startsWith("/login")
|| trimmedUri.endsWith(".svg")
|| trimmedUri.startsWith(
"/register")
|| trimmedUri.startsWith("/error")
|| trimmedUri.startsWith("/images/")
|| trimmedUri.startsWith("/public/")
|| trimmedUri.startsWith("/css/")
|| trimmedUri.startsWith("/js/") ||
trimmedUri.startsWith("/api/v1/info/status");
})
.permitAll()
.anyRequest()
.authenticated())
.userDetailsService(userDetailsService)
.authenticationProvider(authenticationProvider());
} else {
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
}
return trimmedUri.startsWith("/login") || trimmedUri.endsWith(".svg") ||
trimmedUri.startsWith("/register") || trimmedUri.startsWith("/error") ||
trimmedUri.startsWith("/images/") || trimmedUri.startsWith("/public/") ||
trimmedUri.startsWith("/css/") || trimmedUri.startsWith("/js/") ||
trimmedUri.startsWith("/api/v1/info/status");
}
).permitAll()
.anyRequest().authenticated()
)
.userDetailsService(userDetailsService)
.authenticationProvider(authenticationProvider());
} else {
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz
.anyRequest().permitAll()
);
}
return http.build();
}
@Bean
public IPRateLimitingFilter rateLimitingFilter() {
int maxRequestsPerIp = 1000000; // Example limit TODO add config level
return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
@@ -125,13 +131,9 @@ public class SecurityConfiguration {
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
return new JPATokenRepositoryImpl();
}
}

View File

@@ -19,32 +19,29 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
@Component
public class UserAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired private UserDetailsService userDetailsService;
@Autowired @Lazy private UserService userService;
@Autowired
@Lazy
private UserService userService;
@Autowired
@Qualifier("loginEnabled")
public boolean loginEnabledValue;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!loginEnabledValue) {
// If login is not enabled, just pass all requests without authentication
filterChain.doFilter(request, response);
return;
}
String requestURI = request.getRequestURI();
String requestURI = request.getRequestURI();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// Check for API key in the request headers if no authentication exists
@@ -52,15 +49,17 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
String apiKey = request.getHeader("X-API-Key");
if (apiKey != null && !apiKey.trim().isEmpty()) {
try {
// Use API key to authenticate. This requires you to have an authentication provider for API keys.
UserDetails userDetails = userService.loadUserByApiKey(apiKey);
if(userDetails == null)
{
response.setStatus(HttpStatus.UNAUTHORIZED.value());
// Use API key to authenticate. This requires you to have an authentication
// provider for API keys.
UserDetails userDetails = userService.loadUserByApiKey(apiKey);
if (userDetails == null) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Invalid API Key.");
return;
}
authentication = new ApiKeyAuthenticationToken(userDetails, apiKey, userDetails.getAuthorities());
}
authentication =
new ApiKeyAuthenticationToken(
userDetails, apiKey, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (AuthenticationException e) {
// If API key authentication fails, deny the request
@@ -73,22 +72,24 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
// If we still don't have any authentication, deny the request
if (authentication == null || !authentication.isAuthenticated()) {
String method = request.getMethod();
String contextPath = request.getContextPath();
if ("GET".equalsIgnoreCase(method) && ! (contextPath + "/login").equals(requestURI)) {
response.sendRedirect(contextPath + "/login"); // redirect to the login page
return;
String method = request.getMethod();
String contextPath = request.getContextPath();
if ("GET".equalsIgnoreCase(method) && !(contextPath + "/login").equals(requestURI)) {
response.sendRedirect(contextPath + "/login"); // redirect to the login page
return;
} else {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
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;
response.setStatus(HttpStatus.UNAUTHORIZED.value());
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);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String uri = request.getRequestURI();
@@ -114,5 +115,4 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
return false;
}
}

View File

@@ -20,28 +20,29 @@ import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.ConsumptionProbe;
import io.github.bucket4j.Refill;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.model.Role;
@Component
public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>();
private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>();
private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>();
@Autowired
private UserDetailsService userDetailsService;
@Autowired private UserDetailsService userDetailsService;
@Autowired
@Qualifier("rateLimit")
public boolean rateLimit;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!rateLimit) {
// If rateLimit is not enabled, just pass all requests without rate limiting
filterChain.doFilter(request, response);
@@ -60,7 +61,8 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
// Check for API key in the request headers
String apiKey = request.getHeader("X-API-Key");
if (apiKey != null && !apiKey.trim().isEmpty()) {
identifier = "API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames
identifier =
"API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames
} else {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
@@ -74,14 +76,27 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
identifier = request.getRemoteAddr();
}
Role userRole = getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
Role userRole =
getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
if (request.getHeader("X-API-Key") != null) {
// It's an API call
processRequest(userRole.getApiCallsPerDay(), identifier, apiBuckets, request, response, filterChain);
processRequest(
userRole.getApiCallsPerDay(),
identifier,
apiBuckets,
request,
response,
filterChain);
} else {
// It's a Web UI call
processRequest(userRole.getWebCallsPerDay(), identifier, webBuckets, request, response, filterChain);
processRequest(
userRole.getWebCallsPerDay(),
identifier,
webBuckets,
request,
response,
filterChain);
}
}
@@ -98,8 +113,13 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
throw new IllegalStateException("User does not have a valid role.");
}
private void processRequest(int limitPerDay, String identifier, Map<String, Bucket> buckets,
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
private void processRequest(
int limitPerDay,
String identifier,
Map<String, Bucket> buckets,
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws IOException, ServletException {
Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay));
ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1);
@@ -116,10 +136,8 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
}
private Bucket createUserBucket(int limitPerDay) {
Bandwidth limit = Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1)));
Bandwidth limit =
Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1)));
return Bucket.builder().addLimit(limit).build();
}
}

View File

@@ -1,4 +1,5 @@
package stirling.software.SPDF.config.security;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@@ -21,38 +22,35 @@ import stirling.software.SPDF.model.Authority;
import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.UserRepository;
@Service
public class UserService implements UserServiceInterface{
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Service
public class UserService implements UserServiceInterface {
@Autowired private UserRepository userRepository;
@Autowired private PasswordEncoder passwordEncoder;
public Authentication getAuthentication(String apiKey) {
User user = getUserByApiKey(apiKey);
if (user == null) {
throw new UsernameNotFoundException("API key is not valid");
}
// Convert the user into an Authentication object
return new UsernamePasswordAuthenticationToken(
user, // principal (typically the user)
null, // credentials (we don't expose the password or API key here)
getAuthorities(user) // user's authorities (roles/permissions)
);
user, // principal (typically the user)
null, // credentials (we don't expose the password or API key here)
getAuthorities(user) // user's authorities (roles/permissions)
);
}
private Collection<? extends GrantedAuthority> getAuthorities(User user) {
// Convert each Authority object into a SimpleGrantedAuthority object.
return user.getAuthorities().stream()
.map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority()))
.collect(Collectors.toList());
return user.getAuthorities().stream()
.map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority()))
.collect(Collectors.toList());
}
private String generateApiKey() {
String apiKey;
do {
@@ -62,9 +60,11 @@ public class UserService implements UserServiceInterface{
}
public User addApiKeyToUser(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
User user =
userRepository
.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
user.setApiKey(generateApiKey());
return userRepository.save(user);
}
@@ -74,8 +74,10 @@ public class UserService implements UserServiceInterface{
}
public String getApiKeyForUser(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
User user =
userRepository
.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return user.getApiKey();
}
@@ -86,27 +88,25 @@ public class UserService implements UserServiceInterface{
public User getUserByApiKey(String apiKey) {
return userRepository.findByApiKey(apiKey);
}
public UserDetails loadUserByApiKey(String apiKey) {
User userOptional = userRepository.findByApiKey(apiKey);
if (userOptional != null) {
User user = userOptional;
// Convert your User entity to a UserDetails object with authorities
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(), // you might not need this for API key auth
getAuthorities(user)
);
user.getUsername(),
user.getPassword(), // you might not need this for API key auth
getAuthorities(user));
}
return null; // or throw an exception
return null; // or throw an exception
}
public boolean validateApiKeyForUser(String username, String apiKey) {
Optional<User> userOpt = userRepository.findByUsername(username);
return userOpt.isPresent() && userOpt.get().getApiKey().equals(apiKey);
}
public void saveUser(String username, String password) {
User user = new User();
user.setUsername(username);
@@ -124,7 +124,7 @@ public class UserService implements UserServiceInterface{
user.setFirstLogin(firstLogin);
userRepository.save(user);
}
public void saveUser(String username, String password, String role) {
User user = new User();
user.setUsername(username);
@@ -134,42 +134,42 @@ public class UserService implements UserServiceInterface{
user.setFirstLogin(false);
userRepository.save(user);
}
public void deleteUser(String username) {
Optional<User> userOpt = userRepository.findByUsername(username);
if (userOpt.isPresent()) {
for (Authority authority : userOpt.get().getAuthorities()) {
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
return;
}
}
userRepository.delete(userOpt.get());
}
Optional<User> userOpt = userRepository.findByUsername(username);
if (userOpt.isPresent()) {
for (Authority authority : userOpt.get().getAuthorities()) {
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
return;
}
}
userRepository.delete(userOpt.get());
}
}
public boolean usernameExists(String username) {
return userRepository.findByUsername(username).isPresent();
}
public boolean hasUsers() {
return userRepository.count() > 0;
}
public void updateUserSettings(String username, Map<String, String> updates) {
Optional<User> userOpt = userRepository.findByUsername(username);
if (userOpt.isPresent()) {
User user = userOpt.get();
Map<String, String> settingsMap = user.getSettings();
if(settingsMap == null) {
settingsMap = new HashMap<String,String>();
}
if (settingsMap == null) {
settingsMap = new HashMap<String, String>();
}
settingsMap.clear();
settingsMap.putAll(updates);
user.setSettings(settingsMap);
userRepository.save(user);
}
}
}
public Optional<User> findByUsername(String username) {
@@ -185,13 +185,12 @@ public class UserService implements UserServiceInterface{
user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user);
}
public void changeFirstUse(User user, boolean firstUse) {
user.setFirstLogin(firstUse);
userRepository.save(user);
}
public boolean isPasswordCorrect(User user, String currentPassword) {
return passwordEncoder.matches(currentPassword, user.getPassword());
}