Defaulting JWT settings to false (#4416)

Defaulting the configuration settings for Stirling PDF's JWT to false to
avoid any unexpected issues
This commit is contained in:
Dario Ghunney Ware 2025-09-30 12:02:11 +01:00 committed by GitHub
parent 7bd31a954e
commit dabc52ef73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 111 additions and 107 deletions

View File

@ -303,11 +303,10 @@ public class ApplicationProperties {
@Data
public static class Jwt {
private boolean enableKeystore = true;
private boolean enableKeyRotation = false;
private boolean enableKeyCleanup = true;
private boolean enabled = true;
private boolean keyCleanup = true;
private int keyRetentionDays = 7;
private boolean secureCookie;
private Boolean secureCookie;
}
}
@ -377,16 +376,19 @@ public class ApplicationProperties {
@JsonIgnore
public String getBaseTmpDir() {
return baseTmpDir != null && !baseTmpDir.isEmpty()
? baseTmpDir
: java.lang.System.getProperty("java.io.tmpdir") + "/stirling-pdf";
if (baseTmpDir != null && !baseTmpDir.isEmpty()) {
return baseTmpDir;
}
String tmp = java.lang.System.getProperty("java.io.tmpdir");
return new File(tmp, "stirling-pdf").getPath();
}
@JsonIgnore
public String getLibreofficeDir() {
return libreofficeDir != null && !libreofficeDir.isEmpty()
? libreofficeDir
: getBaseTmpDir() + "/libreoffice";
if (libreofficeDir != null && !libreofficeDir.isEmpty()) {
return libreofficeDir;
}
return new File(getBaseTmpDir(), "libreoffice").getPath();
}
}

View File

@ -636,7 +636,7 @@ public class PdfUtils {
case "equal" -> actualPageCount == pageCount;
case "less" -> actualPageCount < pageCount;
default ->
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
};
}
@ -659,15 +659,9 @@ public class PdfUtils {
return actualPageWidth == expectedPageWidth && actualPageHeight == expectedPageHeight;
}
/**
* Key for storing the dimensions of a rendered image in a map.
*/
private record PdfRenderSettingsKey(float mediaBoxWidth, float mediaBoxHeight, int rotation) {
}
/** Key for storing the dimensions of a rendered image in a map. */
private record PdfRenderSettingsKey(float mediaBoxWidth, float mediaBoxHeight, int rotation) {}
/**
* Value for storing the dimensions of a rendered image in a map.
*/
private record PdfImageDimensionValue(int width, int height) {
}
/** Value for storing the dimensions of a rendered image in a map. */
private record PdfImageDimensionValue(int width, int height) {}
}

View File

