diff --git a/src/main/java/stirling/software/SPDF/config/AppConfig.java b/src/main/java/stirling/software/SPDF/config/AppConfig.java index 3fd65261..29290a32 100644 --- a/src/main/java/stirling/software/SPDF/config/AppConfig.java +++ b/src/main/java/stirling/software/SPDF/config/AppConfig.java @@ -7,12 +7,22 @@ import org.springframework.context.annotation.Configuration; public class AppConfig { + + @Bean(name = "rateLimit") + public boolean rateLimit() { + String appName = System.getProperty("rateLimit"); + if (appName == null) + appName = System.getenv("rateLimit"); + System.out.println("rateLimit=" + appName); + return (appName != null) ? Boolean.valueOf(appName) : false; + } + @Bean(name = "loginEnabled") public boolean loginEnabled() { String appName = System.getProperty("login.enabled"); if (appName == null) appName = System.getenv("login.enabled"); - + System.out.println("loginEnabled=" + appName); return (appName != null) ? Boolean.valueOf(appName) : false; } diff --git a/src/main/java/stirling/software/SPDF/config/UserBasedRateLimitingFilter.java b/src/main/java/stirling/software/SPDF/config/UserBasedRateLimitingFilter.java index 2695b72e..45c5feba 100644 --- a/src/main/java/stirling/software/SPDF/config/UserBasedRateLimitingFilter.java +++ b/src/main/java/stirling/software/SPDF/config/UserBasedRateLimitingFilter.java @@ -6,6 +6,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpStatus; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -30,49 +31,68 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter { @Autowired private UserDetailsService userDetailsService; + @Autowired + @Qualifier("rateLimit") + public boolean rateLimit; + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - String method = request.getMethod(); - - if (!"POST".equalsIgnoreCase(method)) { - // If the request is not a POST, just pass it through without rate limiting - filterChain.doFilter(request, response); - return; + if (!rateLimit) { + // If rateLimit is not enabled, just pass all requests without rate limiting + filterChain.doFilter(request, response); + return; + } + + String method = request.getMethod(); + + if (!"POST".equalsIgnoreCase(method)) { + // If the request is not a POST, just pass it through without rate limiting + filterChain.doFilter(request, response); + return; + } + + String identifier = null; + + // 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 + } else { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null && authentication.isAuthenticated()) { + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + identifier = userDetails.getUsername(); + } + } + + // If neither API key nor an authenticated user is present, use IP address + if (identifier == null) { + identifier = request.getRemoteAddr(); + } + + Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket()); + ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1); + + if (probe.isConsumed()) { + response.setHeader("X-Rate-Limit-Remaining", Long.toString(probe.getRemainingTokens())); + filterChain.doFilter(request, response); + } else { + long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000; + response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); + response.setHeader("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill)); + response.getWriter().write("Rate limit exceeded for POST requests."); + return; + } } - String identifier; - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - if (authentication != null && authentication.isAuthenticated()) { - UserDetails userDetails = (UserDetails) authentication.getPrincipal(); - identifier = userDetails.getUsername(); - } else { - identifier = request.getRemoteAddr(); // Use IP as identifier if not authenticated - } - - Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket()); - ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1); - - if (probe.isConsumed()) { - response.setHeader("X-Rate-Limit-Remaining", Long.toString(probe.getRemainingTokens())); - filterChain.doFilter(request, response); - } else { - long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000; - response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); - response.setHeader("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill)); - response.getWriter().write("Rate limit exceeded for POST requests."); - return; + private Bucket createUserBucket() { + Bandwidth limit = Bandwidth.classic(1000, Refill.intervally(1000, Duration.ofDays(1))); + return Bucket.builder().addLimit(limit).build(); } } -private Bucket createUserBucket() { - Bandwidth limit = Bandwidth.classic(1000, Refill.intervally(1000, Duration.ofDays(1))); - return Bucket.builder().addLimit(limit).build(); -} - -} 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 21d6d678..074316ee 100644 --- a/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java +++ b/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java @@ -23,6 +23,7 @@ public class CustomUserDetailsService implements UserDetailsService { @Autowired private UserRepository userRepository; + @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username) 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 90cc2ca0..11d63a61 100644 --- a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -5,6 +5,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -14,7 +15,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; - +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration public class SecurityConfiguration { @@ -25,14 +26,23 @@ public class SecurityConfiguration { public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } - + @Autowired + @Lazy + private UserService userService; + @Autowired @Qualifier("loginEnabled") public boolean loginEnabledValue; + @Autowired + private UserAuthenticationFilter userAuthenticationFilter; + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + if(loginEnabledValue) { + http.csrf().disable(); http .formLogin(formLogin -> formLogin @@ -74,5 +84,7 @@ public class SecurityConfiguration { return authProvider; } + + } diff --git a/src/main/java/stirling/software/SPDF/config/security/UserService.java b/src/main/java/stirling/software/SPDF/config/security/UserService.java index 15f7d48c..7dab8d67 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserService.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserService.java @@ -14,6 +14,7 @@ import java.util.HashMap; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -87,6 +88,21 @@ public class UserService { 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) + ); + } + return null; // or throw an exception + } + public boolean validateApiKeyForUser(String username, String apiKey) { Optional userOpt = userRepository.findByUsername(username); diff --git a/src/main/java/stirling/software/SPDF/controller/api/UserController.java b/src/main/java/stirling/software/SPDF/controller/api/UserController.java index 6242a458..771955e8 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/UserController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/UserController.java @@ -6,6 +6,8 @@ import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -16,6 +18,7 @@ import org.springframework.web.bind.annotation.RequestParam; import jakarta.servlet.http.HttpServletRequest; import stirling.software.SPDF.config.security.UserService; +import stirling.software.SPDF.model.User; @Controller public class UserController { @@ -68,6 +71,33 @@ public class UserController { userService.deleteUser(username); return "redirect:/addUsers"; } + + @PostMapping("/get-api-key") + public ResponseEntity getApiKey(Principal principal) { + if (principal == null) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated."); + } + String username = principal.getName(); + String apiKey = userService.getApiKeyForUser(username); + if (apiKey == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("API key not found for user."); + } + return ResponseEntity.ok(apiKey); + } + @PostMapping("/update-api-key") + public ResponseEntity updateApiKey(Principal principal) { + if (principal == null) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated."); + } + String username = principal.getName(); + User user = userService.refreshApiKeyForUser(username); + String apiKey = user.getApiKey(); + if (apiKey == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("API key not found for user."); + } + return ResponseEntity.ok(apiKey); + } + } diff --git a/src/main/java/stirling/software/SPDF/model/ApiKeyAuthenticationToken.java b/src/main/java/stirling/software/SPDF/model/ApiKeyAuthenticationToken.java new file mode 100644 index 00000000..3276ac0c --- /dev/null +++ b/src/main/java/stirling/software/SPDF/model/ApiKeyAuthenticationToken.java @@ -0,0 +1,49 @@ +package stirling.software.SPDF.model; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken { + + private final Object principal; + private Object credentials; + + public ApiKeyAuthenticationToken(String apiKey) { + super(null); + this.principal = null; + this.credentials = apiKey; + setAuthenticated(false); + } + + public ApiKeyAuthenticationToken(Object principal, String apiKey, Collection authorities) { + super(authorities); + this.principal = principal; // principal can be a UserDetails object + this.credentials = apiKey; + super.setAuthenticated(true); // this authentication is trusted + } + + @Override + public Object getCredentials() { + return credentials; + } + + @Override + public Object getPrincipal() { + return principal; + } + + @Override + public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { + if (isAuthenticated) { + throw new IllegalArgumentException("Cannot set this token to trusted. Use constructor which takes a GrantedAuthority list instead."); + } + super.setAuthenticated(false); + } + + @Override + public void eraseCredentials() { + super.eraseCredentials(); + credentials = null; + } +} diff --git a/src/main/resources/templates/account.html b/src/main/resources/templates/account.html index 932f98f3..ab8428f0 100644 --- a/src/main/resources/templates/account.html +++ b/src/main/resources/templates/account.html @@ -11,7 +11,7 @@

-
+

User Settings

@@ -58,6 +58,81 @@
+
+ +
+
+ Your API Key +
+
+
+ +
+ + +
+
+
+
+ + + +

Sync browser settings with Account