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 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 spCert: classpath:saml-public-cert.crt # Your signing certificate. Generated from your keypair
jwt: jwt:
enableKeyStore: true # Set to 'true' to enable JWT key store persistence: true # Set to 'true' to enable JWT key store
enableKeyRotation: true # Set to 'true' to enable JWT key rotation enableKeyRotation: true # Set to 'true' to enable key pair rotation
enableKeyCleanup: true # Set to 'true' to enable JWT key cleanup enableKeyCleanup: true # Set to 'true' to enable key pair cleanup
keyRetentionDays: 7 # Number of days to retain old keys. The default is 7 days. 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. 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: premium:
key: 00000000-0000-0000-0000-000000000000 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.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseCookie;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails; 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 String ISSUER = "Stirling PDF";
private static final long EXPIRATION = 3600000; private static final long EXPIRATION = 3600000;
@Value("${stirling.security.jwt.secureCookie:true}")
private boolean secureCookie;
private final KeystoreServiceInterface keystoreService; private final KeystoreServiceInterface keystoreService;
private final boolean v2Enabled; private final boolean v2Enabled;
@ -198,7 +202,7 @@ public class JwtService implements JwtServiceInterface {
ResponseCookie cookie = ResponseCookie cookie =
ResponseCookie.from(JWT_COOKIE_NAME, Newlines.stripAll(token)) ResponseCookie.from(JWT_COOKIE_NAME, Newlines.stripAll(token))
.httpOnly(true) .httpOnly(true)
// .secure(true) // todo: fix, make configurable .secure(secureCookie)
.sameSite("Strict") .sameSite("Strict")
.maxAge(EXPIRATION / 1000) .maxAge(EXPIRATION / 1000)
.path("/") .path("/")
@ -214,7 +218,7 @@ public class JwtService implements JwtServiceInterface {
ResponseCookie cookie = ResponseCookie cookie =
ResponseCookie.from(JWT_COOKIE_NAME, "") ResponseCookie.from(JWT_COOKIE_NAME, "")
.httpOnly(true) .httpOnly(true)
.secure(true) .secure(secureCookie)
.sameSite("None") .sameSite("None")
.maxAge(0) .maxAge(0)
.path("/") .path("/")

View File

@ -11,6 +11,8 @@ import java.util.Optional;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; 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.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@ -201,19 +203,10 @@ class JwtServiceTest {
assertThrows(AuthenticationFailureException.class, () -> jwtService.extractClaims("invalid-token")); 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 @Test
void testExtractTokenWithCookie() { void testExtractTokenWithCookie() {
String token = "test-token"; String token = "test-token";
Cookie[] cookies = { new Cookie("stirling_jwt", token) }; Cookie[] cookies = { new Cookie("stirling_jwt", token) };
when(request.getHeader("Authorization")).thenReturn(null);
when(request.getCookies()).thenReturn(cookies); when(request.getCookies()).thenReturn(cookies);
assertEquals(token, jwtService.extractToken(request)); assertEquals(token, jwtService.extractToken(request));
@ -221,7 +214,6 @@ class JwtServiceTest {
@Test @Test
void testExtractTokenWithNoCookies() { void testExtractTokenWithNoCookies() {
when(request.getHeader("Authorization")).thenReturn(null);
when(request.getCookies()).thenReturn(null); when(request.getCookies()).thenReturn(null);
assertNull(jwtService.extractToken(request)); assertNull(jwtService.extractToken(request));
@ -230,7 +222,6 @@ class JwtServiceTest {
@Test @Test
void testExtractTokenWithWrongCookie() { void testExtractTokenWithWrongCookie() {
Cookie[] cookies = {new Cookie("OTHER_COOKIE", "value")}; Cookie[] cookies = {new Cookie("OTHER_COOKIE", "value")};
when(request.getHeader("Authorization")).thenReturn(null);
when(request.getCookies()).thenReturn(cookies); when(request.getCookies()).thenReturn(cookies);
assertNull(jwtService.extractToken(request)); assertNull(jwtService.extractToken(request));
@ -238,22 +229,30 @@ class JwtServiceTest {
@Test @Test
void testExtractTokenWithInvalidAuthorizationHeader() { void testExtractTokenWithInvalidAuthorizationHeader() {
when(request.getHeader("Authorization")).thenReturn("Basic token");
when(request.getCookies()).thenReturn(null); when(request.getCookies()).thenReturn(null);
assertNull(jwtService.extractToken(request)); assertNull(jwtService.extractToken(request));
} }
@Test @ParameterizedTest
void testAddToken() { @ValueSource(booleans = {true, false})
void testAddToken(boolean secureCookie) throws Exception {
String token = "test-token"; 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).setHeader("Authorization", "Bearer " + token);
verify(response).addHeader(eq("Set-Cookie"), contains("stirling_jwt=" + 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("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 @Test
@ -327,4 +326,16 @@ class JwtServiceTest {
// Verify fallback to active keypair was used (called multiple times during token operations) // Verify fallback to active keypair was used (called multiple times during token operations)
verify(keystoreService, atLeast(1)).getActiveKeyPair(); 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;
}
} }