0

I'm experimenting with the possibility of serving an Angular application through Spring Cloud Gateway.

Everything works fine for GET requests but as soon as I try to send a POST request to the resource service through the gateway, the request fails because of "Invalid CSRF token".

This is probably because of some misconfiguration but I'm not being able to find it. Also, online documentation for both Spring Gateway and Angular are not being very useful for this specific scenario.

Here is the repository with the current configuration if you want to replicate this behavior: github repo.

Current behavior: POST request from Angular fails because of "Invalid CSRF token":

error: "Invalid CSRF Token"
message: "Http failure response for http://gateway:8000/api/post: 403 Forbidden"
name: "HttpErrorResponse"
​ok: false
​status: 403
​statusText: "Forbidden"
​url: "http://gateway:8000/api/post"

Desired behavior: POST requests executing successfully without disabling csrf in spring security.

This is my Spring Security configuration for the gateway:

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

    @Bean
    SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) throws Exception {

        return http
                .authorizeExchange(exchange -> exchange
                        .pathMatchers("/**").permitAll()
                        .anyExchange().authenticated())
                .csrf(csrf -> csrf.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()))
                .build();
    }
}

Gateway also contains the following WebFilter:

@Component
@Configuration
public class CsrfCookieWebFilter implements WebFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        String key = CsrfToken.class.getName();
        Mono<CsrfToken> csrfToken = null != exchange.getAttribute(key) ? exchange.getAttribute(key) : Mono.empty();
        return csrfToken.doOnSuccess(token -> {
            ResponseCookie cookie = ResponseCookie.from("XSRF-TOKEN", token.getToken())
                    .maxAge(Duration.ofHours(1))
                    .httpOnly(false)
                    .path("/")
                    .sameSite(Cookie.SameSite.LAX.attributeValue())
                    .build();
            exchange.getResponse().getCookies().add("XSRF-TOKEN", cookie);
        }).then(chain.filter(exchange));
    }
}

The angular application AppModule contains the import of HttpClientXsrfModule.

1 Answers1

0

Short answer: RTFM and double check the CsrfToken you imported (there is one for WebMVC and a different one from another package for WebFlux).

Also, as usual for Angular apps, double check that the X-XSRF-TOKEN header was correctly positioned (Angular does not set this header if the request URI is absolute: use this.httpClient.post('/api/post', {}, {observe: 'response'}) and not this.httpClient.post('http://gateway:8000/api/post', {}, {observe: 'response'}), which requires to serve the Angular app through the gateway too)

Having the CSRF cookie set

Since Spring Boot 3 (spring-security 6), it is mandatory to provide with a filter to add the CSRF cookie to the response. The Cookie(Server)CsrfTokenRepository is not enough any more.

This is documented here for servlets and there for reactive applications (the second is the one to refer to when configuring spring-cloud-gateway). This doc contains the exact configuration to copy / paste in each case.

Also, be very careful to import the right CsrfToken depending on the nature of your application or the token will be null: the request / exchange attribute has CsrfToken.class.getName() as name and you could import the one from org.springframework.security.web.csrf or org.springframework.security.web.server.csrf without any compilation error (the first is to be used in servlet and the second in webflux). The CSRF cookie was not set by my gateway instance until I realized that I had imported servlet version of CsrfCookie in my WebFilter => the CSRF token value was not resolved.

Having the CSRF token correctly validated

As stated in the doc, the handle method of a Xor(Server)CsrfTokenRequestAttributeHandler should be used as csrf request handler (only the handle method, not the full Xor(Server)CsrfTokenRequestAttributeHandler instance)

Working conf for spring-cloud-gateway

@Bean
WebFilter csrfCookieWebFilter() {
    return (exchange, chain) -> {
        // Make sure you import
        // org.springframework.security.web.server.csrf.CsrfToken
        // and not
        // org.springframework.security.web.csrf.CsrfToken
        Mono<CsrfToken> csrfToken = exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty());
        return csrfToken.doOnSuccess(token -> {
        }).then(chain.filter(exchange));
    };
}

and in SecurityWebFilterChain:

var delegate = new XorServerCsrfTokenRequestAttributeHandler();
http.csrf().csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())
        .csrfTokenRequestHandler(delegate::handle);
ch4mp
  • 6,622
  • 6
  • 29
  • 49