diff --git a/src/test/java/io/github/sequelcore/vigil/integration/VigilAsyncSecurityIntegrationTest.java b/src/test/java/io/github/sequelcore/vigil/integration/VigilAsyncSecurityIntegrationTest.java index ab0f5ff..ebeb2f6 100644 --- a/src/test/java/io/github/sequelcore/vigil/integration/VigilAsyncSecurityIntegrationTest.java +++ b/src/test/java/io/github/sequelcore/vigil/integration/VigilAsyncSecurityIntegrationTest.java @@ -1,8 +1,10 @@ package io.github.sequelcore.vigil.integration; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -120,6 +122,13 @@ void statelessAsyncSupportDoesNotCreateOrReuseHttpSession() throws Exception { mvc.perform(get("/protected/session-state")).andExpect(status().isForbidden()); } + @Test + void stateChangingBrowserRequestWithoutCsrfTokenIsRejected() throws Exception { + mvc.perform(post("/auth/login")).andExpect(status().isForbidden()); + mvc.perform(post("/auth/login").with(csrf().useInvalidToken())) + .andExpect(status().isForbidden()); + } + @Test void authorizationRulesApplyBeforeAndDuringAsyncDispatch() throws Exception { mvc.perform(authenticatedGet("/protected/admin/deferred", "ordinary-user", "USER")) diff --git a/src/test/java/io/github/sequelcore/vigil/integration/VigilIntegrationTest.java b/src/test/java/io/github/sequelcore/vigil/integration/VigilIntegrationTest.java index 2c52e8e..f373749 100644 --- a/src/test/java/io/github/sequelcore/vigil/integration/VigilIntegrationTest.java +++ b/src/test/java/io/github/sequelcore/vigil/integration/VigilIntegrationTest.java @@ -106,8 +106,21 @@ void cookieBasedAuthenticationSucceeds() { @Test void loginEndpointSetsHttpOnlySecureCookies() { + ResponseEntity csrf = restTemplate.getForEntity(url("/csrf"), String.class); + assertThat(csrf.getHeaders().get(HttpHeaders.SET_COOKIE)) + .noneMatch(header -> header.startsWith("JSESSIONID=")); + String csrfCookie = + csrf.getHeaders().get(HttpHeaders.SET_COOKIE).stream() + .filter(header -> header.startsWith("XSRF-TOKEN=")) + .findFirst() + .orElseThrow() + .split(";", 2)[0]; + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.COOKIE, csrfCookie); + headers.add("X-XSRF-TOKEN", csrf.getBody()); + ResponseEntity response = - restTemplate.postForEntity(url("/auth/login"), HttpEntity.EMPTY, Void.class); + restTemplate.postForEntity(url("/auth/login"), new HttpEntity<>(headers), Void.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); List setCookie = response.getHeaders().get(HttpHeaders.SET_COOKIE); @@ -117,6 +130,7 @@ void loginEndpointSetsHttpOnlySecureCookies() { assertThat(setCookie).allMatch(header -> header.contains("HttpOnly")); assertThat(setCookie).allMatch(header -> header.contains("Secure")); assertThat(setCookie).allMatch(header -> header.contains("SameSite=Lax")); + assertThat(setCookie).noneMatch(header -> header.startsWith("JSESSIONID=")); } @Test diff --git a/src/test/java/io/github/sequelcore/vigil/integration/testapp/TestApplication.java b/src/test/java/io/github/sequelcore/vigil/integration/testapp/TestApplication.java index 89ea4af..207eb18 100644 --- a/src/test/java/io/github/sequelcore/vigil/integration/testapp/TestApplication.java +++ b/src/test/java/io/github/sequelcore/vigil/integration/testapp/TestApplication.java @@ -6,12 +6,12 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.context.RequestAttributeSecurityContextRepository; import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; @SpringBootApplication public class TestApplication { @@ -27,7 +27,7 @@ SecurityFilterChain securityFilterChain( SecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository(); authenticationFilter.setSecurityContextRepository(securityContextRepository); - http.csrf(AbstractHttpConfigurer::disable) + http.csrf(csrf -> csrf.csrfTokenRepository(new CookieCsrfTokenRepository())) .sessionManagement( session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .securityContext(context -> context.securityContextRepository(securityContextRepository)) diff --git a/src/test/java/io/github/sequelcore/vigil/integration/testapp/TestController.java b/src/test/java/io/github/sequelcore/vigil/integration/testapp/TestController.java index c901881..1cc0472 100644 --- a/src/test/java/io/github/sequelcore/vigil/integration/testapp/TestController.java +++ b/src/test/java/io/github/sequelcore/vigil/integration/testapp/TestController.java @@ -12,6 +12,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; +import org.springframework.security.web.csrf.CsrfToken; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; @@ -37,6 +38,11 @@ public String health() { return "ok"; } + @GetMapping("/csrf") + public String csrf(CsrfToken csrfToken) { + return csrfToken.getToken(); + } + @GetMapping("/protected/hello") public String protectedHello(Authentication authentication) { return authentication.getName(); diff --git a/src/test/resources/io/github/sequelcore/vigil/integration/testapp/application-test.yml b/src/test/resources/io/github/sequelcore/vigil/integration/testapp/application-test.yml index cf06df5..9e824c8 100644 --- a/src/test/resources/io/github/sequelcore/vigil/integration/testapp/application-test.yml +++ b/src/test/resources/io/github/sequelcore/vigil/integration/testapp/application-test.yml @@ -10,6 +10,7 @@ vigil: public-paths: - /public/** - /auth/** + - /csrf tenant: enabled: true protection: