IT WORKS almost

This commit is contained in:
Anthony Stirling 2023-08-13 18:19:15 +01:00
parent 7f7ea6da9f
commit cadc8e499d
8 changed files with 251 additions and 38 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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<User> userOpt = userRepository.findByUsername(username);

View file

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

View file

@ -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<? extends GrantedAuthority> 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;
}
}

View file

@ -11,7 +11,7 @@
<br> <br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="col-md-9">
<!-- User Settings Title -->
<h2 class="text-center" th:text="#{settings.accountSettings}">User Settings</h2>
@ -58,6 +58,81 @@
</div>
</form>
<hr>
<div class="card">
<div class="card-header">
Your API Key
</div>
<div class="card-body">
<div class="input-group mb-3">
<input type="password" class="form-control" id="apiKey" placeholder="Your API Key" readonly>
<div class="input-group-append">
<button class="btn btn-outline-secondary" id="showBtn" type="button" onclick="showApiKey()">👁️ Show</button>
<button class="btn btn-outline-secondary" id="refreshBtn" type="button" onclick="refreshApiKey()">🔄 Refresh</button>
</div>
</div>
</div>
</div>
<script>
function showApiKey() {
const apiKeyElement = document.getElementById("apiKey");
if (apiKeyElement.type === "password") {
apiKeyElement.type = "text";
} else {
apiKeyElement.type = "password";
}
}
document.addEventListener("DOMContentLoaded", async function() {
try {
let response = await fetch('/get-api-key', { method: 'POST' });
if (response.status === 200) {
let apiKey = await response.text();
manageUIState(apiKey);
} else {
manageUIState(null);
}
} catch (error) {
console.error('There was an error:', error);
}
});
async function refreshApiKey() {
try {
let response = await fetch('/update-api-key', { method: 'POST' });
if (response.status === 200) {
let apiKey = await response.text();
manageUIState(apiKey);
document.getElementById("apiKey").type = 'text';
} else {
alert('Error refreshing API key.');
}
} catch (error) {
console.error('There was an error:', error);
}
}
function manageUIState(apiKey) {
const apiKeyElement = document.getElementById("apiKey");
const showBtn = document.getElementById("showBtn");
if (apiKey && apiKey.trim().length > 0) {
apiKeyElement.value = apiKey;
showBtn.disabled = false;
} else {
apiKeyElement.value = "";
showBtn.disabled = true;
}
}
</script>
<hr> <!-- Separator Line -->
<h4>Sync browser settings with Account</h4>