JWT Auth into V2 (#4187)

Merging JWT feature from main into V2

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: dependabot[bot] <[email protected]>
Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: Ludy <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: EthanHealy01 <[email protected]>
Co-authored-by: Ethan <[email protected]>
Co-authored-by: Anthony Stirling <[email protected]>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: albanobattistella <[email protected]>
This commit is contained in:
Dario Ghunney Ware
2025-08-15 14:13:45 +01:00
committed by GitHub
co-authored by Ludy dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> EthanHealy01 Ethan Anthony Stirling stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> albanobattistella
parent 7e60eb40b4
commit 1468df3e21
115 changed files with 2993 additions and 253 deletions
@@ -14,12 +14,18 @@ import org.springframework.security.oauth2.client.authentication.OAuth2Authentic
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.common.configuration.AppConfig;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.proprietary.security.service.JwtServiceInterface;
@ExtendWith(MockitoExtension.class)
class CustomLogoutSuccessHandlerTest {
@Mock private ApplicationProperties applicationProperties;
@Mock private ApplicationProperties.Security securityProperties;
@Mock private AppConfig appConfig;
@Mock private JwtServiceInterface jwtService;
@InjectMocks private CustomLogoutSuccessHandler customLogoutSuccessHandler;
@@ -27,9 +33,12 @@ class CustomLogoutSuccessHandlerTest {
void testSuccessfulLogout() throws IOException {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
String logoutPath = "logout=true";
String token = "token";
String logoutPath = "/login?logout=true";
when(response.isCommitted()).thenReturn(false);
when(jwtService.extractToken(request)).thenReturn(token);
doNothing().when(jwtService).clearToken(response);
when(request.getContextPath()).thenReturn("");
when(response.encodeRedirectURL(logoutPath)).thenReturn(logoutPath);
@@ -38,12 +47,30 @@ class CustomLogoutSuccessHandlerTest {
verify(response).sendRedirect(logoutPath);
}
@Test
void testSuccessfulLogoutViaJWT() throws IOException {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
String logoutPath = "/login?logout=true";
String token = "token";
when(response.isCommitted()).thenReturn(false);
when(jwtService.extractToken(request)).thenReturn(token);
doNothing().when(jwtService).clearToken(response);
when(request.getContextPath()).thenReturn("");
when(response.encodeRedirectURL(logoutPath)).thenReturn(logoutPath);
customLogoutSuccessHandler.onLogoutSuccess(request, response, null);
verify(response).sendRedirect(logoutPath);
verify(jwtService).clearToken(response);
}
@Test
void testSuccessfulLogoutViaOAuth2() throws IOException {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
OAuth2AuthenticationToken oAuth2AuthenticationToken = mock(OAuth2AuthenticationToken.class);
ApplicationProperties.Security security = mock(ApplicationProperties.Security.class);
ApplicationProperties.Security.OAUTH2 oauth =
mock(ApplicationProperties.Security.OAUTH2.class);
@@ -54,8 +81,7 @@ class CustomLogoutSuccessHandlerTest {
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("");
when(applicationProperties.getSecurity()).thenReturn(security);
when(security.getOauth2()).thenReturn(oauth);
when(securityProperties.getOauth2()).thenReturn(oauth);
when(oAuth2AuthenticationToken.getAuthorizedClientRegistrationId()).thenReturn("test");
customLogoutSuccessHandler.onLogoutSuccess(request, response, oAuth2AuthenticationToken);
@@ -70,7 +96,6 @@ class CustomLogoutSuccessHandlerTest {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
ApplicationProperties.Security security = mock(ApplicationProperties.Security.class);
ApplicationProperties.Security.OAUTH2 oauth =
mock(ApplicationProperties.Security.OAUTH2.class);
@@ -84,8 +109,7 @@ class CustomLogoutSuccessHandlerTest {
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("");
when(applicationProperties.getSecurity()).thenReturn(security);
when(security.getOauth2()).thenReturn(oauth);
when(securityProperties.getOauth2()).thenReturn(oauth);
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
@@ -101,7 +125,6 @@ class CustomLogoutSuccessHandlerTest {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
ApplicationProperties.Security security = mock(ApplicationProperties.Security.class);
ApplicationProperties.Security.OAUTH2 oauth =
mock(ApplicationProperties.Security.OAUTH2.class);
@@ -111,8 +134,7 @@ class CustomLogoutSuccessHandlerTest {
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("");
when(applicationProperties.getSecurity()).thenReturn(security);
when(security.getOauth2()).thenReturn(oauth);
when(securityProperties.getOauth2()).thenReturn(oauth);
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
@@ -127,7 +149,6 @@ class CustomLogoutSuccessHandlerTest {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
ApplicationProperties.Security security = mock(ApplicationProperties.Security.class);
ApplicationProperties.Security.OAUTH2 oauth =
mock(ApplicationProperties.Security.OAUTH2.class);
@@ -138,8 +159,7 @@ class CustomLogoutSuccessHandlerTest {
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("");
when(applicationProperties.getSecurity()).thenReturn(security);
when(security.getOauth2()).thenReturn(oauth);
when(securityProperties.getOauth2()).thenReturn(oauth);
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
@@ -154,7 +174,6 @@ class CustomLogoutSuccessHandlerTest {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
ApplicationProperties.Security security = mock(ApplicationProperties.Security.class);
ApplicationProperties.Security.OAUTH2 oauth =
mock(ApplicationProperties.Security.OAUTH2.class);
@@ -167,8 +186,7 @@ class CustomLogoutSuccessHandlerTest {
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("");
when(applicationProperties.getSecurity()).thenReturn(security);
when(security.getOauth2()).thenReturn(oauth);
when(securityProperties.getOauth2()).thenReturn(oauth);
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
@@ -183,7 +201,6 @@ class CustomLogoutSuccessHandlerTest {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
ApplicationProperties.Security security = mock(ApplicationProperties.Security.class);
ApplicationProperties.Security.OAUTH2 oauth =
mock(ApplicationProperties.Security.OAUTH2.class);
@@ -198,8 +215,7 @@ class CustomLogoutSuccessHandlerTest {
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("");
when(applicationProperties.getSecurity()).thenReturn(security);
when(security.getOauth2()).thenReturn(oauth);
when(securityProperties.getOauth2()).thenReturn(oauth);
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
@@ -214,7 +230,6 @@ class CustomLogoutSuccessHandlerTest {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
ApplicationProperties.Security security = mock(ApplicationProperties.Security.class);
ApplicationProperties.Security.OAUTH2 oauth =
mock(ApplicationProperties.Security.OAUTH2.class);
@@ -230,8 +245,7 @@ class CustomLogoutSuccessHandlerTest {
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("");
when(applicationProperties.getSecurity()).thenReturn(security);
when(security.getOauth2()).thenReturn(oauth);
when(securityProperties.getOauth2()).thenReturn(oauth);
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
@@ -246,7 +260,6 @@ class CustomLogoutSuccessHandlerTest {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
ApplicationProperties.Security security = mock(ApplicationProperties.Security.class);
ApplicationProperties.Security.OAUTH2 oauth =
mock(ApplicationProperties.Security.OAUTH2.class);
@@ -259,8 +272,7 @@ class CustomLogoutSuccessHandlerTest {
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("");
when(applicationProperties.getSecurity()).thenReturn(security);
when(security.getOauth2()).thenReturn(oauth);
when(securityProperties.getOauth2()).thenReturn(oauth);
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("test");
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
@@ -0,0 +1,38 @@
package stirling.software.proprietary.security;
import static org.mockito.Mockito.*;
import java.io.IOException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.proprietary.security.model.exception.AuthenticationFailureException;
@ExtendWith(MockitoExtension.class)
class JwtAuthenticationEntryPointTest {
@Mock private HttpServletRequest request;
@Mock private HttpServletResponse response;
@Mock private AuthenticationFailureException authException;
@InjectMocks private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Test
void testCommence() throws IOException {
String errorMessage = "Authentication failed";
when(authException.getMessage()).thenReturn(errorMessage);
jwtAuthenticationEntryPoint.commence(request, response, authException);
verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED, errorMessage);
}
}
@@ -0,0 +1,242 @@
package stirling.software.proprietary.security.filter;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.AuthenticationEntryPoint;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.proprietary.security.model.exception.AuthenticationFailureException;
import stirling.software.proprietary.security.service.CustomUserDetailsService;
import stirling.software.proprietary.security.service.JwtServiceInterface;
import stirling.software.proprietary.security.service.UserService;
@Disabled
@ExtendWith(MockitoExtension.class)
class JwtAuthenticationFilterTest {
@Mock private JwtServiceInterface jwtService;
@Mock private CustomUserDetailsService userDetailsService;
@Mock private UserService userService;
@Mock private ApplicationProperties.Security securityProperties;
@Mock private HttpServletRequest request;
@Mock private HttpServletResponse response;
@Mock private FilterChain filterChain;
@Mock private UserDetails userDetails;
@Mock private SecurityContext securityContext;
@Mock private AuthenticationEntryPoint authenticationEntryPoint;
@InjectMocks private JwtAuthenticationFilter jwtAuthenticationFilter;
@Test
void shouldNotAuthenticateWhenJwtDisabled() throws ServletException, IOException {
when(jwtService.isJwtEnabled()).thenReturn(false);
jwtAuthenticationFilter.doFilterInternal(request, response, filterChain);
verify(filterChain).doFilter(request, response);
verify(jwtService, never()).extractToken(any());
}
@Test
void shouldNotFilterWhenPageIsLogin() throws ServletException, IOException {
when(jwtService.isJwtEnabled()).thenReturn(true);
when(request.getRequestURI()).thenReturn("/login");
when(request.getContextPath()).thenReturn("/login");
jwtAuthenticationFilter.doFilterInternal(request, response, filterChain);
verify(filterChain, never()).doFilter(request, response);
}
@Test
void testDoFilterInternal() throws ServletException, IOException {
String token = "valid-jwt-token";
String newToken = "new-jwt-token";
String username = "testuser";
Map<String, Object> claims = Map.of("sub", username, "authType", "WEB");
when(jwtService.isJwtEnabled()).thenReturn(true);
when(request.getContextPath()).thenReturn("/");
when(request.getRequestURI()).thenReturn("/protected");
when(jwtService.extractToken(request)).thenReturn(token);
doNothing().when(jwtService).validateToken(token);
when(jwtService.extractClaims(token)).thenReturn(claims);
when(userDetails.getAuthorities()).thenReturn(Collections.emptyList());
when(userDetailsService.loadUserByUsername(username)).thenReturn(userDetails);
try (MockedStatic<SecurityContextHolder> mockedSecurityContextHolder =
mockStatic(SecurityContextHolder.class)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
when(securityContext.getAuthentication()).thenReturn(null).thenReturn(authToken);
mockedSecurityContextHolder
.when(SecurityContextHolder::getContext)
.thenReturn(securityContext);
when(jwtService.generateToken(
any(UsernamePasswordAuthenticationToken.class), eq(claims)))
.thenReturn(newToken);
jwtAuthenticationFilter.doFilterInternal(request, response, filterChain);
verify(jwtService).validateToken(token);
verify(jwtService).extractClaims(token);
verify(userDetailsService).loadUserByUsername(username);
verify(securityContext)
.setAuthentication(any(UsernamePasswordAuthenticationToken.class));
verify(jwtService)
.generateToken(any(UsernamePasswordAuthenticationToken.class), eq(claims));
verify(jwtService).addToken(response, newToken);
verify(filterChain).doFilter(request, response);
}
}
@Test
void testDoFilterInternalWithMissingTokenForRootPath() throws ServletException, IOException {
when(jwtService.isJwtEnabled()).thenReturn(true);
when(request.getRequestURI()).thenReturn("/");
when(request.getMethod()).thenReturn("GET");
when(jwtService.extractToken(request)).thenReturn(null);
jwtAuthenticationFilter.doFilterInternal(request, response, filterChain);
verify(response).sendRedirect("/login");
verify(filterChain, never()).doFilter(request, response);
}
@Test
void validationFailsWithInvalidToken() throws ServletException, IOException {
String token = "invalid-jwt-token";
when(jwtService.isJwtEnabled()).thenReturn(true);
when(request.getRequestURI()).thenReturn("/protected");
when(request.getContextPath()).thenReturn("/");
when(jwtService.extractToken(request)).thenReturn(token);
doThrow(new AuthenticationFailureException("Invalid token"))
.when(jwtService)
.validateToken(token);
jwtAuthenticationFilter.doFilterInternal(request, response, filterChain);
verify(jwtService).validateToken(token);
verify(authenticationEntryPoint)
.commence(eq(request), eq(response), any(AuthenticationFailureException.class));
verify(filterChain, never()).doFilter(request, response);
}
@Test
void validationFailsWithExpiredToken() throws ServletException, IOException {
String token = "expired-jwt-token";
when(jwtService.isJwtEnabled()).thenReturn(true);
when(request.getRequestURI()).thenReturn("/protected");
when(request.getContextPath()).thenReturn("/");
when(jwtService.extractToken(request)).thenReturn(token);
doThrow(new AuthenticationFailureException("The token has expired"))
.when(jwtService)
.validateToken(token);
jwtAuthenticationFilter.doFilterInternal(request, response, filterChain);
verify(jwtService).validateToken(token);
verify(authenticationEntryPoint).commence(eq(request), eq(response), any());
verify(filterChain, never()).doFilter(request, response);
}
@Test
void exceptionThrown_WhenUserNotFound() throws ServletException, IOException {
String token = "valid-jwt-token";
String username = "nonexistentuser";
Map<String, Object> claims = Map.of("sub", username, "authType", "WEB");
when(jwtService.isJwtEnabled()).thenReturn(true);
when(request.getRequestURI()).thenReturn("/protected");
when(request.getContextPath()).thenReturn("/");
when(jwtService.extractToken(request)).thenReturn(token);
doNothing().when(jwtService).validateToken(token);
when(jwtService.extractClaims(token)).thenReturn(claims);
when(userDetailsService.loadUserByUsername(username)).thenReturn(null);
try (MockedStatic<SecurityContextHolder> mockedSecurityContextHolder =
mockStatic(SecurityContextHolder.class)) {
when(securityContext.getAuthentication()).thenReturn(null);
mockedSecurityContextHolder
.when(SecurityContextHolder::getContext)
.thenReturn(securityContext);
UsernameNotFoundException result =
assertThrows(
UsernameNotFoundException.class,
() ->
jwtAuthenticationFilter.doFilterInternal(
request, response, filterChain));
assertEquals("User not found: " + username, result.getMessage());
verify(userDetailsService).loadUserByUsername(username);
verify(filterChain, never()).doFilter(request, response);
}
}
@Test
void testAuthenticationEntryPointCalledWithCorrectException()
throws ServletException, IOException {
when(jwtService.isJwtEnabled()).thenReturn(true);
when(request.getRequestURI()).thenReturn("/protected");
when(request.getContextPath()).thenReturn("/");
when(jwtService.extractToken(request)).thenReturn(null);
jwtAuthenticationFilter.doFilterInternal(request, response, filterChain);
verify(authenticationEntryPoint)
.commence(
eq(request),
eq(response),
argThat(
exception ->
exception
.getMessage()
.equals("JWT is missing from the request")));
verify(filterChain, never()).doFilter(request, response);
}
}
@@ -0,0 +1,247 @@
package stirling.software.proprietary.security.saml2;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
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.NullAndEmptySource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest;
import org.springframework.security.saml2.provider.service.registration.AssertingPartyMetadata;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.proprietary.security.service.JwtServiceInterface;
@ExtendWith(MockitoExtension.class)
class JwtSaml2AuthenticationRequestRepositoryTest {
private static final String SAML_REQUEST_TOKEN = "stirling_saml_request_token";
private Map<String, String> tokenStore;
@Mock private JwtServiceInterface jwtService;
@Mock private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
private JwtSaml2AuthenticationRequestRepository jwtSaml2AuthenticationRequestRepository;
@BeforeEach
void setUp() {
tokenStore = new ConcurrentHashMap<>();
jwtSaml2AuthenticationRequestRepository =
new JwtSaml2AuthenticationRequestRepository(
tokenStore, jwtService, relyingPartyRegistrationRepository);
}
@Test
void saveAuthenticationRequest() {
var authRequest = mock(Saml2PostAuthenticationRequest.class);
var request = mock(MockHttpServletRequest.class);
var response = mock(MockHttpServletResponse.class);
String token = "testToken";
String id = "testId";
String relayState = "testRelayState";
String authnRequestUri = "example.com/authnRequest";
Map<String, Object> claims = Map.of();
String samlRequest = "testSamlRequest";
String relyingPartyRegistrationId = "stirling-pdf";
when(jwtService.isJwtEnabled()).thenReturn(true);
when(authRequest.getRelayState()).thenReturn(relayState);
when(authRequest.getId()).thenReturn(id);
when(authRequest.getAuthenticationRequestUri()).thenReturn(authnRequestUri);
when(authRequest.getSamlRequest()).thenReturn(samlRequest);
when(authRequest.getRelyingPartyRegistrationId()).thenReturn(relyingPartyRegistrationId);
when(jwtService.generateToken(eq(""), anyMap())).thenReturn(token);
jwtSaml2AuthenticationRequestRepository.saveAuthenticationRequest(
authRequest, request, response);
verify(request).setAttribute(SAML_REQUEST_TOKEN, relayState);
verify(response).addHeader(SAML_REQUEST_TOKEN, relayState);
}
@Test
void saveAuthenticationRequestWithNullRequest() {
var request = mock(MockHttpServletRequest.class);
var response = mock(MockHttpServletResponse.class);
jwtSaml2AuthenticationRequestRepository.saveAuthenticationRequest(null, request, response);
assertTrue(tokenStore.isEmpty());
}
@Test
void loadAuthenticationRequest() {
var request = mock(MockHttpServletRequest.class);
var relyingPartyRegistration = mock(RelyingPartyRegistration.class);
var assertingPartyMetadata = mock(AssertingPartyMetadata.class);
String relayState = "testRelayState";
String token = "testToken";
Map<String, Object> claims =
Map.of(
"id", "testId",
"relyingPartyRegistrationId", "stirling-pdf",
"authenticationRequestUri", "example.com/authnRequest",
"samlRequest", "testSamlRequest",
"relayState", relayState);
when(request.getParameter("RelayState")).thenReturn(relayState);
when(jwtService.extractClaims(token)).thenReturn(claims);
when(relyingPartyRegistrationRepository.findByRegistrationId("stirling-pdf"))
.thenReturn(relyingPartyRegistration);
when(relyingPartyRegistration.getRegistrationId()).thenReturn("stirling-pdf");
when(relyingPartyRegistration.getAssertingPartyMetadata())
.thenReturn(assertingPartyMetadata);
when(assertingPartyMetadata.getSingleSignOnServiceLocation())
.thenReturn("https://example.com/sso");
tokenStore.put(relayState, token);
var result = jwtSaml2AuthenticationRequestRepository.loadAuthenticationRequest(request);
assertNotNull(result);
assertFalse(tokenStore.containsKey(relayState));
}
@ParameterizedTest
@NullAndEmptySource
void loadAuthenticationRequestWithInvalidRelayState(String relayState) {
var request = mock(MockHttpServletRequest.class);
when(request.getParameter("RelayState")).thenReturn(relayState);
var result = jwtSaml2AuthenticationRequestRepository.loadAuthenticationRequest(request);
assertNull(result);
}
@Test
void loadAuthenticationRequestWithNonExistentToken() {
var request = mock(MockHttpServletRequest.class);
when(request.getParameter("RelayState")).thenReturn("nonExistentRelayState");
var result = jwtSaml2AuthenticationRequestRepository.loadAuthenticationRequest(request);
assertNull(result);
}
@Test
void loadAuthenticationRequestWithNullRelyingPartyRegistration() {
var request = mock(MockHttpServletRequest.class);
String relayState = "testRelayState";
String token = "testToken";
Map<String, Object> claims =
Map.of(
"id", "testId",
"relyingPartyRegistrationId", "stirling-pdf",
"authenticationRequestUri", "example.com/authnRequest",
"samlRequest", "testSamlRequest",
"relayState", relayState);
when(request.getParameter("RelayState")).thenReturn(relayState);
when(jwtService.extractClaims(token)).thenReturn(claims);
when(relyingPartyRegistrationRepository.findByRegistrationId("stirling-pdf"))
.thenReturn(null);
tokenStore.put(relayState, token);
var result = jwtSaml2AuthenticationRequestRepository.loadAuthenticationRequest(request);
assertNull(result);
}
@Test
void removeAuthenticationRequest() {
var request = mock(HttpServletRequest.class);
var response = mock(HttpServletResponse.class);
var relyingPartyRegistration = mock(RelyingPartyRegistration.class);
var assertingPartyMetadata = mock(AssertingPartyMetadata.class);
String relayState = "testRelayState";
String token = "testToken";
Map<String, Object> claims =
Map.of(
"id", "testId",
"relyingPartyRegistrationId", "stirling-pdf",
"authenticationRequestUri", "example.com/authnRequest",
"samlRequest", "testSamlRequest",
"relayState", relayState);
when(request.getParameter("RelayState")).thenReturn(relayState);
when(jwtService.extractClaims(token)).thenReturn(claims);
when(relyingPartyRegistrationRepository.findByRegistrationId("stirling-pdf"))
.thenReturn(relyingPartyRegistration);
when(relyingPartyRegistration.getRegistrationId()).thenReturn("stirling-pdf");
when(relyingPartyRegistration.getAssertingPartyMetadata())
.thenReturn(assertingPartyMetadata);
when(assertingPartyMetadata.getSingleSignOnServiceLocation())
.thenReturn("https://example.com/sso");
tokenStore.put(relayState, token);
var result =
jwtSaml2AuthenticationRequestRepository.removeAuthenticationRequest(
request, response);
assertNotNull(result);
assertFalse(tokenStore.containsKey(relayState));
}
@Test
void removeAuthenticationRequestWithNullRelayState() {
var request = mock(HttpServletRequest.class);
var response = mock(HttpServletResponse.class);
when(request.getParameter("RelayState")).thenReturn(null);
var result =
jwtSaml2AuthenticationRequestRepository.removeAuthenticationRequest(
request, response);
assertNull(result);
}
@Test
void removeAuthenticationRequestWithNonExistentToken() {
var request = mock(HttpServletRequest.class);
var response = mock(HttpServletResponse.class);
when(request.getParameter("RelayState")).thenReturn("nonExistentRelayState");
var result =
jwtSaml2AuthenticationRequestRepository.removeAuthenticationRequest(
request, response);
assertNull(result);
}
@Test
void removeAuthenticationRequestWithOnlyRelayState() {
var request = mock(HttpServletRequest.class);
var response = mock(HttpServletResponse.class);
String relayState = "testRelayState";
when(request.getParameter("RelayState")).thenReturn(relayState);
var result =
jwtSaml2AuthenticationRequestRepository.removeAuthenticationRequest(
request, response);
assertNull(result);
assertFalse(tokenStore.containsKey(relayState));
}
}
@@ -0,0 +1,389 @@
package stirling.software.proprietary.security.service;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
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.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.contains;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
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;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.proprietary.security.model.JwtVerificationKey;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.model.exception.AuthenticationFailureException;
@ExtendWith(MockitoExtension.class)
class JwtServiceTest {
@Mock private Authentication authentication;
@Mock private User userDetails;
@Mock private HttpServletRequest request;
@Mock private HttpServletResponse response;
@Mock private KeyPersistenceServiceInterface keystoreService;
private JwtService jwtService;
private KeyPair testKeyPair;
private JwtVerificationKey testVerificationKey;
@BeforeEach
void setUp() throws NoSuchAlgorithmException {
// Generate a test keypair
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
testKeyPair = keyPairGenerator.generateKeyPair();
// Create test verification key
String encodedPublicKey =
Base64.getEncoder().encodeToString(testKeyPair.getPublic().getEncoded());
testVerificationKey = new JwtVerificationKey("test-key-id", encodedPublicKey);
jwtService = new JwtService(true, keystoreService);
}
@Test
void testGenerateTokenWithAuthentication() throws Exception {
String username = "testuser";
when(keystoreService.getActiveKey()).thenReturn(testVerificationKey);
when(keystoreService.getKeyPair("test-key-id")).thenReturn(Optional.of(testKeyPair));
when(keystoreService.decodePublicKey(testVerificationKey.getVerifyingKey()))
.thenReturn(testKeyPair.getPublic());
when(authentication.getPrincipal()).thenReturn(userDetails);
when(userDetails.getUsername()).thenReturn(username);
String token = jwtService.generateToken(authentication, Collections.emptyMap());
assertNotNull(token);
assertFalse(token.isEmpty());
assertEquals(username, jwtService.extractUsername(token));
}
@Test
void testGenerateTokenWithUsernameAndClaims() throws Exception {
String username = "testuser";
Map<String, Object> claims = new HashMap<>();
claims.put("role", "admin");
claims.put("department", "IT");
when(keystoreService.getActiveKey()).thenReturn(testVerificationKey);
when(keystoreService.getKeyPair("test-key-id")).thenReturn(Optional.of(testKeyPair));
when(keystoreService.decodePublicKey(testVerificationKey.getVerifyingKey()))
.thenReturn(testKeyPair.getPublic());
when(authentication.getPrincipal()).thenReturn(userDetails);
when(userDetails.getUsername()).thenReturn(username);
String token = jwtService.generateToken(authentication, claims);
assertNotNull(token);
assertFalse(token.isEmpty());
assertEquals(username, jwtService.extractUsername(token));
Map<String, Object> extractedClaims = jwtService.extractClaims(token);
assertEquals("admin", extractedClaims.get("role"));
assertEquals("IT", extractedClaims.get("department"));
}
@Test
void testValidateTokenSuccess() throws Exception {
when(keystoreService.getActiveKey()).thenReturn(testVerificationKey);
when(keystoreService.getKeyPair("test-key-id")).thenReturn(Optional.of(testKeyPair));
when(keystoreService.decodePublicKey(testVerificationKey.getVerifyingKey()))
.thenReturn(testKeyPair.getPublic());
when(authentication.getPrincipal()).thenReturn(userDetails);
when(userDetails.getUsername()).thenReturn("testuser");
String token = jwtService.generateToken(authentication, new HashMap<>());
assertDoesNotThrow(() -> jwtService.validateToken(token));
}
@Test
void testValidateTokenWithInvalidToken() throws Exception {
when(keystoreService.getActiveKey()).thenReturn(testVerificationKey);
when(keystoreService.decodePublicKey(testVerificationKey.getVerifyingKey()))
.thenReturn(testKeyPair.getPublic());
assertThrows(
AuthenticationFailureException.class,
() -> {
jwtService.validateToken("invalid-token");
});
}
@Test
void testValidateTokenWithMalformedToken() throws Exception {
when(keystoreService.getActiveKey()).thenReturn(testVerificationKey);
when(keystoreService.decodePublicKey(testVerificationKey.getVerifyingKey()))
.thenReturn(testKeyPair.getPublic());
AuthenticationFailureException exception =
assertThrows(
AuthenticationFailureException.class,
() -> {
jwtService.validateToken("malformed.token");
});
assertTrue(exception.getMessage().contains("Invalid"));
}
@Test
void testValidateTokenWithEmptyToken() throws Exception {
when(keystoreService.getActiveKey()).thenReturn(testVerificationKey);
when(keystoreService.decodePublicKey(testVerificationKey.getVerifyingKey()))
.thenReturn(testKeyPair.getPublic());
AuthenticationFailureException exception =
assertThrows(
AuthenticationFailureException.class,
() -> {
jwtService.validateToken("");
});
assertTrue(
exception.getMessage().contains("Claims are empty")
|| exception.getMessage().contains("Invalid"));
}
@Test
void testExtractUsername() throws Exception {
String username = "testuser";
User user = mock(User.class);
Map<String, Object> claims = Map.of("sub", "testuser", "authType", "WEB");
when(keystoreService.getActiveKey()).thenReturn(testVerificationKey);
when(keystoreService.getKeyPair("test-key-id")).thenReturn(Optional.of(testKeyPair));
when(keystoreService.decodePublicKey(testVerificationKey.getVerifyingKey()))
.thenReturn(testKeyPair.getPublic());
when(authentication.getPrincipal()).thenReturn(user);
when(user.getUsername()).thenReturn(username);
String token = jwtService.generateToken(authentication, claims);
assertEquals(username, jwtService.extractUsername(token));
}
@Test
void testExtractUsernameWithInvalidToken() throws Exception {
when(keystoreService.getActiveKey()).thenReturn(testVerificationKey);
when(keystoreService.decodePublicKey(testVerificationKey.getVerifyingKey()))
.thenReturn(testKeyPair.getPublic());
assertThrows(
AuthenticationFailureException.class,
() -> jwtService.extractUsername("invalid-token"));
}
@Test
void testExtractClaims() throws Exception {
String username = "testuser";
Map<String, Object> claims = Map.of("role", "admin", "department", "IT");
when(keystoreService.getActiveKey()).thenReturn(testVerificationKey);
when(keystoreService.getKeyPair("test-key-id")).thenReturn(Optional.of(testKeyPair));
when(keystoreService.decodePublicKey(testVerificationKey.getVerifyingKey()))
.thenReturn(testKeyPair.getPublic());
when(authentication.getPrincipal()).thenReturn(userDetails);
when(userDetails.getUsername()).thenReturn(username);
String token = jwtService.generateToken(authentication, claims);
Map<String, Object> extractedClaims = jwtService.extractClaims(token);
assertEquals("admin", extractedClaims.get("role"));
assertEquals("IT", extractedClaims.get("department"));
assertEquals(username, extractedClaims.get("sub"));
assertEquals("Stirling PDF", extractedClaims.get("iss"));
}
@Test
void testExtractClaimsWithInvalidToken() throws Exception {
when(keystoreService.getActiveKey()).thenReturn(testVerificationKey);
when(keystoreService.decodePublicKey(testVerificationKey.getVerifyingKey()))
.thenReturn(testKeyPair.getPublic());
assertThrows(
AuthenticationFailureException.class,
() -> jwtService.extractClaims("invalid-token"));
}
@Test
void testExtractTokenWithCookie() {
String token = "test-token";
Cookie[] cookies = {new Cookie("stirling_jwt", token)};
when(request.getCookies()).thenReturn(cookies);
assertEquals(token, jwtService.extractToken(request));
}
@Test
void testExtractTokenWithNoCookies() {
when(request.getCookies()).thenReturn(null);
assertNull(jwtService.extractToken(request));
}
@Test
void testExtractTokenWithWrongCookie() {
Cookie[] cookies = {new Cookie("OTHER_COOKIE", "value")};
when(request.getCookies()).thenReturn(cookies);
assertNull(jwtService.extractToken(request));
}
@Test
void testExtractTokenWithInvalidAuthorizationHeader() {
when(request.getCookies()).thenReturn(null);
assertNull(jwtService.extractToken(request));
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
void testAddToken(boolean secureCookie) throws Exception {
String token = "test-token";
// Create new JwtService instance with the secureCookie parameter
JwtService testJwtService = createJwtServiceWithSecureCookie(secureCookie);
testJwtService.addToken(response, token);
verify(response).addHeader(eq("Set-Cookie"), contains("stirling_jwt=" + token));
verify(response).addHeader(eq("Set-Cookie"), contains("HttpOnly"));
if (secureCookie) {
verify(response).addHeader(eq("Set-Cookie"), contains("Secure"));
}
}
@Test
void testClearToken() {
jwtService.clearToken(response);
verify(response).addHeader(eq("Set-Cookie"), contains("stirling_jwt="));
verify(response).addHeader(eq("Set-Cookie"), contains("Max-Age=0"));
}
@Test
void testGenerateTokenWithKeyId() throws Exception {
String username = "testuser";
Map<String, Object> claims = new HashMap<>();
when(keystoreService.getActiveKey()).thenReturn(testVerificationKey);
when(keystoreService.getKeyPair("test-key-id")).thenReturn(Optional.of(testKeyPair));
when(authentication.getPrincipal()).thenReturn(userDetails);
when(userDetails.getUsername()).thenReturn(username);
String token = jwtService.generateToken(authentication, claims);
assertNotNull(token);
assertFalse(token.isEmpty());
// Verify that the keystore service was called
verify(keystoreService).getActiveKey();
verify(keystoreService).getKeyPair("test-key-id");
}
@Test
void testTokenVerificationWithSpecificKeyId() throws Exception {
String username = "testuser";
Map<String, Object> claims = new HashMap<>();
when(keystoreService.getActiveKey()).thenReturn(testVerificationKey);
when(keystoreService.getKeyPair("test-key-id")).thenReturn(Optional.of(testKeyPair));
when(keystoreService.decodePublicKey(testVerificationKey.getVerifyingKey()))
.thenReturn(testKeyPair.getPublic());
when(authentication.getPrincipal()).thenReturn(userDetails);
when(userDetails.getUsername()).thenReturn(username);
// Generate token with key ID
String token = jwtService.generateToken(authentication, claims);
// Mock extraction of key ID and verification (lenient to avoid unused stubbing)
lenient()
.when(keystoreService.getKeyPair("test-key-id"))
.thenReturn(Optional.of(testKeyPair));
// Verify token can be validated
assertDoesNotThrow(() -> jwtService.validateToken(token));
assertEquals(username, jwtService.extractUsername(token));
}
@Test
void testTokenVerificationFallsBackToActiveKeyWhenKeyIdNotFound() throws Exception {
String username = "testuser";
Map<String, Object> claims = new HashMap<>();
// First, generate a token successfully
when(keystoreService.getActiveKey()).thenReturn(testVerificationKey);
when(keystoreService.getKeyPair("test-key-id")).thenReturn(Optional.of(testKeyPair));
when(keystoreService.decodePublicKey(testVerificationKey.getVerifyingKey()))
.thenReturn(testKeyPair.getPublic());
when(authentication.getPrincipal()).thenReturn(userDetails);
when(userDetails.getUsername()).thenReturn(username);
String token = jwtService.generateToken(authentication, claims);
// Now mock the scenario for validation - key not found, but fallback works
// Create a fallback key pair that can be used
JwtVerificationKey fallbackKey =
new JwtVerificationKey(
"fallback-key",
Base64.getEncoder().encodeToString(testKeyPair.getPublic().getEncoded()));
// Mock the specific key lookup to fail, but the active key should work
when(keystoreService.getKeyPair("test-key-id")).thenReturn(Optional.empty());
when(keystoreService.refreshActiveKeyPair()).thenReturn(fallbackKey);
when(keystoreService.getKeyPair("fallback-key")).thenReturn(Optional.of(testKeyPair));
// Should still work by falling back to the active keypair
assertDoesNotThrow(() -> jwtService.validateToken(token));
assertEquals(username, jwtService.extractUsername(token));
// Verify fallback logic was used
verify(keystoreService, atLeast(1)).getActiveKey();
}
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;
}
}
@@ -0,0 +1,232 @@
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;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
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.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;
import org.springframework.cache.CacheManager;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import stirling.software.common.configuration.InstallationPathConfig;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.proprietary.security.model.JwtVerificationKey;
@ExtendWith(MockitoExtension.class)
class KeyPersistenceServiceInterfaceTest {
@Mock private ApplicationProperties applicationProperties;
@Mock private ApplicationProperties.Security security;
@Mock private ApplicationProperties.Security.Jwt jwtConfig;
@TempDir Path tempDir;
private KeyPersistenceService keyPersistenceService;
private KeyPair testKeyPair;
private CacheManager cacheManager;
@BeforeEach
void setUp() throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
testKeyPair = keyPairGenerator.generateKeyPair();
cacheManager = new ConcurrentMapCacheManager("verifyingKeys");
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());
}
}
@Test
void testGetActiveKeypairWhenNoActiveKeyExists() {
try (MockedStatic<InstallationPathConfig> mockedStatic =
mockStatic(InstallationPathConfig.class)) {
mockedStatic
.when(InstallationPathConfig::getPrivateKeyPath)
.thenReturn(tempDir.toString());
keyPersistenceService = new KeyPersistenceService(applicationProperties, cacheManager);
keyPersistenceService.initializeKeystore();
JwtVerificationKey result = keyPersistenceService.getActiveKey();
assertNotNull(result);
assertNotNull(result.getKeyId());
assertNotNull(result.getVerifyingKey());
}
}
@Test
void testGetActiveKeyPairWithExistingKey() throws Exception {
String keyId = "test-key-2024-01-01-120000";
String publicKeyBase64 =
Base64.getEncoder().encodeToString(testKeyPair.getPublic().getEncoded());
String privateKeyBase64 =
Base64.getEncoder().encodeToString(testKeyPair.getPrivate().getEncoded());
JwtVerificationKey existingKey = new JwtVerificationKey(keyId, publicKeyBase64);
Path keyFile = tempDir.resolve(keyId + ".key");
Files.writeString(keyFile, privateKeyBase64);
try (MockedStatic<InstallationPathConfig> mockedStatic =
mockStatic(InstallationPathConfig.class)) {
mockedStatic
.when(InstallationPathConfig::getPrivateKeyPath)
.thenReturn(tempDir.toString());
keyPersistenceService = new KeyPersistenceService(applicationProperties, cacheManager);
keyPersistenceService.initializeKeystore();
JwtVerificationKey result = keyPersistenceService.getActiveKey();
assertNotNull(result);
assertNotNull(result.getKeyId());
}
}
@Test
void testGetKeyPair() throws Exception {
String keyId = "test-key-123";
String publicKeyBase64 =
Base64.getEncoder().encodeToString(testKeyPair.getPublic().getEncoded());
String privateKeyBase64 =
Base64.getEncoder().encodeToString(testKeyPair.getPrivate().getEncoded());
JwtVerificationKey signingKey = new JwtVerificationKey(keyId, publicKeyBase64);
Path keyFile = tempDir.resolve(keyId + ".key");
Files.writeString(keyFile, privateKeyBase64);
try (MockedStatic<InstallationPathConfig> mockedStatic =
mockStatic(InstallationPathConfig.class)) {
mockedStatic
.when(InstallationPathConfig::getPrivateKeyPath)
.thenReturn(tempDir.toString());
keyPersistenceService = new KeyPersistenceService(applicationProperties, cacheManager);
keyPersistenceService
.getClass()
.getDeclaredField("verifyingKeyCache")
.setAccessible(true);
var cache = cacheManager.getCache("verifyingKeys");
cache.put(keyId, signingKey);
Optional<KeyPair> result = keyPersistenceService.getKeyPair(keyId);
assertTrue(result.isPresent());
assertNotNull(result.get().getPublic());
assertNotNull(result.get().getPrivate());
}
}
@Test
void testGetKeyPairNotFound() {
String keyId = "non-existent-key";
try (MockedStatic<InstallationPathConfig> mockedStatic =
mockStatic(InstallationPathConfig.class)) {
mockedStatic
.when(InstallationPathConfig::getPrivateKeyPath)
.thenReturn(tempDir.toString());
keyPersistenceService = new KeyPersistenceService(applicationProperties, cacheManager);
Optional<KeyPair> result = keyPersistenceService.getKeyPair(keyId);
assertFalse(result.isPresent());
}
}
@Test
void testGetKeyPairWhenKeystoreDisabled() {
when(jwtConfig.isEnableKeystore()).thenReturn(false);
try (MockedStatic<InstallationPathConfig> mockedStatic =
mockStatic(InstallationPathConfig.class)) {
mockedStatic
.when(InstallationPathConfig::getPrivateKeyPath)
.thenReturn(tempDir.toString());
keyPersistenceService = new KeyPersistenceService(applicationProperties, cacheManager);
Optional<KeyPair> result = keyPersistenceService.getKeyPair("any-key");
assertFalse(result.isPresent());
}
}
@Test
void testInitializeKeystoreCreatesDirectory() throws IOException {
try (MockedStatic<InstallationPathConfig> mockedStatic =
mockStatic(InstallationPathConfig.class)) {
mockedStatic
.when(InstallationPathConfig::getPrivateKeyPath)
.thenReturn(tempDir.toString());
keyPersistenceService = new KeyPersistenceService(applicationProperties, cacheManager);
keyPersistenceService.initializeKeystore();
assertTrue(Files.exists(tempDir));
assertTrue(Files.isDirectory(tempDir));
}
}
@Test
void testLoadExistingKeypairWithMissingPrivateKeyFile() throws Exception {
String keyId = "test-key-missing-file";
String publicKeyBase64 =
Base64.getEncoder().encodeToString(testKeyPair.getPublic().getEncoded());
JwtVerificationKey existingKey = new JwtVerificationKey(keyId, publicKeyBase64);
try (MockedStatic<InstallationPathConfig> mockedStatic =
mockStatic(InstallationPathConfig.class)) {
mockedStatic
.when(InstallationPathConfig::getPrivateKeyPath)
.thenReturn(tempDir.toString());
keyPersistenceService = new KeyPersistenceService(applicationProperties, cacheManager);
keyPersistenceService.initializeKeystore();
JwtVerificationKey result = keyPersistenceService.getActiveKey();
assertNotNull(result);
assertNotNull(result.getKeyId());
assertNotNull(result.getVerifyingKey());
}
}
}