dev #8

Closed
JoeKung wants to merge 72 commits from dev into int
6 changed files with 55 additions and 16 deletions
Showing only changes of commit c00ca5a32e - Show all commits

5
.gitignore vendored
View File

@@ -45,4 +45,7 @@ build/
./storage_orders ./storage_orders
./storage_quotes ./storage_quotes
storage_orders storage_orders
storage_quotes storage_quotes
# Qodana local reports/artifacts
backend/.qodana/

View File

@@ -1,6 +1,7 @@
package com.printcalculator.security; package com.printcalculator.security;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.Instant; import java.time.Instant;
@@ -14,6 +15,13 @@ public class AdminLoginThrottleService {
private static final long MAX_DELAY_SECONDS = 3600L; private static final long MAX_DELAY_SECONDS = 3600L;
private final ConcurrentHashMap<String, LoginAttemptState> attemptsByClient = new ConcurrentHashMap<>(); 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) { public OptionalLong getRemainingLockSeconds(String clientKey) {
LoginAttemptState state = attemptsByClient.get(clientKey); LoginAttemptState state = attemptsByClient.get(clientKey);
@@ -47,17 +55,19 @@ public class AdminLoginThrottleService {
} }
public String resolveClientKey(HttpServletRequest request) { public String resolveClientKey(HttpServletRequest request) {
String forwardedFor = request.getHeader("X-Forwarded-For"); if (trustProxyHeaders) {
if (forwardedFor != null && !forwardedFor.isBlank()) { String forwardedFor = request.getHeader("X-Forwarded-For");
String[] parts = forwardedFor.split(","); if (forwardedFor != null && !forwardedFor.isBlank()) {
if (parts.length > 0 && !parts[0].trim().isEmpty()) { String[] parts = forwardedFor.split(",");
return parts[0].trim(); if (parts.length > 0 && !parts[0].trim().isEmpty()) {
return parts[0].trim();
}
} }
}
String realIp = request.getHeader("X-Real-IP"); String realIp = request.getHeader("X-Real-IP");
if (realIp != null && !realIp.isBlank()) { if (realIp != null && !realIp.isBlank()) {
return realIp.trim(); return realIp.trim();
}
} }
String remoteAddress = request.getRemoteAddr(); String remoteAddress = request.getRemoteAddr();

View File

@@ -24,6 +24,7 @@ import java.util.UUID;
public class AdminSessionService { public class AdminSessionService {
public static final String COOKIE_NAME = "admin_session"; public static final String COOKIE_NAME = "admin_session";
private static final String COOKIE_PATH = "/api/admin";
private static final String HMAC_ALGORITHM = "HmacSHA256"; private static final String HMAC_ALGORITHM = "HmacSHA256";
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
@@ -127,20 +128,20 @@ public class AdminSessionService {
public ResponseCookie buildLoginCookie(String token) { public ResponseCookie buildLoginCookie(String token) {
return ResponseCookie.from(COOKIE_NAME, token) return ResponseCookie.from(COOKIE_NAME, token)
.path("/") .path(COOKIE_PATH)
.httpOnly(true) .httpOnly(true)
.secure(true) .secure(true)
.sameSite("Lax") .sameSite("Strict")
.maxAge(Duration.ofMinutes(sessionTtlMinutes)) .maxAge(Duration.ofMinutes(sessionTtlMinutes))
.build(); .build();
} }
public ResponseCookie buildLogoutCookie() { public ResponseCookie buildLogoutCookie() {
return ResponseCookie.from(COOKIE_NAME, "") return ResponseCookie.from(COOKIE_NAME, "")
.path("/") .path(COOKIE_PATH)
.httpOnly(true) .httpOnly(true)
.secure(true) .secure(true)
.sameSite("Lax") .sameSite("Strict")
.maxAge(Duration.ZERO) .maxAge(Duration.ZERO)
.build(); .build();
} }

View File

@@ -46,3 +46,4 @@ app.frontend.base-url=${APP_FRONTEND_BASE_URL:http://localhost:4200}
admin.password=${ADMIN_PASSWORD} admin.password=${ADMIN_PASSWORD}
admin.session.secret=${ADMIN_SESSION_SECRET} admin.session.secret=${ADMIN_SESSION_SECRET}
admin.session.ttl-minutes=${ADMIN_SESSION_TTL_MINUTES:480} admin.session.ttl-minutes=${ADMIN_SESSION_TTL_MINUTES:480}
admin.auth.trust-proxy-headers=${ADMIN_AUTH_TRUST_PROXY_HEADERS:false}

View File

@@ -58,7 +58,8 @@ class AdminAuthSecurityTest {
assertTrue(setCookie.contains("admin_session=")); assertTrue(setCookie.contains("admin_session="));
assertTrue(setCookie.contains("HttpOnly")); assertTrue(setCookie.contains("HttpOnly"));
assertTrue(setCookie.contains("Secure")); assertTrue(setCookie.contains("Secure"));
assertTrue(setCookie.contains("SameSite=Lax")); assertTrue(setCookie.contains("SameSite=Strict"));
assertTrue(setCookie.contains("Path=/api/admin"));
} }
@Test @Test

View File

@@ -1,12 +1,15 @@
package com.printcalculator.security; package com.printcalculator.security;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class AdminLoginThrottleServiceTest { class AdminLoginThrottleServiceTest {
private final AdminLoginThrottleService service = new AdminLoginThrottleService(); private final AdminLoginThrottleService service = new AdminLoginThrottleService(false);
@Test @Test
void registerFailure_ShouldDoubleDelay() { void registerFailure_ShouldDoubleDelay() {
@@ -14,4 +17,24 @@ class AdminLoginThrottleServiceTest {
assertEquals(4L, service.registerFailure("127.0.0.1")); assertEquals(4L, service.registerFailure("127.0.0.1"));
assertEquals(8L, 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));
}
} }