dev #8
5
.gitignore
vendored
5
.gitignore
vendored
@@ -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/
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user