diff --git a/app/core/src/main/resources/settings.yml.template b/app/core/src/main/resources/settings.yml.template index d346be07a..3d5608bd5 100644 --- a/app/core/src/main/resources/settings.yml.template +++ b/app/core/src/main/resources/settings.yml.template @@ -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 diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/JwtService.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/JwtService.java index 7e46c9b4f..32701aa59 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/JwtService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/JwtService.java @@ -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("/") diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/security/service/JwtServiceTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/security/service/JwtServiceTest.java index 6c7e2770a..4f8726335 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/security/service/JwtServiceTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/security/service/JwtServiceTest.java @@ -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; + } }