diff --git a/src/main/java/tech/jhipster/lite/generator/server/springboot/mvc/security/jwt/domain/JwtAuthenticationModuleFactory.java b/src/main/java/tech/jhipster/lite/generator/server/springboot/mvc/security/jwt/domain/JwtAuthenticationModuleFactory.java index 552cbf52fd5..a2e61f7dff3 100644 --- a/src/main/java/tech/jhipster/lite/generator/server/springboot/mvc/security/jwt/domain/JwtAuthenticationModuleFactory.java +++ b/src/main/java/tech/jhipster/lite/generator/server/springboot/mvc/security/jwt/domain/JwtAuthenticationModuleFactory.java @@ -69,11 +69,13 @@ public JHipsterModule buildModule(JHipsterModuleProperties properties) { .addTemplate("JWTFilter.java") .addTemplate("JwtReader.java") .addTemplate("SecurityConfiguration.java") + .addTemplate("SpaWebFilter.java") .and() .add(TEST_SOURCE.append(APPLICATION).template("AuthenticatedUserTest.java"), testDestination.append(APPLICATION).append("AuthenticatedUserTest.java")) .batch(TEST_SOURCE.append(PRIMARY), testDestination.append(PRIMARY)) .addTemplate("JWTFilterTest.java") .addTemplate("JwtReaderTest.java") + .addTemplate("SpaWebFilterIT.java") .and() .and() .springMainProperties() diff --git a/src/main/resources/generator/server/springboot/mvc/security/jwt/authentication/main/infrastructure/primary/SecurityConfiguration.java.mustache b/src/main/resources/generator/server/springboot/mvc/security/jwt/authentication/main/infrastructure/primary/SecurityConfiguration.java.mustache index fd46970e1a0..6548d690fc6 100644 --- a/src/main/resources/generator/server/springboot/mvc/security/jwt/authentication/main/infrastructure/primary/SecurityConfiguration.java.mustache +++ b/src/main/resources/generator/server/springboot/mvc/security/jwt/authentication/main/infrastructure/primary/SecurityConfiguration.java.mustache @@ -22,6 +22,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; import org.springframework.web.filter.CorsFilter; @@ -55,6 +56,7 @@ class SecurityConfiguration { http .csrf(csrf -> csrf.disable()) .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) + .addFilterAfter(new SpaWebFilter(), BasicAuthenticationFilter.class) .headers(headers -> headers .contentSecurityPolicy(csp -> csp.policyDirectives(properties.getContentSecurityPolicy())) .frameOptions(FrameOptionsConfig::deny) @@ -67,9 +69,12 @@ class SecurityConfiguration { .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authz -> authz .requestMatchers(antMatcher(HttpMethod.OPTIONS, "/**")).permitAll() + .requestMatchers(antMatcher("/"), antMatcher("/index.html"), antMatcher("/*.css"), antMatcher("/*.js")).permitAll() .requestMatchers(antMatcher("/app/**")).permitAll() + .requestMatchers(antMatcher("/assets/**")).permitAll() .requestMatchers(antMatcher("/i18n/**")).permitAll() .requestMatchers(antMatcher("/content/**")).permitAll() + .requestMatchers(antMatcher("/style/**")).permitAll() .requestMatchers(antMatcher("/swagger-ui/**")).permitAll() .requestMatchers(antMatcher("/swagger-ui.html")).permitAll() .requestMatchers(antMatcher("/v3/api-docs/**")).permitAll() diff --git a/src/main/resources/generator/server/springboot/mvc/security/jwt/authentication/main/infrastructure/primary/SpaWebFilter.java.mustache b/src/main/resources/generator/server/springboot/mvc/security/jwt/authentication/main/infrastructure/primary/SpaWebFilter.java.mustache new file mode 100644 index 00000000000..65f4b522126 --- /dev/null +++ b/src/main/resources/generator/server/springboot/mvc/security/jwt/authentication/main/infrastructure/primary/SpaWebFilter.java.mustache @@ -0,0 +1,32 @@ +package {{packageName}}.shared.authentication.infrastructure.primary; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.springframework.web.filter.OncePerRequestFilter; + +public class SpaWebFilter extends OncePerRequestFilter { + + /** + * Forwards any unmapped paths (except those containing a period) to the client {@code index.html}. + */ + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + // Request URI includes the contextPath if any, removed it. + String path = request.getRequestURI().substring(request.getContextPath().length()); + if ( + !path.startsWith("/api") && + !path.startsWith("/management") && + !path.startsWith("/v3/api-docs") && + !path.contains(".") + ) { + request.getRequestDispatcher("/index.html").forward(request, response); + return; + } + + filterChain.doFilter(request, response); + } +} diff --git a/src/main/resources/generator/server/springboot/mvc/security/jwt/authentication/test/infrastructure/primary/SpaWebFilterIT.java.mustache b/src/main/resources/generator/server/springboot/mvc/security/jwt/authentication/test/infrastructure/primary/SpaWebFilterIT.java.mustache new file mode 100644 index 00000000000..ba11b99569e --- /dev/null +++ b/src/main/resources/generator/server/springboot/mvc/security/jwt/authentication/test/infrastructure/primary/SpaWebFilterIT.java.mustache @@ -0,0 +1,90 @@ +package {{packageName}}.shared.authentication.infrastructure.primary; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import {{packageName}}.IntegrationTest; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@AutoConfigureMockMvc +@WithMockUser +@IntegrationTest +class SpaWebFilterIT { + + @Autowired + private MockMvc mockMvc; + + @Test + void testFilterForwardsToIndex() throws Exception { + mockMvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(forwardedUrl("/index.html")); + } + + @Test + @WithMockUser(authorities = "ADMIN") + void testFilterDoesNotForwardToIndexForV3ApiDocs() throws Exception { + mockMvc.perform(get("/v3/api-docs")) + .andExpect(status().isOk()) + .andExpect(forwardedUrl(null)); + } + + @Test + @WithMockUser(authorities = "ADMIN") + void testFilterDoesNotForwardToIndexForManagement() throws Exception { + mockMvc.perform(get("/management")) + .andExpect(status().isForbidden()) + .andExpect(forwardedUrl(null)); + } + + @Test + void testFilterDoesNotForwardToIndexForDotFile() throws Exception { + mockMvc.perform(get("/file.js")) + .andExpect(status().isNotFound()); + } + + @Test + void getBackendEndpoint() throws Exception { + mockMvc.perform(get("/test")) + .andExpect(status().isOk()) + .andExpect(forwardedUrl("/index.html")); + } + + @Test + void forwardUnmappedFirstLevelMapping() throws Exception { + mockMvc.perform(get("/first-level")) + .andExpect(status().isOk()) + .andExpect(forwardedUrl("/index.html")); + } + + @Test + void forwardUnmappedSecondLevelMapping() throws Exception { + mockMvc.perform(get("/first-level/second-level")) + .andExpect(status().isOk()) + .andExpect(forwardedUrl("/index.html")); + } + + @Test + void forwardUnmappedThirdLevelMapping() throws Exception { + mockMvc.perform(get("/first-level/second-level/third-level")) + .andExpect(status().isOk()) + .andExpect(forwardedUrl("/index.html")); + } + + @Test + void forwardUnmappedDeepMapping() throws Exception { + mockMvc.perform(get("/1/2/3/4/5/6/7/8/9/10")) + .andExpect(forwardedUrl("/index.html")); + } + + @Test + void getUnmappedFirstLevelFile() throws Exception { + mockMvc.perform(get("/foo.js")) + .andExpect(status().isNotFound()); + } +}