dev #8
5
.gitignore
vendored
5
.gitignore
vendored
@@ -45,4 +45,7 @@ build/
|
||||
./storage_orders
|
||||
./storage_quotes
|
||||
storage_orders
|
||||
storage_quotes
|
||||
storage_quotes
|
||||
|
||||
# Qodana local reports/artifacts
|
||||
backend/.qodana/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.printcalculator.security;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
@@ -14,6 +15,13 @@ public class AdminLoginThrottleService {
|
||||
private static final long MAX_DELAY_SECONDS = 3600L;
|
||||
|
||||
private final ConcurrentHashMap<String, LoginAttemptState> attemptsByClient = new ConcurrentHashMap<>();
|
||||
private final boolean trustProxyHeaders;
|
||||
|
||||
public AdminLoginThrottleService(
|
||||
@Value("${admin.auth.trust-proxy-headers:false}") boolean trustProxyHeaders
|
||||
) {
|
||||
this.trustProxyHeaders = trustProxyHeaders;
|
||||
}
|
||||
|
||||
public OptionalLong getRemainingLockSeconds(String clientKey) {
|
||||
LoginAttemptState state = attemptsByClient.get(clientKey);
|
||||
@@ -47,17 +55,19 @@ public class AdminLoginThrottleService {
|
||||
}
|
||||
|
||||
public String resolveClientKey(HttpServletRequest request) {
|
||||
String forwardedFor = request.getHeader("X-Forwarded-For");
|
||||
if (forwardedFor != null && !forwardedFor.isBlank()) {
|
||||
String[] parts = forwardedFor.split(",");
|
||||
if (parts.length > 0 && !parts[0].trim().isEmpty()) {
|
||||
return parts[0].trim();
|
||||
if (trustProxyHeaders) {
|
||||
String forwardedFor = request.getHeader("X-Forwarded-For");
|
||||
if (forwardedFor != null && !forwardedFor.isBlank()) {
|
||||
String[] parts = forwardedFor.split(",");
|
||||
if (parts.length > 0 && !parts[0].trim().isEmpty()) {
|
||||
return parts[0].trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String realIp = request.getHeader("X-Real-IP");
|
||||
if (realIp != null && !realIp.isBlank()) {
|
||||
return realIp.trim();
|
||||
String realIp = request.getHeader("X-Real-IP");
|
||||
if (realIp != null && !realIp.isBlank()) {
|
||||
return realIp.trim();
|
||||
}
|
||||
}
|
||||
|
||||
String remoteAddress = request.getRemoteAddr();
|
||||
|
||||
@@ -24,6 +24,7 @@ import java.util.UUID;
|
||||
public class AdminSessionService {
|
||||
|
||||
public static final String COOKIE_NAME = "admin_session";
|
||||
private static final String COOKIE_PATH = "/api/admin";
|
||||
private static final String HMAC_ALGORITHM = "HmacSHA256";
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
@@ -127,20 +128,20 @@ public class AdminSessionService {
|
||||
|
||||
public ResponseCookie buildLoginCookie(String token) {
|
||||
return ResponseCookie.from(COOKIE_NAME, token)
|
||||
.path("/")
|
||||
.path(COOKIE_PATH)
|
||||
.httpOnly(true)
|
||||
.secure(true)
|
||||
.sameSite("Lax")
|
||||
.sameSite("Strict")
|
||||
.maxAge(Duration.ofMinutes(sessionTtlMinutes))
|
||||
.build();
|
||||
}
|
||||
|
||||
public ResponseCookie buildLogoutCookie() {
|
||||
return ResponseCookie.from(COOKIE_NAME, "")
|
||||
.path("/")
|
||||
.path(COOKIE_PATH)
|
||||
.httpOnly(true)
|
||||
.secure(true)
|
||||
.sameSite("Lax")
|
||||
.sameSite("Strict")
|
||||
.maxAge(Duration.ZERO)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -46,3 +46,4 @@ app.frontend.base-url=${APP_FRONTEND_BASE_URL:http://localhost:4200}
|
||||
admin.password=${ADMIN_PASSWORD}
|
||||
admin.session.secret=${ADMIN_SESSION_SECRET}
|
||||
admin.session.ttl-minutes=${ADMIN_SESSION_TTL_MINUTES:480}
|
||||
admin.auth.trust-proxy-headers=${ADMIN_AUTH_TRUST_PROXY_HEADERS:false}
|
||||
|
||||
@@ -58,7 +58,8 @@ class AdminAuthSecurityTest {
|
||||
assertTrue(setCookie.contains("admin_session="));
|
||||
assertTrue(setCookie.contains("HttpOnly"));
|
||||
assertTrue(setCookie.contains("Secure"));
|
||||
assertTrue(setCookie.contains("SameSite=Lax"));
|
||||
assertTrue(setCookie.contains("SameSite=Strict"));
|
||||
assertTrue(setCookie.contains("Path=/api/admin"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package com.printcalculator.security;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class AdminLoginThrottleServiceTest {
|
||||
|
||||
private final AdminLoginThrottleService service = new AdminLoginThrottleService();
|
||||
private final AdminLoginThrottleService service = new AdminLoginThrottleService(false);
|
||||
|
||||
@Test
|
||||
void registerFailure_ShouldDoubleDelay() {
|
||||
@@ -14,4 +17,24 @@ class AdminLoginThrottleServiceTest {
|
||||
assertEquals(4L, service.registerFailure("127.0.0.1"));
|
||||
assertEquals(8L, service.registerFailure("127.0.0.1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveClientKey_ShouldUseRemoteAddress_WhenProxyHeadersAreNotTrusted() {
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
when(request.getHeader("X-Forwarded-For")).thenReturn("203.0.113.10");
|
||||
when(request.getHeader("X-Real-IP")).thenReturn("203.0.113.11");
|
||||
when(request.getRemoteAddr()).thenReturn("10.0.0.5");
|
||||
|
||||
assertEquals("10.0.0.5", service.resolveClientKey(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveClientKey_ShouldUseForwardedFor_WhenProxyHeadersAreTrusted() {
|
||||
AdminLoginThrottleService trustedService = new AdminLoginThrottleService(true);
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
when(request.getHeader("X-Forwarded-For")).thenReturn("203.0.113.10, 10.0.0.5");
|
||||
when(request.getRemoteAddr()).thenReturn("10.0.0.5");
|
||||
|
||||
assertEquals("203.0.113.10", trustedService.resolveClientKey(request));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user