mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
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:
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
+38
-26
@@ -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);
|
||||
|
||||
+38
@@ -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);
|
||||
}
|
||||
}
|
||||
+242
@@ -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);
|
||||
}
|
||||
}
|
||||
+247
@@ -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));
|
||||
}
|
||||
}
|
||||
+389
@@ -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;
|
||||
}
|
||||
}
|
||||
+232
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user