This commit is contained in:
Dario Ghunney Ware 2025-07-31 13:16:01 +01:00
parent d197285a56
commit 71513ba762
3 changed files with 37 additions and 21 deletions

View File

@ -60,11 +60,12 @@ security:
privateKey: classpath:saml-private-key.key # Your private key. Generated from your keypair
spCert: classpath:saml-public-cert.crt # Your signing certificate. Generated from your keypair
jwt:
enableKeyStore: true # Set to 'true' to enable JWT key store
enableKeyRotation: true # Set to 'true' to enable JWT key rotation
enableKeyCleanup: true # Set to 'true' to enable JWT key cleanup
persistence: true # Set to 'true' to enable JWT key store
enableKeyRotation: true # Set to 'true' to enable key pair rotation
enableKeyCleanup: true # Set to 'true' to enable key pair cleanup
keyRetentionDays: 7 # Number of days to retain old keys. The default is 7 days.
cleanupBatchSize: 100 # Number of keys to clean up in each batch. The default is 100.
secureCookie: false # Set to 'true' to use secure cookies for JWTs
premium:
key: 00000000-0000-0000-0000-000000000000

View File

@ -10,6 +10,7 @@ import java.util.function.Function;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseCookie;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
@ -43,6 +44,9 @@ public class JwtService implements JwtServiceInterface {
private static final String ISSUER = "Stirling PDF";
private static final long EXPIRATION = 3600000;
@Value("${stirling.security.jwt.secureCookie:true}")
private boolean secureCookie;
private final KeystoreServiceInterface keystoreService;
private final boolean v2Enabled;
@ -198,7 +202,7 @@ public class JwtService implements JwtServiceInterface {
ResponseCookie cookie =
ResponseCookie.from(JWT_COOKIE_NAME, Newlines.stripAll(token))
.httpOnly(true)
// .secure(true) // todo: fix, make configurable
.secure(secureCookie)
.sameSite("Strict")
.maxAge(EXPIRATION / 1000)
.path("/")
@ -214,7 +218,7 @@ public class JwtService implements JwtServiceInterface {
ResponseCookie cookie =
ResponseCookie.from(JWT_COOKIE_NAME, "")
.httpOnly(true)
.secure(true)
.secure(secureCookie)
.sameSite("None")
.maxAge(0)
.path("/")

View File

@ -11,6 +11,8 @@ import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.core.Authentication;
@ -201,19 +203,10 @@ class JwtServiceTest {
assertThrows(AuthenticationFailureException.class, () -> jwtService.extractClaims("invalid-token"));
}
@Test
void testExtractTokenWithAuthorizationHeader() {
String token = "test-token";
when(request.getHeader("Authorization")).thenReturn("Bearer " + token);
assertEquals(token, jwtService.extractToken(request));
}
@Test
void testExtractTokenWithCookie() {
String token = "test-token";
Cookie[] cookies = { new Cookie("stirling_jwt", token) };
when(request.getHeader("Authorization")).thenReturn(null);
when(request.getCookies()).thenReturn(cookies);
assertEquals(token, jwtService.extractToken(request));
@ -221,7 +214,6 @@ class JwtServiceTest {
@Test
void testExtractTokenWithNoCookies() {
when(request.getHeader("Authorization")).thenReturn(null);
when(request.getCookies()).thenReturn(null);
assertNull(jwtService.extractToken(request));
@ -230,7 +222,6 @@ class JwtServiceTest {
@Test
void testExtractTokenWithWrongCookie() {
Cookie[] cookies = {new Cookie("OTHER_COOKIE", "value")};
when(request.getHeader("Authorization")).thenReturn(null);
when(request.getCookies()).thenReturn(cookies);
assertNull(jwtService.extractToken(request));
@ -238,22 +229,30 @@ class JwtServiceTest {
@Test
void testExtractTokenWithInvalidAuthorizationHeader() {
when(request.getHeader("Authorization")).thenReturn("Basic token");
when(request.getCookies()).thenReturn(null);
assertNull(jwtService.extractToken(request));
}
@Test
void testAddToken() {
@ParameterizedTest
@ValueSource(booleans = {true, false})
void testAddToken(boolean secureCookie) throws Exception {
String token = "test-token";
jwtService.addToken(response, token);
// Create new JwtService instance with the secureCookie parameter
JwtService testJwtService = createJwtServiceWithSecureCookie(secureCookie);
testJwtService.addToken(response, token);
verify(response).setHeader("Authorization", "Bearer " + token);
verify(response).addHeader(eq("Set-Cookie"), contains("stirling_jwt=" + token));
verify(response).addHeader(eq("Set-Cookie"), contains("HttpOnly"));
verify(response).addHeader(eq("Set-Cookie"), contains("Secure"));
if (secureCookie) {
verify(response).addHeader(eq("Set-Cookie"), contains("Secure"));
} else {
verify(response, org.mockito.Mockito.never()).addHeader(eq("Set-Cookie"), contains("Secure"));
}
}
@Test
@ -327,4 +326,16 @@ class JwtServiceTest {
// Verify fallback to active keypair was used (called multiple times during token operations)
verify(keystoreService, atLeast(1)).getActiveKeyPair();
}
private JwtService createJwtServiceWithSecureCookie(boolean secureCookie) throws Exception {
// Use reflection to create JwtService with custom secureCookie value
JwtService testService = new JwtService(true, keystoreService);
// Set the secureCookie field using reflection
java.lang.reflect.Field secureCookieField = JwtService.class.getDeclaredField("secureCookie");
secureCookieField.setAccessible(true);
secureCookieField.set(testService, secureCookie);
return testService;
}
}