Fixed blank login page when v2 is disabled

This commit is contained in:
Dario Ghunney Ware 2025-07-22 15:03:31 +01:00
parent 13fa7475aa
commit 622a0f91aa
5 changed files with 320 additions and 5 deletions

View File

@ -50,4 +50,4 @@ spring.main.allow-bean-definition-overriding=true
java.io.tmpdir=${stirling.tempfiles.directory:${java.io.tmpdir}/stirling-pdf}
# V2 features
v2=true
v2=false

View File

@ -78,6 +78,9 @@ public class CustomAuthenticationSuccessHandler
request.getContextPath(), savedRequest.getRedirectUrl())) {
// Redirect to the original destination
super.onAuthenticationSuccess(request, response, authentication);
} else {
// No saved request or it's a static resource, redirect to home page
getRedirectStrategy().sendRedirect(request, response, "/");
}
}
}

View File

@ -135,7 +135,7 @@ public class SecurityConfiguration {
boolean v2Enabled = appConfig.v2Enabled();
if (v2Enabled) {
http.addFilterBefore(
http.addFilterAt(
jwtAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(
@ -143,7 +143,8 @@ public class SecurityConfiguration {
exceptionHandling.authenticationEntryPoint(
jwtAuthenticationEntryPoint));
}
http.addFilterAt(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
http.addFilterBefore(
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(rateLimitingFilter(), userAuthenticationFilter.getClass())
.addFilterAfter(firstLoginFilter, rateLimitingFilter().getClass());
@ -209,8 +210,7 @@ public class SecurityConfiguration {
securityProperties, appConfig, jwtService))
.clearAuthentication(true)
.invalidateHttpSession(true)
.deleteCookies(
"JSESSIONID", "remember-me", "STIRLING_JWT_TOKEN"));
.deleteCookies("JSESSIONID", "remember-me", "stirling_jwt"));
http.rememberMe(
rememberMeConfigurer -> // Use the configurator directly
rememberMeConfigurer

View File

@ -38,6 +38,11 @@ public class JwtSaml2AuthenticationRequestRepository
Saml2PostAuthenticationRequest authRequest,
HttpServletRequest request,
HttpServletResponse response) {
if (!jwtService.isJwtEnabled()) {
log.warn("SAML2 v2 is not enabled, skipping saveAuthenticationRequest");
return;
}
if (authRequest == null) {
removeAuthenticationRequest(request, response);
return;

View File

@ -0,0 +1,307 @@
package stirling.software.proprietary.security.filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
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 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;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
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;
@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()).extractTokenFromRequest(any());
}
@Test
void shouldNotFilterWhenPageIsLogin() throws ServletException, IOException {
when(jwtService.isJwtEnabled()).thenReturn(true);
when(request.getRequestURI()).thenReturn("/login");
when(request.getMethod()).thenReturn("POST");
jwtAuthenticationFilter.doFilterInternal(request, response, filterChain);
verify(filterChain).doFilter(request, response);
verify(jwtService, never()).extractTokenFromRequest(any());
}
@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.getRequestURI()).thenReturn("/protected");
when(request.getMethod()).thenReturn("GET");
when(jwtService.extractTokenFromRequest(request)).thenReturn(token);
doNothing().when(jwtService).validateToken(token);
when(jwtService.extractAllClaims(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).extractAllClaims(token);
verify(userDetailsService).loadUserByUsername(username);
verify(securityContext).setAuthentication(any(UsernamePasswordAuthenticationToken.class));
verify(jwtService).generateToken(any(UsernamePasswordAuthenticationToken.class), eq(claims));
verify(jwtService).addTokenToResponse(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.extractTokenFromRequest(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.getMethod()).thenReturn("GET");
when(jwtService.extractTokenFromRequest(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.getMethod()).thenReturn("GET");
when(jwtService.extractTokenFromRequest(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 exceptinonThrown_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.getMethod()).thenReturn("GET");
when(jwtService.extractTokenFromRequest(request)).thenReturn(token);
doNothing().when(jwtService).validateToken(token);
when(jwtService.extractAllClaims(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 shouldNotFilterLoginPost() {
when(request.getRequestURI()).thenReturn("/login");
when(request.getMethod()).thenReturn("POST");
assertTrue(jwtAuthenticationFilter.shouldNotFilter(request));
}
@Test
void shouldNotFilterLoginGet() {
when(request.getRequestURI()).thenReturn("/login");
when(request.getMethod()).thenReturn("GET");
assertTrue(jwtAuthenticationFilter.shouldNotFilter(request));
}
@Test
void shouldNotFilterPublicPaths() {
String[] publicPaths = {
"/register",
"/error",
"/images/logo.png",
"/public/file.txt",
"/css/style.css",
"/fonts/font.ttf",
"/js/script.js",
"/pdfjs/viewer.js",
"/pdfjs-legacy/viewer.js",
"/api/v1/info/status",
"/site.webmanifest",
"/favicon.ico"
};
for (String path : publicPaths) {
when(request.getRequestURI()).thenReturn(path);
when(request.getMethod()).thenReturn("GET");
assertTrue(jwtAuthenticationFilter.shouldNotFilter(request),
"Should not filter path: " + path);
}
}
@Test
void shouldNotFilterStaticFiles() {
String[] staticFiles = {
"/some/path/file.svg",
"/another/path/image.png",
"/path/to/icon.ico"
};
for (String file : staticFiles) {
when(request.getRequestURI()).thenReturn(file);
when(request.getMethod()).thenReturn("GET");
assertTrue(jwtAuthenticationFilter.shouldNotFilter(request),
"Should not filter file: " + file);
}
}
@Test
void shouldFilterProtectedPaths() {
String[] protectedPaths = {
"/protected",
"/api/v1/user/profile",
"/admin",
"/dashboard"
};
for (String path : protectedPaths) {
when(request.getRequestURI()).thenReturn(path);
when(request.getMethod()).thenReturn("GET");
assertFalse(jwtAuthenticationFilter.shouldNotFilter(request),
"Should filter path: " + path);
}
}
@Test
void shouldFilterRootPath() {
when(request.getRequestURI()).thenReturn("/");
when(request.getMethod()).thenReturn("GET");
assertFalse(jwtAuthenticationFilter.shouldNotFilter(request));
}
@Test
void testAuthenticationEntryPointCalledWithCorrectException() throws ServletException, IOException {
when(jwtService.isJwtEnabled()).thenReturn(true);
when(request.getRequestURI()).thenReturn("/protected");
when(request.getMethod()).thenReturn("GET");
when(jwtService.extractTokenFromRequest(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);
}
}