mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-02 18:45:21 +00:00
Fix JWT display on account page
This commit is contained in:
parent
5ca688fc2e
commit
72d0339588
@ -24,6 +24,7 @@ public class InstallationPathConfig {
|
|||||||
private static final String STATIC_PATH;
|
private static final String STATIC_PATH;
|
||||||
private static final String TEMPLATES_PATH;
|
private static final String TEMPLATES_PATH;
|
||||||
private static final String SIGNATURES_PATH;
|
private static final String SIGNATURES_PATH;
|
||||||
|
private static final String PRIVATE_KEY_PATH;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
BASE_PATH = initializeBasePath();
|
BASE_PATH = initializeBasePath();
|
||||||
@ -43,6 +44,7 @@ public class InstallationPathConfig {
|
|||||||
STATIC_PATH = CUSTOM_FILES_PATH + "static" + File.separator;
|
STATIC_PATH = CUSTOM_FILES_PATH + "static" + File.separator;
|
||||||
TEMPLATES_PATH = CUSTOM_FILES_PATH + "templates" + File.separator;
|
TEMPLATES_PATH = CUSTOM_FILES_PATH + "templates" + File.separator;
|
||||||
SIGNATURES_PATH = CUSTOM_FILES_PATH + "signatures" + File.separator;
|
SIGNATURES_PATH = CUSTOM_FILES_PATH + "signatures" + File.separator;
|
||||||
|
PRIVATE_KEY_PATH = CUSTOM_FILES_PATH + "keys" + File.separator;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String initializeBasePath() {
|
private static String initializeBasePath() {
|
||||||
@ -114,4 +116,8 @@ public class InstallationPathConfig {
|
|||||||
public static String getSignaturesPath() {
|
public static String getSignaturesPath() {
|
||||||
return SIGNATURES_PATH;
|
return SIGNATURES_PATH;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getPrivateKeyPath() {
|
||||||
|
return PRIVATE_KEY_PATH;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -303,6 +303,9 @@ public class ApplicationProperties {
|
|||||||
public static class Jwt {
|
public static class Jwt {
|
||||||
private boolean enableKeystore = true;
|
private boolean enableKeystore = true;
|
||||||
private boolean enableKeyRotation = false;
|
private boolean enableKeyRotation = false;
|
||||||
|
private boolean enableKeyCleanup = true;
|
||||||
|
private int keyRetentionDays = 7;
|
||||||
|
private int cleanupBatchSize = 100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -390,8 +390,13 @@
|
|||||||
key.includes('clientSubmissionOrder') ||
|
key.includes('clientSubmissionOrder') ||
|
||||||
key.includes('lastSubmitTime') ||
|
key.includes('lastSubmitTime') ||
|
||||||
key.includes('lastClientId') ||
|
key.includes('lastClientId') ||
|
||||||
|
key.includes('stirling_jwt') ||
|
||||||
|
key.includes('JSESSIONID') ||
|
||||||
|
key.includes('XSRF-TOKEN') ||
|
||||||
|
key.includes('remember-me') ||
|
||||||
|
key.includes('auth') ||
|
||||||
|
key.includes('token') ||
|
||||||
|
key.includes('session') ||
|
||||||
key.includes('posthog') || key.includes('ssoRedirectAttempts') || key.includes('lastRedirectAttempt') || key.includes('surveyVersion') ||
|
key.includes('posthog') || key.includes('ssoRedirectAttempts') || key.includes('lastRedirectAttempt') || key.includes('surveyVersion') ||
|
||||||
key.includes('pageViews');
|
key.includes('pageViews');
|
||||||
}
|
}
|
||||||
|
@ -250,6 +250,8 @@ public class SecurityConfiguration {
|
|||||||
|| trimmedUri.startsWith("/css/")
|
|| trimmedUri.startsWith("/css/")
|
||||||
|| trimmedUri.startsWith("/fonts/")
|
|| trimmedUri.startsWith("/fonts/")
|
||||||
|| trimmedUri.startsWith("/js/")
|
|| trimmedUri.startsWith("/js/")
|
||||||
|
|| trimmedUri.startsWith("/pdfjs/")
|
||||||
|
|| trimmedUri.startsWith("/pdfjs-legacy/")
|
||||||
|| trimmedUri.startsWith("/favicon")
|
|| trimmedUri.startsWith("/favicon")
|
||||||
|| trimmedUri.startsWith(
|
|| trimmedUri.startsWith(
|
||||||
"/api/v1/info/status")
|
"/api/v1/info/status")
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
package stirling.software.proprietary.security.database.repository;
|
package stirling.software.proprietary.security.database.repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import stirling.software.proprietary.security.model.JwtSigningKey;
|
import stirling.software.proprietary.security.model.JwtSigningKey;
|
||||||
@ -14,5 +20,16 @@ public interface JwtSigningKeyRepository extends JpaRepository<JwtSigningKey, Lo
|
|||||||
|
|
||||||
Optional<JwtSigningKey> findByKeyId(String keyId);
|
Optional<JwtSigningKey> findByKeyId(String keyId);
|
||||||
|
|
||||||
Optional<JwtSigningKey> findByKeyIdAndIsActiveTrue(String keyId);
|
@Query(
|
||||||
|
"SELECT k FROM signing_keys k WHERE k.isActive = false AND k.createdAt < :cutoffDate ORDER BY k.createdAt ASC")
|
||||||
|
List<JwtSigningKey> findInactiveKeysOlderThan(
|
||||||
|
@Param("cutoffDate") LocalDateTime cutoffDate, Pageable pageable);
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT COUNT(k) FROM signing_keys k WHERE k.isActive = false AND k.createdAt < :cutoffDate")
|
||||||
|
long countKeysEligibleForCleanup(@Param("cutoffDate") LocalDateTime cutoffDate);
|
||||||
|
|
||||||
|
@Modifying
|
||||||
|
@Query("DELETE FROM signing_keys k WHERE k.id IN :ids")
|
||||||
|
void deleteAllByIdInBatch(@Param("ids") List<Long> ids);
|
||||||
}
|
}
|
||||||
|
@ -124,11 +124,10 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
response.getWriter()
|
response.getWriter()
|
||||||
.write(
|
.write(
|
||||||
"""
|
"""
|
||||||
Authentication required. Please provide a X-API-KEY in request\
|
Authentication required. Please provide a X-API-KEY in request header.
|
||||||
header.
|
|
||||||
This is found in Settings -> Account Settings -> API Key
|
This is found in Settings -> Account Settings -> API Key
|
||||||
Alternatively you can disable authentication if this is\
|
Alternatively you can disable authentication if this is unexpected.
|
||||||
unexpected""");
|
""");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,133 @@
|
|||||||
|
package stirling.software.proprietary.security.service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.configuration.InstallationPathConfig;
|
||||||
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
import stirling.software.proprietary.security.database.repository.JwtSigningKeyRepository;
|
||||||
|
import stirling.software.proprietary.security.model.JwtSigningKey;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class JwtKeyCleanupService {
|
||||||
|
|
||||||
|
private final JwtSigningKeyRepository signingKeyRepository;
|
||||||
|
private final JwtKeystoreService keystoreService;
|
||||||
|
private final ApplicationProperties.Security.Jwt jwtProperties;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public JwtKeyCleanupService(
|
||||||
|
JwtSigningKeyRepository signingKeyRepository,
|
||||||
|
JwtKeystoreService keystoreService,
|
||||||
|
ApplicationProperties applicationProperties) {
|
||||||
|
this.signingKeyRepository = signingKeyRepository;
|
||||||
|
this.keystoreService = keystoreService;
|
||||||
|
this.jwtProperties = applicationProperties.getSecurity().getJwt();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Scheduled(fixedDelay = 1, timeUnit = TimeUnit.MINUTES)
|
||||||
|
public void cleanup() {
|
||||||
|
if (!jwtProperties.isEnableKeyCleanup() || !keystoreService.isKeystoreEnabled()) {
|
||||||
|
log.debug("Key cleanup is disabled, skipping cleanup");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Removing inactive keys older than {} days", jwtProperties.getKeyRetentionDays());
|
||||||
|
|
||||||
|
try {
|
||||||
|
LocalDateTime cutoffDate =
|
||||||
|
LocalDateTime.now().minusDays(jwtProperties.getKeyRetentionDays());
|
||||||
|
long totalKeysEligible = signingKeyRepository.countKeysEligibleForCleanup(cutoffDate);
|
||||||
|
|
||||||
|
if (totalKeysEligible == 0) {
|
||||||
|
log.info("No keys eligible for cleanup");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("{} eligible keys found", totalKeysEligible);
|
||||||
|
|
||||||
|
batchCleanup(cutoffDate);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error during scheduled key cleanup", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void batchCleanup(LocalDateTime cutoffDate) {
|
||||||
|
int batchSize = jwtProperties.getCleanupBatchSize();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
Pageable pageable = PageRequest.of(0, batchSize);
|
||||||
|
List<JwtSigningKey> keysToCleanup =
|
||||||
|
signingKeyRepository.findInactiveKeysOlderThan(cutoffDate, pageable);
|
||||||
|
|
||||||
|
if (keysToCleanup.isEmpty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupKeyBatch(keysToCleanup);
|
||||||
|
|
||||||
|
if (keysToCleanup.size() < batchSize) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanupKeyBatch(List<JwtSigningKey> keys) {
|
||||||
|
keys.forEach(
|
||||||
|
key -> {
|
||||||
|
try {
|
||||||
|
removePrivateKey(key.getKeyId());
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to cleanup private key for keyId: {}", key.getKeyId(), e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
List<Long> keyIds = keys.stream().map(JwtSigningKey::getId).collect(Collectors.toList());
|
||||||
|
|
||||||
|
signingKeyRepository.deleteAllByIdInBatch(keyIds);
|
||||||
|
log.debug("Deleted {} signing keys from database", keyIds.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removePrivateKey(String keyId) throws IOException {
|
||||||
|
if (!keystoreService.isKeystoreEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path privateKeyDirectory = Paths.get(InstallationPathConfig.getPrivateKeyPath());
|
||||||
|
Path keyFile = privateKeyDirectory.resolve(keyId + JwtKeystoreService.KEY_SUFFIX);
|
||||||
|
|
||||||
|
if (Files.exists(keyFile)) {
|
||||||
|
Files.delete(keyFile);
|
||||||
|
log.debug("Deleted private key file: {}", keyFile);
|
||||||
|
} else {
|
||||||
|
log.debug("Private key file not found: {}", keyFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getKeysEligibleForCleanup() {
|
||||||
|
if (!jwtProperties.isEnableKeyCleanup() || !keystoreService.isKeystoreEnabled()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime cutoffDate =
|
||||||
|
LocalDateTime.now().minusDays(jwtProperties.getKeyRetentionDays());
|
||||||
|
return signingKeyRepository.countKeysEligibleForCleanup(cutoffDate);
|
||||||
|
}
|
||||||
|
}
|
@ -20,7 +20,6 @@ import java.util.Optional;
|
|||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
|
|
||||||
@ -31,14 +30,13 @@ import stirling.software.common.model.ApplicationProperties;
|
|||||||
import stirling.software.proprietary.security.database.repository.JwtSigningKeyRepository;
|
import stirling.software.proprietary.security.database.repository.JwtSigningKeyRepository;
|
||||||
import stirling.software.proprietary.security.model.JwtSigningKey;
|
import stirling.software.proprietary.security.model.JwtSigningKey;
|
||||||
|
|
||||||
@Service
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@Service
|
||||||
public class JwtKeystoreService implements JwtKeystoreServiceInterface {
|
public class JwtKeystoreService implements JwtKeystoreServiceInterface {
|
||||||
|
|
||||||
public static final String KEY_SUFFIX = ".key";
|
public static final String KEY_SUFFIX = ".key";
|
||||||
private final JwtSigningKeyRepository repository;
|
private final JwtSigningKeyRepository repository;
|
||||||
private final ApplicationProperties.Security.Jwt jwtProperties;
|
private final ApplicationProperties.Security.Jwt jwtProperties;
|
||||||
private final Path privateKeyDirectory;
|
|
||||||
|
|
||||||
private volatile KeyPair currentKeyPair;
|
private volatile KeyPair currentKeyPair;
|
||||||
private volatile String currentKeyId;
|
private volatile String currentKeyId;
|
||||||
@ -48,13 +46,12 @@ public class JwtKeystoreService implements JwtKeystoreServiceInterface {
|
|||||||
JwtSigningKeyRepository repository, ApplicationProperties applicationProperties) {
|
JwtSigningKeyRepository repository, ApplicationProperties applicationProperties) {
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
this.jwtProperties = applicationProperties.getSecurity().getJwt();
|
this.jwtProperties = applicationProperties.getSecurity().getJwt();
|
||||||
this.privateKeyDirectory = Paths.get(InstallationPathConfig.getConfigPath(), "jwt-keys");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void initializeKeystore() {
|
public void initializeKeystore() {
|
||||||
if (!isKeystoreEnabled()) {
|
if (!isKeystoreEnabled()) {
|
||||||
log.info("JWT keystore is disabled, using in-memory key generation");
|
log.info("Keystore is disabled, using in-memory key generation");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,7 +59,7 @@ public class JwtKeystoreService implements JwtKeystoreServiceInterface {
|
|||||||
ensurePrivateKeyDirectoryExists();
|
ensurePrivateKeyDirectoryExists();
|
||||||
loadOrGenerateKeypair();
|
loadOrGenerateKeypair();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to initialize JWT keystore, falling back to in-memory generation", e);
|
log.error("Failed to initialize keystore, falling back to in-memory generation", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,31 +98,6 @@ public class JwtKeystoreService implements JwtKeystoreServiceInterface {
|
|||||||
return currentKeyId;
|
return currentKeyId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public void rotateKeypair() {
|
|
||||||
if (!isKeystoreEnabled()) {
|
|
||||||
log.warn("Cannot rotate keypair when keystore is disabled");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
repository
|
|
||||||
.findByIsActiveTrue()
|
|
||||||
.ifPresent(
|
|
||||||
key -> {
|
|
||||||
key.setIsActive(false);
|
|
||||||
repository.save(key);
|
|
||||||
});
|
|
||||||
|
|
||||||
generateAndStoreKeypair();
|
|
||||||
log.info("Successfully rotated JWT keypair");
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Failed to rotate JWT keypair", e);
|
|
||||||
throw new RuntimeException("Keypair rotation failed", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isKeystoreEnabled() {
|
public boolean isKeystoreEnabled() {
|
||||||
return jwtProperties.isEnableKeystore();
|
return jwtProperties.isEnableKeystore();
|
||||||
@ -140,7 +112,7 @@ public class JwtKeystoreService implements JwtKeystoreServiceInterface {
|
|||||||
PrivateKey privateKey = loadPrivateKey(currentKeyId);
|
PrivateKey privateKey = loadPrivateKey(currentKeyId);
|
||||||
PublicKey publicKey = decodePublicKey(activeKey.get().getSigningKey());
|
PublicKey publicKey = decodePublicKey(activeKey.get().getSigningKey());
|
||||||
currentKeyPair = new KeyPair(publicKey, privateKey);
|
currentKeyPair = new KeyPair(publicKey, privateKey);
|
||||||
log.info("Loaded existing JWT keypair with keyId: {}", currentKeyId);
|
log.info("Loaded existing keypair with keyId: {}", currentKeyId);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to load existing keypair, generating new one", e);
|
log.error("Failed to load existing keypair, generating new one", e);
|
||||||
generateAndStoreKeypair();
|
generateAndStoreKeypair();
|
||||||
@ -163,7 +135,7 @@ public class JwtKeystoreService implements JwtKeystoreServiceInterface {
|
|||||||
currentKeyPair = keyPair;
|
currentKeyPair = keyPair;
|
||||||
currentKeyId = keyId;
|
currentKeyId = keyId;
|
||||||
|
|
||||||
log.info("Generated and stored new JWT keypair with keyId: {}", keyId);
|
log.info("Generated and stored new keypair with keyId: {}", keyId);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to generate and store keypair", e);
|
log.error("Failed to generate and store keypair", e);
|
||||||
throw new RuntimeException("Keypair generation failed", e);
|
throw new RuntimeException("Keypair generation failed", e);
|
||||||
@ -189,14 +161,16 @@ public class JwtKeystoreService implements JwtKeystoreServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void ensurePrivateKeyDirectoryExists() throws IOException {
|
private void ensurePrivateKeyDirectoryExists() throws IOException {
|
||||||
if (!Files.exists(privateKeyDirectory)) {
|
Path keyPath = Paths.get(InstallationPathConfig.getPrivateKeyPath());
|
||||||
Files.createDirectories(privateKeyDirectory);
|
|
||||||
log.info("Created JWT private key directory: {}", privateKeyDirectory);
|
if (!Files.exists(keyPath)) {
|
||||||
|
Files.createDirectories(keyPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void storePrivateKey(String keyId, PrivateKey privateKey) throws IOException {
|
private void storePrivateKey(String keyId, PrivateKey privateKey) throws IOException {
|
||||||
Path keyFile = privateKeyDirectory.resolve(keyId + KEY_SUFFIX);
|
Path keyFile =
|
||||||
|
Paths.get(InstallationPathConfig.getPrivateKeyPath()).resolve(keyId + KEY_SUFFIX);
|
||||||
String encodedKey = Base64.getEncoder().encodeToString(privateKey.getEncoded());
|
String encodedKey = Base64.getEncoder().encodeToString(privateKey.getEncoded());
|
||||||
Files.writeString(keyFile, encodedKey);
|
Files.writeString(keyFile, encodedKey);
|
||||||
|
|
||||||
@ -212,9 +186,11 @@ public class JwtKeystoreService implements JwtKeystoreServiceInterface {
|
|||||||
|
|
||||||
private PrivateKey loadPrivateKey(String keyId)
|
private PrivateKey loadPrivateKey(String keyId)
|
||||||
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
|
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
|
||||||
Path keyFile = privateKeyDirectory.resolve(keyId + KEY_SUFFIX);
|
Path keyFile =
|
||||||
|
Paths.get(InstallationPathConfig.getPrivateKeyPath()).resolve(keyId + KEY_SUFFIX);
|
||||||
|
|
||||||
if (!Files.exists(keyFile)) {
|
if (!Files.exists(keyFile)) {
|
||||||
throw new IOException("Private key file not found: " + keyFile);
|
throw new IOException("Private key not found: " + keyFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
String encodedKey = Files.readString(keyFile);
|
String encodedKey = Files.readString(keyFile);
|
||||||
|
@ -11,7 +11,5 @@ public interface JwtKeystoreServiceInterface {
|
|||||||
|
|
||||||
String getActiveKeyId();
|
String getActiveKeyId();
|
||||||
|
|
||||||
void rotateKeypair();
|
|
||||||
|
|
||||||
boolean isKeystoreEnabled();
|
boolean isKeystoreEnabled();
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,248 @@
|
|||||||
|
package stirling.software.proprietary.security.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
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.mockito.Mock;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
|
||||||
|
import stirling.software.common.configuration.InstallationPathConfig;
|
||||||
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
import stirling.software.proprietary.security.database.repository.JwtSigningKeyRepository;
|
||||||
|
import stirling.software.proprietary.security.model.JwtSigningKey;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class JwtKeyCleanupServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private JwtSigningKeyRepository signingKeyRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private JwtKeystoreService keystoreService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ApplicationProperties.Security security;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ApplicationProperties.Security.Jwt jwtConfig;
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
private Path tempDir;
|
||||||
|
|
||||||
|
private JwtKeyCleanupService cleanupService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
lenient().when(applicationProperties.getSecurity()).thenReturn(security);
|
||||||
|
lenient().when(security.getJwt()).thenReturn(jwtConfig);
|
||||||
|
|
||||||
|
lenient().when(jwtConfig.isEnableKeyCleanup()).thenReturn(true);
|
||||||
|
lenient().when(jwtConfig.getKeyRetentionDays()).thenReturn(7);
|
||||||
|
lenient().when(jwtConfig.getCleanupBatchSize()).thenReturn(100);
|
||||||
|
lenient().when(keystoreService.isKeystoreEnabled()).thenReturn(true);
|
||||||
|
|
||||||
|
cleanupService = new JwtKeyCleanupService(signingKeyRepository, keystoreService, applicationProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCleanupDisabled_ShouldSkip() {
|
||||||
|
when(jwtConfig.isEnableKeyCleanup()).thenReturn(false);
|
||||||
|
|
||||||
|
cleanupService.cleanup();
|
||||||
|
|
||||||
|
verify(signingKeyRepository, never()).countKeysEligibleForCleanup(any(LocalDateTime.class));
|
||||||
|
verify(signingKeyRepository, never()).findInactiveKeysOlderThan(any(LocalDateTime.class), any(Pageable.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCleanup_WhenKeystoreDisabled_ShouldSkip() {
|
||||||
|
when(keystoreService.isKeystoreEnabled()).thenReturn(false);
|
||||||
|
|
||||||
|
cleanupService.cleanup();
|
||||||
|
|
||||||
|
verify(signingKeyRepository, never()).countKeysEligibleForCleanup(any(LocalDateTime.class));
|
||||||
|
verify(signingKeyRepository, never()).findInactiveKeysOlderThan(any(LocalDateTime.class), any(Pageable.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCleanup_WhenNoKeysEligible_ShouldExitEarly() {
|
||||||
|
when(signingKeyRepository.countKeysEligibleForCleanup(any(LocalDateTime.class))).thenReturn(0L);
|
||||||
|
|
||||||
|
cleanupService.cleanup();
|
||||||
|
|
||||||
|
verify(signingKeyRepository).countKeysEligibleForCleanup(any(LocalDateTime.class));
|
||||||
|
verify(signingKeyRepository, never()).findInactiveKeysOlderThan(any(LocalDateTime.class), any(Pageable.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCleanupSuccessfully() throws IOException {
|
||||||
|
JwtSigningKey key1 = createTestKey("key-1", 1L);
|
||||||
|
JwtSigningKey key2 = createTestKey("key-2", 2L);
|
||||||
|
List<JwtSigningKey> keysToCleanup = Arrays.asList(key1, key2);
|
||||||
|
|
||||||
|
try (MockedStatic<InstallationPathConfig> mockedStatic = mockStatic(InstallationPathConfig.class)) {
|
||||||
|
mockedStatic.when(InstallationPathConfig::getPrivateKeyPath).thenReturn(tempDir.toString());
|
||||||
|
|
||||||
|
createTestKeyFile("key-1");
|
||||||
|
createTestKeyFile("key-2");
|
||||||
|
|
||||||
|
when(signingKeyRepository.countKeysEligibleForCleanup(any(LocalDateTime.class))).thenReturn(2L);
|
||||||
|
when(signingKeyRepository.findInactiveKeysOlderThan(any(LocalDateTime.class), any(Pageable.class)))
|
||||||
|
.thenReturn(keysToCleanup)
|
||||||
|
.thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
cleanupService.cleanup();
|
||||||
|
|
||||||
|
verify(signingKeyRepository).countKeysEligibleForCleanup(any(LocalDateTime.class));
|
||||||
|
verify(signingKeyRepository).findInactiveKeysOlderThan(any(LocalDateTime.class), any(Pageable.class));
|
||||||
|
verify(signingKeyRepository).deleteAllByIdInBatch(Arrays.asList(1L, 2L));
|
||||||
|
|
||||||
|
assertFalse(Files.exists(tempDir.resolve("key-1.key")));
|
||||||
|
assertFalse(Files.exists(tempDir.resolve("key-2.key")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCleanup_WithBatchProcessing_ShouldProcessMultipleBatches() throws IOException {
|
||||||
|
when(jwtConfig.getCleanupBatchSize()).thenReturn(2);
|
||||||
|
|
||||||
|
JwtSigningKey key1 = createTestKey("key-1", 1L);
|
||||||
|
JwtSigningKey key2 = createTestKey("key-2", 2L);
|
||||||
|
JwtSigningKey key3 = createTestKey("key-3", 3L);
|
||||||
|
|
||||||
|
List<JwtSigningKey> firstBatch = Arrays.asList(key1, key2);
|
||||||
|
List<JwtSigningKey> secondBatch = Arrays.asList(key3);
|
||||||
|
|
||||||
|
try (MockedStatic<InstallationPathConfig> mockedStatic = mockStatic(InstallationPathConfig.class)) {
|
||||||
|
mockedStatic.when(InstallationPathConfig::getPrivateKeyPath).thenReturn(tempDir.toString());
|
||||||
|
|
||||||
|
createTestKeyFile("key-1");
|
||||||
|
createTestKeyFile("key-2");
|
||||||
|
createTestKeyFile("key-3");
|
||||||
|
|
||||||
|
when(signingKeyRepository.countKeysEligibleForCleanup(any(LocalDateTime.class))).thenReturn(3L);
|
||||||
|
when(signingKeyRepository.findInactiveKeysOlderThan(any(LocalDateTime.class), any(Pageable.class)))
|
||||||
|
.thenReturn(firstBatch)
|
||||||
|
.thenReturn(secondBatch)
|
||||||
|
.thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
cleanupService.cleanup();
|
||||||
|
|
||||||
|
verify(signingKeyRepository, times(2)).deleteAllByIdInBatch(any());
|
||||||
|
verify(signingKeyRepository).deleteAllByIdInBatch(Arrays.asList(1L, 2L));
|
||||||
|
verify(signingKeyRepository).deleteAllByIdInBatch(Arrays.asList(3L));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCleanup() throws IOException {
|
||||||
|
JwtSigningKey key1 = createTestKey("key-1", 1L);
|
||||||
|
JwtSigningKey key2 = createTestKey("key-2", 2L);
|
||||||
|
List<JwtSigningKey> keysToCleanup = Arrays.asList(key1, key2);
|
||||||
|
|
||||||
|
try (MockedStatic<InstallationPathConfig> mockedStatic = mockStatic(InstallationPathConfig.class)) {
|
||||||
|
mockedStatic.when(InstallationPathConfig::getPrivateKeyPath).thenReturn(tempDir.toString());
|
||||||
|
|
||||||
|
createTestKeyFile("key-1");
|
||||||
|
|
||||||
|
when(signingKeyRepository.countKeysEligibleForCleanup(any(LocalDateTime.class))).thenReturn(2L);
|
||||||
|
when(signingKeyRepository.findInactiveKeysOlderThan(any(LocalDateTime.class), any(Pageable.class)))
|
||||||
|
.thenReturn(keysToCleanup)
|
||||||
|
.thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
cleanupService.cleanup();
|
||||||
|
|
||||||
|
verify(signingKeyRepository).deleteAllByIdInBatch(Arrays.asList(1L, 2L));
|
||||||
|
assertFalse(Files.exists(tempDir.resolve("key-1.key")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetKeysEligibleForCleanup() {
|
||||||
|
when(signingKeyRepository.countKeysEligibleForCleanup(any(LocalDateTime.class))).thenReturn(5L);
|
||||||
|
|
||||||
|
long result = cleanupService.getKeysEligibleForCleanup();
|
||||||
|
|
||||||
|
assertEquals(5L, result);
|
||||||
|
verify(signingKeyRepository).countKeysEligibleForCleanup(any(LocalDateTime.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnZero_WhenCleanupDisabled() {
|
||||||
|
when(jwtConfig.isEnableKeyCleanup()).thenReturn(false);
|
||||||
|
|
||||||
|
long result = cleanupService.getKeysEligibleForCleanup();
|
||||||
|
|
||||||
|
assertEquals(0L, result);
|
||||||
|
verify(signingKeyRepository, never()).countKeysEligibleForCleanup(any(LocalDateTime.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnZero_WhenKeystoreDisabled() {
|
||||||
|
when(keystoreService.isKeystoreEnabled()).thenReturn(false);
|
||||||
|
|
||||||
|
long result = cleanupService.getKeysEligibleForCleanup();
|
||||||
|
|
||||||
|
assertEquals(0L, result);
|
||||||
|
verify(signingKeyRepository, never()).countKeysEligibleForCleanup(any(LocalDateTime.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCleanup_WithRetentionDaysConfiguration_ShouldUseCorrectCutoffDate() {
|
||||||
|
when(jwtConfig.getKeyRetentionDays()).thenReturn(14);
|
||||||
|
when(signingKeyRepository.countKeysEligibleForCleanup(any(LocalDateTime.class))).thenReturn(0L);
|
||||||
|
|
||||||
|
cleanupService.cleanup();
|
||||||
|
|
||||||
|
verify(signingKeyRepository).countKeysEligibleForCleanup(argThat((LocalDateTime cutoffDate) -> {
|
||||||
|
LocalDateTime expectedCutoff = LocalDateTime.now().minusDays(14);
|
||||||
|
return Math.abs(java.time.Duration.between(cutoffDate, expectedCutoff).toMinutes()) <= 1;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCleanupPrivateKeyFile_WhenKeystoreDisabled_ShouldSkipFileRemove() throws IOException {
|
||||||
|
when(keystoreService.isKeystoreEnabled()).thenReturn(false);
|
||||||
|
|
||||||
|
cleanupService.cleanup();
|
||||||
|
|
||||||
|
verify(signingKeyRepository, never()).countKeysEligibleForCleanup(any(LocalDateTime.class));
|
||||||
|
verify(signingKeyRepository, never()).findInactiveKeysOlderThan(any(LocalDateTime.class), any(Pageable.class));
|
||||||
|
verify(signingKeyRepository, never()).deleteAllByIdInBatch(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
private JwtSigningKey createTestKey(String keyId, Long id) {
|
||||||
|
JwtSigningKey key = new JwtSigningKey();
|
||||||
|
key.setId(id);
|
||||||
|
key.setKeyId(keyId);
|
||||||
|
key.setSigningKey("test-public-key");
|
||||||
|
key.setAlgorithm("RS256");
|
||||||
|
key.setIsActive(false);
|
||||||
|
key.setCreatedAt(LocalDateTime.now().minusDays(10));
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createTestKeyFile(String keyId) throws IOException {
|
||||||
|
Path keyFile = tempDir.resolve(keyId + ".key");
|
||||||
|
Files.writeString(keyFile, "test-private-key-content");
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,5 @@
|
|||||||
package stirling.software.proprietary.security.service;
|
package stirling.software.proprietary.security.service;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.Mockito.*;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@ -12,7 +8,6 @@ import java.security.KeyPairGenerator;
|
|||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Optional;
|
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;
|
||||||
@ -22,11 +17,19 @@ import org.junit.jupiter.params.provider.ValueSource;
|
|||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.MockedStatic;
|
import org.mockito.MockedStatic;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
import stirling.software.common.configuration.InstallationPathConfig;
|
import stirling.software.common.configuration.InstallationPathConfig;
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.proprietary.security.database.repository.JwtSigningKeyRepository;
|
import stirling.software.proprietary.security.database.repository.JwtSigningKeyRepository;
|
||||||
import stirling.software.proprietary.security.model.JwtSigningKey;
|
import stirling.software.proprietary.security.model.JwtSigningKey;
|
||||||
|
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;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class JwtKeystoreServiceInterfaceTest {
|
class JwtKeystoreServiceInterfaceTest {
|
||||||
@ -55,9 +58,9 @@ class JwtKeystoreServiceInterfaceTest {
|
|||||||
keyPairGenerator.initialize(2048);
|
keyPairGenerator.initialize(2048);
|
||||||
testKeyPair = keyPairGenerator.generateKeyPair();
|
testKeyPair = keyPairGenerator.generateKeyPair();
|
||||||
|
|
||||||
when(applicationProperties.getSecurity()).thenReturn(security);
|
lenient().when(applicationProperties.getSecurity()).thenReturn(security);
|
||||||
when(security.getJwt()).thenReturn(jwtConfig);
|
lenient().when(security.getJwt()).thenReturn(jwtConfig);
|
||||||
when(jwtConfig.isEnableKeystore()).thenReturn(true);
|
lenient().when(jwtConfig.isEnableKeystore()).thenReturn(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@ -66,7 +69,7 @@ class JwtKeystoreServiceInterfaceTest {
|
|||||||
when(jwtConfig.isEnableKeystore()).thenReturn(keystoreEnabled);
|
when(jwtConfig.isEnableKeystore()).thenReturn(keystoreEnabled);
|
||||||
|
|
||||||
try (MockedStatic<InstallationPathConfig> mockedStatic = mockStatic(InstallationPathConfig.class)) {
|
try (MockedStatic<InstallationPathConfig> mockedStatic = mockStatic(InstallationPathConfig.class)) {
|
||||||
mockedStatic.when(InstallationPathConfig::getConfigPath).thenReturn(tempDir.toString());
|
mockedStatic.when(InstallationPathConfig::getPrivateKeyPath).thenReturn(tempDir.toString());
|
||||||
keystoreService = new JwtKeystoreService(repository, applicationProperties);
|
keystoreService = new JwtKeystoreService(repository, applicationProperties);
|
||||||
|
|
||||||
assertEquals(keystoreEnabled, keystoreService.isKeystoreEnabled());
|
assertEquals(keystoreEnabled, keystoreService.isKeystoreEnabled());
|
||||||
@ -78,7 +81,7 @@ class JwtKeystoreServiceInterfaceTest {
|
|||||||
when(jwtConfig.isEnableKeystore()).thenReturn(false);
|
when(jwtConfig.isEnableKeystore()).thenReturn(false);
|
||||||
|
|
||||||
try (MockedStatic<InstallationPathConfig> mockedStatic = mockStatic(InstallationPathConfig.class)) {
|
try (MockedStatic<InstallationPathConfig> mockedStatic = mockStatic(InstallationPathConfig.class)) {
|
||||||
mockedStatic.when(InstallationPathConfig::getConfigPath).thenReturn(tempDir.toString());
|
mockedStatic.when(InstallationPathConfig::getPrivateKeyPath).thenReturn(tempDir.toString());
|
||||||
keystoreService = new JwtKeystoreService(repository, applicationProperties);
|
keystoreService = new JwtKeystoreService(repository, applicationProperties);
|
||||||
|
|
||||||
KeyPair result = keystoreService.getActiveKeypair();
|
KeyPair result = keystoreService.getActiveKeypair();
|
||||||
@ -94,7 +97,7 @@ class JwtKeystoreServiceInterfaceTest {
|
|||||||
when(repository.findByIsActiveTrue()).thenReturn(Optional.empty());
|
when(repository.findByIsActiveTrue()).thenReturn(Optional.empty());
|
||||||
|
|
||||||
try (MockedStatic<InstallationPathConfig> mockedStatic = mockStatic(InstallationPathConfig.class)) {
|
try (MockedStatic<InstallationPathConfig> mockedStatic = mockStatic(InstallationPathConfig.class)) {
|
||||||
mockedStatic.when(InstallationPathConfig::getConfigPath).thenReturn(tempDir.toString());
|
mockedStatic.when(InstallationPathConfig::getPrivateKeyPath).thenReturn(tempDir.toString());
|
||||||
keystoreService = new JwtKeystoreService(repository, applicationProperties);
|
keystoreService = new JwtKeystoreService(repository, applicationProperties);
|
||||||
keystoreService.initializeKeystore();
|
keystoreService.initializeKeystore();
|
||||||
|
|
||||||
@ -114,12 +117,11 @@ class JwtKeystoreServiceInterfaceTest {
|
|||||||
JwtSigningKey existingKey = new JwtSigningKey(keyId, publicKeyBase64, "RS256");
|
JwtSigningKey existingKey = new JwtSigningKey(keyId, publicKeyBase64, "RS256");
|
||||||
when(repository.findByIsActiveTrue()).thenReturn(Optional.of(existingKey));
|
when(repository.findByIsActiveTrue()).thenReturn(Optional.of(existingKey));
|
||||||
|
|
||||||
Path keyFile = tempDir.resolve("jwt-keys").resolve(keyId + ".key");
|
Path keyFile = tempDir.resolve(keyId + ".key");
|
||||||
Files.createDirectories(keyFile.getParent());
|
|
||||||
Files.writeString(keyFile, privateKeyBase64);
|
Files.writeString(keyFile, privateKeyBase64);
|
||||||
|
|
||||||
try (MockedStatic<InstallationPathConfig> mockedStatic = mockStatic(InstallationPathConfig.class)) {
|
try (MockedStatic<InstallationPathConfig> mockedStatic = mockStatic(InstallationPathConfig.class)) {
|
||||||
mockedStatic.when(InstallationPathConfig::getConfigPath).thenReturn(tempDir.toString());
|
mockedStatic.when(InstallationPathConfig::getPrivateKeyPath).thenReturn(tempDir.toString());
|
||||||
keystoreService = new JwtKeystoreService(repository, applicationProperties);
|
keystoreService = new JwtKeystoreService(repository, applicationProperties);
|
||||||
keystoreService.initializeKeystore();
|
keystoreService.initializeKeystore();
|
||||||
|
|
||||||
@ -139,12 +141,11 @@ class JwtKeystoreServiceInterfaceTest {
|
|||||||
JwtSigningKey signingKey = new JwtSigningKey(keyId, publicKeyBase64, "RS256");
|
JwtSigningKey signingKey = new JwtSigningKey(keyId, publicKeyBase64, "RS256");
|
||||||
when(repository.findByKeyId(keyId)).thenReturn(Optional.of(signingKey));
|
when(repository.findByKeyId(keyId)).thenReturn(Optional.of(signingKey));
|
||||||
|
|
||||||
Path keyFile = tempDir.resolve("jwt-keys").resolve(keyId + ".key");
|
Path keyFile = tempDir.resolve(keyId + ".key");
|
||||||
Files.createDirectories(keyFile.getParent());
|
|
||||||
Files.writeString(keyFile, privateKeyBase64);
|
Files.writeString(keyFile, privateKeyBase64);
|
||||||
|
|
||||||
try (MockedStatic<InstallationPathConfig> mockedStatic = mockStatic(InstallationPathConfig.class)) {
|
try (MockedStatic<InstallationPathConfig> mockedStatic = mockStatic(InstallationPathConfig.class)) {
|
||||||
mockedStatic.when(InstallationPathConfig::getConfigPath).thenReturn(tempDir.toString());
|
mockedStatic.when(InstallationPathConfig::getPrivateKeyPath).thenReturn(tempDir.toString());
|
||||||
keystoreService = new JwtKeystoreService(repository, applicationProperties);
|
keystoreService = new JwtKeystoreService(repository, applicationProperties);
|
||||||
|
|
||||||
Optional<KeyPair> result = keystoreService.getKeypairByKeyId(keyId);
|
Optional<KeyPair> result = keystoreService.getKeypairByKeyId(keyId);
|
||||||
@ -161,7 +162,7 @@ class JwtKeystoreServiceInterfaceTest {
|
|||||||
when(repository.findByKeyId(keyId)).thenReturn(Optional.empty());
|
when(repository.findByKeyId(keyId)).thenReturn(Optional.empty());
|
||||||
|
|
||||||
try (MockedStatic<InstallationPathConfig> mockedStatic = mockStatic(InstallationPathConfig.class)) {
|
try (MockedStatic<InstallationPathConfig> mockedStatic = mockStatic(InstallationPathConfig.class)) {
|
||||||
mockedStatic.when(InstallationPathConfig::getConfigPath).thenReturn(tempDir.toString());
|
mockedStatic.when(InstallationPathConfig::getPrivateKeyPath).thenReturn(tempDir.toString());
|
||||||
keystoreService = new JwtKeystoreService(repository, applicationProperties);
|
keystoreService = new JwtKeystoreService(repository, applicationProperties);
|
||||||
|
|
||||||
Optional<KeyPair> result = keystoreService.getKeypairByKeyId(keyId);
|
Optional<KeyPair> result = keystoreService.getKeypairByKeyId(keyId);
|
||||||
@ -175,7 +176,7 @@ class JwtKeystoreServiceInterfaceTest {
|
|||||||
when(jwtConfig.isEnableKeystore()).thenReturn(false);
|
when(jwtConfig.isEnableKeystore()).thenReturn(false);
|
||||||
|
|
||||||
try (MockedStatic<InstallationPathConfig> mockedStatic = mockStatic(InstallationPathConfig.class)) {
|
try (MockedStatic<InstallationPathConfig> mockedStatic = mockStatic(InstallationPathConfig.class)) {
|
||||||
mockedStatic.when(InstallationPathConfig::getConfigPath).thenReturn(tempDir.toString());
|
mockedStatic.when(InstallationPathConfig::getPrivateKeyPath).thenReturn(tempDir.toString());
|
||||||
keystoreService = new JwtKeystoreService(repository, applicationProperties);
|
keystoreService = new JwtKeystoreService(repository, applicationProperties);
|
||||||
|
|
||||||
Optional<KeyPair> result = keystoreService.getKeypairByKeyId("any-key");
|
Optional<KeyPair> result = keystoreService.getKeypairByKeyId("any-key");
|
||||||
@ -184,54 +185,17 @@ class JwtKeystoreServiceInterfaceTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void testRotateKeypair() {
|
|
||||||
String oldKeyId = "old-key-123";
|
|
||||||
JwtSigningKey oldKey = new JwtSigningKey(oldKeyId, "old-public-key", "RS256");
|
|
||||||
when(repository.findByIsActiveTrue()).thenReturn(Optional.of(oldKey));
|
|
||||||
|
|
||||||
try (MockedStatic<InstallationPathConfig> mockedStatic = mockStatic(InstallationPathConfig.class)) {
|
|
||||||
mockedStatic.when(InstallationPathConfig::getConfigPath).thenReturn(tempDir.toString());
|
|
||||||
keystoreService = new JwtKeystoreService(repository, applicationProperties);
|
|
||||||
|
|
||||||
keystoreService.initializeKeystore();
|
|
||||||
|
|
||||||
keystoreService.rotateKeypair();
|
|
||||||
|
|
||||||
assertFalse(oldKey.getIsActive());
|
|
||||||
verify(repository, atLeast(2)).save(any(JwtSigningKey.class)); // At least one for deactivation, one for new key
|
|
||||||
|
|
||||||
assertNotNull(keystoreService.getActiveKeyId());
|
|
||||||
assertNotEquals(oldKeyId, keystoreService.getActiveKeyId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testRotateKeypairWhenKeystoreDisabled() {
|
|
||||||
when(jwtConfig.isEnableKeystore()).thenReturn(false);
|
|
||||||
|
|
||||||
try (MockedStatic<InstallationPathConfig> mockedStatic = mockStatic(InstallationPathConfig.class)) {
|
|
||||||
mockedStatic.when(InstallationPathConfig::getConfigPath).thenReturn(tempDir.toString());
|
|
||||||
keystoreService = new JwtKeystoreService(repository, applicationProperties);
|
|
||||||
|
|
||||||
assertDoesNotThrow(() -> keystoreService.rotateKeypair());
|
|
||||||
|
|
||||||
verify(repository, never()).save(any());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testInitializeKeystoreCreatesDirectory() throws IOException {
|
void testInitializeKeystoreCreatesDirectory() throws IOException {
|
||||||
when(repository.findByIsActiveTrue()).thenReturn(Optional.empty());
|
when(repository.findByIsActiveTrue()).thenReturn(Optional.empty());
|
||||||
|
|
||||||
try (MockedStatic<InstallationPathConfig> mockedStatic = mockStatic(InstallationPathConfig.class)) {
|
try (MockedStatic<InstallationPathConfig> mockedStatic = mockStatic(InstallationPathConfig.class)) {
|
||||||
mockedStatic.when(InstallationPathConfig::getConfigPath).thenReturn(tempDir.toString());
|
mockedStatic.when(InstallationPathConfig::getPrivateKeyPath).thenReturn(tempDir.toString());
|
||||||
keystoreService = new JwtKeystoreService(repository, applicationProperties);
|
keystoreService = new JwtKeystoreService(repository, applicationProperties);
|
||||||
keystoreService.initializeKeystore();
|
keystoreService.initializeKeystore();
|
||||||
|
|
||||||
Path jwtKeysDir = tempDir.resolve("jwt-keys");
|
assertTrue(Files.exists(tempDir));
|
||||||
assertTrue(Files.exists(jwtKeysDir));
|
assertTrue(Files.isDirectory(tempDir));
|
||||||
assertTrue(Files.isDirectory(jwtKeysDir));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,7 +208,7 @@ class JwtKeystoreServiceInterfaceTest {
|
|||||||
when(repository.findByIsActiveTrue()).thenReturn(Optional.of(existingKey));
|
when(repository.findByIsActiveTrue()).thenReturn(Optional.of(existingKey));
|
||||||
|
|
||||||
try (MockedStatic<InstallationPathConfig> mockedStatic = mockStatic(InstallationPathConfig.class)) {
|
try (MockedStatic<InstallationPathConfig> mockedStatic = mockStatic(InstallationPathConfig.class)) {
|
||||||
mockedStatic.when(InstallationPathConfig::getConfigPath).thenReturn(tempDir.toString());
|
mockedStatic.when(InstallationPathConfig::getPrivateKeyPath).thenReturn(tempDir.toString());
|
||||||
keystoreService = new JwtKeystoreService(repository, applicationProperties);
|
keystoreService = new JwtKeystoreService(repository, applicationProperties);
|
||||||
keystoreService.initializeKeystore();
|
keystoreService.initializeKeystore();
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user