@ -14,7 +14,6 @@ import stirling.software.common.model.ApplicationProperties.Driver;
import stirling.software.common.model.ApplicationProperties.Premium;
import stirling.software.common.model.ApplicationProperties.Security;
import stirling.software.common.model.exception.UnsupportedProviderException;
import stirling.software.common.util.RegexPatternUtils;
class ApplicationPropertiesLogicTest {
@ -39,15 +38,12 @@ class ApplicationPropertiesLogicTest {
new ApplicationProperties.TempFileManagement();
String expectedBase =
RegexPatternUtils.getInstance()
.getTrailingSlashesPattern()
.matcher(java.lang.System.getProperty("java.io.tmpdir"))
.replaceAll("")
+ "/stirling-pdf";
assertEquals(expectedBase, normalize.apply(tfm.getBaseTmpDir()));
Paths.get(java.lang.System.getProperty("java.io.tmpdir"), "stirling-pdf")
.toString();
assertEquals(expectedBase, tfm.getBaseTmpDir());
String expectedLibre = expectedBase + "/libreoffice";
assertEquals(expectedLibre, normalize.apply(tfm.getLibreofficeDir()));
String expectedLibre = Paths.get(expectedBase, "libreoffice").toString();
assertEquals(expectedLibre, tfm.getLibreofficeDir());
tfm.setBaseTmpDir("/custom/base");
assertEquals("/custom/base", normalize.apply(tfm.getBaseTmpDir()));

View File

@ -227,7 +227,9 @@ public class LoadingWindow extends JDialog {
if (!existingPids
.contains(
pid)) {
log.debug("Found new explorer.exe with PID: {}", pid);
log.debug(
"Found new explorer.exe with PID: {}",
pid);
ProcessBuilder
killProcess =
new ProcessBuilder(
@ -245,7 +247,9 @@ public class LoadingWindow extends JDialog {
2,
TimeUnit
.SECONDS);
log.debug("Explorer process terminated: {}", pid);
log.debug(
"Explorer process terminated: {}",
pid);
}
}
}

View File

@ -87,13 +87,15 @@ public class FilterController {
PDDocument document = pdfDocumentFactory.load(inputFile);
int actualPageCount = document.getNumberOfPages();
// Perform the comparison
boolean valid = switch (comparator) {
case "Greater" -> actualPageCount > pageCount;
case "Equal" -> actualPageCount == pageCount;
case "Less" -> actualPageCount < pageCount;
default ->
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
};
boolean valid =
switch (comparator) {
case "Greater" -> actualPageCount > pageCount;
case "Equal" -> actualPageCount == pageCount;
case "Less" -> actualPageCount < pageCount;
default ->
throw ExceptionUtils.createInvalidArgumentException(
"comparator", comparator);
};
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
return null;
@ -123,13 +125,15 @@ public class FilterController {
float standardArea = standardSize.getWidth() * standardSize.getHeight();
// Perform the comparison
boolean valid = switch (comparator) {
case "Greater" -> actualArea > standardArea;
case "Equal" -> actualArea == standardArea;
case "Less" -> actualArea < standardArea;
default ->
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
};
boolean valid =
switch (comparator) {
case "Greater" -> actualArea > standardArea;
case "Equal" -> actualArea == standardArea;
case "Less" -> actualArea < standardArea;
default ->
throw ExceptionUtils.createInvalidArgumentException(
"comparator", comparator);
};
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
return null;
@ -149,13 +153,15 @@ public class FilterController {
long actualFileSize = inputFile.getSize();
// Perform the comparison
boolean valid = switch (comparator) {
case "Greater" -> actualFileSize > fileSize;
case "Equal" -> actualFileSize == fileSize;
case "Less" -> actualFileSize < fileSize;
default ->
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
};
boolean valid =
switch (comparator) {
case "Greater" -> actualFileSize > fileSize;
case "Equal" -> actualFileSize == fileSize;
case "Less" -> actualFileSize < fileSize;
default ->
throw ExceptionUtils.createInvalidArgumentException(
"comparator", comparator);
};
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
return null;
@ -179,13 +185,15 @@ public class FilterController {
int actualRotation = firstPage.getRotation();
// Perform the comparison
boolean valid = switch (comparator) {
case "Greater" -> actualRotation > rotation;
case "Equal" -> actualRotation == rotation;
case "Less" -> actualRotation < rotation;
default ->
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
};
boolean valid =
switch (comparator) {
case "Greater" -> actualRotation > rotation;
case "Equal" -> actualRotation == rotation;
case "Less" -> actualRotation < rotation;
default ->
throw ExceptionUtils.createInvalidArgumentException(
"comparator", comparator);
};
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
return null;

View File

@ -23,9 +23,9 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.MetadataRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.service.PdfMetadataService;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.RegexPatternUtils;
import stirling.software.common.service.PdfMetadataService;
import stirling.software.common.util.WebResponseUtils;
import stirling.software.common.util.propertyeditor.StringToMapPropertyEditor;

View File

@ -170,14 +170,15 @@ public class WatermarkController {
throws IOException {
String resourceDir = "";
PDFont font = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
resourceDir = switch (alphabet) {
case "arabic" -> "static/fonts/NotoSansArabic-Regular.ttf";
case "japanese" -> "static/fonts/Meiryo.ttf";
case "korean" -> "static/fonts/malgun.ttf";
case "chinese" -> "static/fonts/SimSun.ttf";
case "thai" -> "static/fonts/NotoSansThai-Regular.ttf";
default -> "static/fonts/NotoSans-Regular.ttf";
};
resourceDir =
switch (alphabet) {
case "arabic" -> "static/fonts/NotoSansArabic-Regular.ttf";
case "japanese" -> "static/fonts/Meiryo.ttf";
case "korean" -> "static/fonts/malgun.ttf";
case "chinese" -> "static/fonts/SimSun.ttf";
case "thai" -> "static/fonts/NotoSansThai-Regular.ttf";
default -> "static/fonts/NotoSans-Regular.ttf";
};
ClassPathResource classPathResource = new ClassPathResource(resourceDir);
String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));

View File

@ -288,8 +288,8 @@ public class GeneralWebController {
case "eot" -> "embedded-opentype";
case "svg" -> "svg";
default ->
// or throw an exception if an unexpected extension is encountered
"";
// or throw an exception if an unexpected extension is encountered
"";
};
}

View File

@ -60,11 +60,10 @@ 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: # This feature is currently under development and not yet fully supported. Do not use in production.
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
enabled: false # Set to 'true' to enable JWT key store
keyCleanup: false # Set to 'true' to enable key pair cleanup
keyRetentionDays: 7 # Number of days to retain old keys. The default is 7 days.
secureCookie: false # Set to 'true' to use secure cookies for JWTs
secureCookie: true # Set to 'true' to use secure cookies for JWTs
premium:
key: 00000000-0000-0000-0000-000000000000

View File

@ -5,6 +5,7 @@ import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
@ -26,6 +27,9 @@ import stirling.software.proprietary.security.service.UserService;
@RequiredArgsConstructor
public class InitialSecuritySetup {
@Value("${v2:false}")
private boolean v2Enabled = false;
private final UserService userService;
private final TeamService teamService;
private final ApplicationProperties applicationProperties;
@ -43,6 +47,7 @@ public class InitialSecuritySetup {
}
}
configureJWTSettings();
assignUsersToDefaultTeamIfMissing();
initializeInternalApiUser();
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
@ -51,6 +56,18 @@ public class InitialSecuritySetup {
}
}
private void configureJWTSettings() {
ApplicationProperties.Security.Jwt jwtProperties =
applicationProperties.getSecurity().getJwt();
boolean jwtEnabled = jwtProperties.isEnabled();
if (!v2Enabled || !jwtEnabled) {
log.debug("V2 enabled: {}, JWT enabled: {} - disabling all JWT features", v2Enabled, jwtEnabled);
jwtProperties.setKeyCleanup(false);
}
}
private void assignUsersToDefaultTeamIfMissing() {
Team defaultTeam = teamService.getOrCreateDefaultTeam();
Team internalTeam = teamService.getOrCreateInternalTeam();

View File

@ -40,7 +40,7 @@ public class KeyPairCleanupService {
@PostConstruct
@Scheduled(fixedDelay = 1, timeUnit = TimeUnit.DAYS)
public void cleanup() {
if (!jwtProperties.isEnableKeyCleanup() || !keyPersistenceService.isKeystoreEnabled()) {
if (!jwtProperties.isEnabled() || !jwtProperties.isKeyCleanup()) {
return;
}
@ -71,7 +71,7 @@ public class KeyPairCleanupService {
}
private void removePrivateKey(String keyId) throws IOException {
if (!keyPersistenceService.isKeystoreEnabled()) {
if (!jwtProperties.isEnabled()) {
return;
}

View File

@ -84,7 +84,7 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
@PostConstruct
public void initializeKeystore() {
if (!isKeystoreEnabled()) {
if (!jwtProperties.isEnabled()) {
return;
}
@ -132,7 +132,7 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
@Override
public Optional<KeyPair> getKeyPair(String keyId) {
if (!isKeystoreEnabled()) {
if (!jwtProperties.isEnabled()) {
return Optional.empty();
}
@ -155,11 +155,6 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
}
}
@Override
public boolean isKeystoreEnabled() {
return jwtProperties.isEnableKeystore();
}
@Override
public JwtVerificationKey refreshActiveKeyPair() {
return generateAndStoreKeypair();
@ -268,4 +263,8 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(keySpec);
}
public boolean isKeystoreEnabled() {
return jwtProperties.isEnabled();
}
}

View File

@ -16,8 +16,6 @@ public interface KeyPersistenceServiceInterface {
Optional<KeyPair> getKeyPair(String keyId);
boolean isKeystoreEnabled();
JwtVerificationKey refreshActiveKeyPair();
List<JwtVerificationKey> getKeysEligibleForCleanup(LocalDateTime cutoffDate);

View File

@ -1,6 +1,5 @@
package stirling.software.proprietary.security.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -21,8 +20,6 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
@ -58,23 +55,7 @@ class KeyPersistenceServiceInterfaceTest {
lenient().when(applicationProperties.getSecurity()).thenReturn(security);
lenient().when(security.getJwt()).thenReturn(jwtConfig);
lenient().when(jwtConfig.isEnableKeystore()).thenReturn(true); // Default value
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
void testKeystoreEnabled(boolean keystoreEnabled) {
when(jwtConfig.isEnableKeystore()).thenReturn(keystoreEnabled);
try (MockedStatic<InstallationPathConfig> mockedStatic =
mockStatic(InstallationPathConfig.class)) {
mockedStatic
.when(InstallationPathConfig::getPrivateKeyPath)
.thenReturn(tempDir.toString());
keyPersistenceService = new KeyPersistenceService(applicationProperties, cacheManager);
assertEquals(keystoreEnabled, keyPersistenceService.isKeystoreEnabled());
}
lenient().when(jwtConfig.isEnabled()).thenReturn(true);
}
@Test
@ -177,7 +158,7 @@ class KeyPersistenceServiceInterfaceTest {
@Test
void testGetKeyPairWhenKeystoreDisabled() {
when(jwtConfig.isEnableKeystore()).thenReturn(false);
when(jwtConfig.isEnabled()).thenReturn(false);
try (MockedStatic<InstallationPathConfig> mockedStatic =
mockStatic(InstallationPathConfig.class)) {

View File

@ -59,6 +59,11 @@ security:
idpCert: classpath:okta.cert # The certificate your Provider will use to authenticate your app's SAML authentication requests. Provided by your Provider
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: # This feature is currently under development and not yet fully supported. Do not use in production.
enabled: true # Set to 'true' to enable JWT key store
keyCleanup: true # Set to 'true' to enable key pair cleanup
keyRetentionDays: 7 # Number of days to retain old keys. The default is 7 days.
secureCookie: false # Set to 'true' to use secure cookies for JWTs
premium:
key: 00000000-0000-0000-0000-000000000000