Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,21 @@ void cookieBasedAuthenticationSucceeds() {

@Test
void loginEndpointSetsHttpOnlySecureCookies() {
ResponseEntity<String> 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<Void> 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<String> setCookie = response.getHeaders().get(HttpHeaders.SET_COOKIE);
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ vigil:
public-paths:
- /public/**
- /auth/**
- /csrf
tenant:
enabled: true
protection:
Expand Down
Loading