From eb28ca086af3f46e8bbf69d3802ee41739e2ac51 Mon Sep 17 00:00:00 2001 From: Dario Ghunney Ware Date: Tue, 22 Jul 2025 15:25:59 +0100 Subject: [PATCH] Updated test --- .../filter/JwtAuthenticationFilter.java | 213 ++++++++++++++++++ .../security/model/AuthenticationType.java | 3 +- ...l2AuthenticationRequestRepositoryTest.java | 1 + 3 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java new file mode 100644 index 000000000..eaa333a76 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java @@ -0,0 +1,213 @@ +package stirling.software.proprietary.security.filter; + +import static stirling.software.proprietary.security.model.AuthenticationType.*; +import static stirling.software.proprietary.security.model.AuthenticationType.SAML2; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.Map; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +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 org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.model.exception.UnsupportedProviderException; +import stirling.software.proprietary.security.model.AuthenticationType; +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; + +@Slf4j +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtServiceInterface jwtService; + private final UserService userService; + private final CustomUserDetailsService userDetailsService; + private final AuthenticationEntryPoint authenticationEntryPoint; + private final ApplicationProperties.Security securityProperties; + + public JwtAuthenticationFilter( + JwtServiceInterface jwtService, + UserService userService, + CustomUserDetailsService userDetailsService, + AuthenticationEntryPoint authenticationEntryPoint, + ApplicationProperties.Security securityProperties) { + this.jwtService = jwtService; + this.userService = userService; + this.userDetailsService = userDetailsService; + this.authenticationEntryPoint = authenticationEntryPoint; + this.securityProperties = securityProperties; + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + if (!jwtService.isJwtEnabled()) { + filterChain.doFilter(request, response); + return; + } + if (shouldNotFilter(request)) { + filterChain.doFilter(request, response); + return; + } + + String jwtToken = jwtService.extractTokenFromRequest(request); + + if (jwtToken == null) { + // If they are unauthenticated and navigating to '/', redirect to '/login' instead of + // sending a 401 + if ("/".equals(request.getRequestURI()) + && "GET".equalsIgnoreCase(request.getMethod())) { + response.sendRedirect("/login"); + return; + } + handleAuthenticationFailure( + request, + response, + new AuthenticationFailureException("JWT is missing from the request")); + return; + } + + try { + jwtService.validateToken(jwtToken); + } catch (AuthenticationFailureException e) { + handleAuthenticationFailure(request, response, e); + return; + } + + Map claims = jwtService.extractAllClaims(jwtToken); + String tokenUsername = claims.get("sub").toString(); + + try { + Authentication authentication = createAuthentication(request, claims); + String jwt = jwtService.generateToken(authentication, claims); + + jwtService.addTokenToResponse(response, jwt); + } catch (SQLException | UnsupportedProviderException e) { + log.error("Error processing user authentication for user: {}", tokenUsername, e); + handleAuthenticationFailure( + request, + response, + new AuthenticationFailureException("Error processing user authentication", e)); + return; + } + + filterChain.doFilter(request, response); + } + + private Authentication createAuthentication( + HttpServletRequest request, Map claims) + throws SQLException, UnsupportedProviderException { + String username = claims.get("sub").toString(); + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + processUserAuthenticationType(claims, username); + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + + if (userDetails != null) { + UsernamePasswordAuthenticationToken authToken = + new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + + log.debug("JWT authentication successful for user: {}", username); + + } else { + throw new UsernameNotFoundException("User not found: " + username); + } + } + + return SecurityContextHolder.getContext().getAuthentication(); + } + + private void processUserAuthenticationType(Map claims, String username) + throws SQLException, UnsupportedProviderException { + AuthenticationType authenticationType = + AuthenticationType.valueOf(claims.getOrDefault("authType", WEB).toString()); + log.debug("Processing {} login for {} user", authenticationType, username); + + switch (authenticationType) { + case OAUTH2 -> { + ApplicationProperties.Security.OAUTH2 oauth2Properties = + securityProperties.getOauth2(); + userService.processSSOPostLogin( + username, oauth2Properties.getAutoCreateUser(), OAUTH2); + } + case SAML2 -> { + ApplicationProperties.Security.SAML2 saml2Properties = + securityProperties.getSaml2(); + userService.processSSOPostLogin( + username, saml2Properties.getAutoCreateUser(), SAML2); + } + } + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + String uri = request.getRequestURI(); + String method = request.getMethod(); + + // Skip JWT processing for logout requests to prevent token refresh during logout + if ("/logout".equals(uri)) { + return true; + } + + // Allow login POST requests to be processed + if ("/login".equals(uri) && "POST".equalsIgnoreCase(method)) { + return true; + } + + String[] permitAllPatterns = { + "/login", + "/register", + "/error", + "/images/", + "/public/", + "/css/", + "/fonts/", + "/js/", + "/pdfjs/", + "/pdfjs-legacy/", + "/api/v1/info/status", + "/site.webmanifest", + "/favicon" + }; + + for (String pattern : permitAllPatterns) { + if (uri.startsWith(pattern) + || uri.endsWith(".svg") + || uri.endsWith(".png") + || uri.endsWith(".ico")) { + return true; + } + } + + return false; + } + + private void handleAuthenticationFailure( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) + throws IOException, ServletException { + authenticationEntryPoint.commence(request, response, authException); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/model/AuthenticationType.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/model/AuthenticationType.java index b3042dd25..cf9f15e35 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/model/AuthenticationType.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/model/AuthenticationType.java @@ -2,8 +2,7 @@ package stirling.software.proprietary.security.model; public enum AuthenticationType { WEB, - SSO, - // TODO: Worth making a distinction between OAuth2 and SAML2? + @Deprecated(since = "1.0.2") SSO, OAUTH2, SAML2 } diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepositoryTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepositoryTest.java index 11f3c00d9..bce663aee 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepositoryTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepositoryTest.java @@ -65,6 +65,7 @@ class JwtSaml2AuthenticationRequestRepositoryTest { 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);