ch4mp
ch4mp

Reputation: 12754

CSRF configuration on spring-cloud-gateway for Angular

I tried to adapt the documentation for reactive applications (spring-cloud-gateway used as BFF) and have configured it as an OAuth2 client with:

http.csrf().csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())
        .csrfTokenRequestHandler(new XorServerCsrfTokenRequestAttributeHandler()::handle);

but I still had a "An expected CSRF token cannot be found" error, and actually, I couldn't find the XSRF-TOKEN cookie is my browser debugging tools.

I then defined such a WebFilter:

@Bean
WebFilter csrfCookieWebFilter() {
    return (exchange, chain) -> {
        Mono<CsrfToken> csrfToken = exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty());
        return csrfToken.doOnSuccess(token -> {
        }).then(chain.filter(exchange));
    };
}

I now have a XSRF-TOKEN cookie, but also an "Invalid CSRF Token" error.

So, what is the CookieServerCsrfTokenRepository exactly and why couldn't I find the CSRF token cookie with it on spring-cloud-gateway?

How should I configure my spring-cloud-gateway to allow PUT requests to the /logout endpoint from an Angular application?

Edit

As a very similar question was asked more than 1.5 month ago but is still unanswered: "Angular app served behind Spring Cloud Gateway cannot send POST requests to backend because of invalid CSRF token", I opened a ticket for spring-security: https://github.com/spring-projects/spring-security/issues/12871

The other question is missing the part in the doc I linked, but still, it should produce a CSRF cookie (even if the value then fails to be validated because of the new BREACH proof handler).

Upvotes: 1

Views: 2407

Answers (2)

ch4mp
ch4mp

Reputation: 12754

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

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. This doc contains the exact configuration to copy / paste for 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 with 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). This was the reason why the cookie was not set after I added the filter: I had the import for servlet referenced in a 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)

Complete sample

The gateway module of the Backend For Frontend tutorial in this serie is a spring-cloud-gateway securing requests from an Angular application with sessions (and CSRF protection). This module using a Spring Boot starter of mine, the above configuration is controlled by a single configuration property: com.c4-soft.springaddons.security.client.csrf=cookie-accessible-from-js. Refer to the starter source code here and there for Java configuration:

@Bean
public SecurityWebFilterChain clientSecurityFilterChain(ServerHttpSecurity http) {
    ...
    http.csrf(csrf -> {
        var delegate = new XorServerCsrfTokenRequestAttributeHandler();
        csrf.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()).csrfTokenRequestHandler(delegate::handle);
    }
    ...
}

@Bean
WebFilter csrfCookieWebFilter() {
    return (exchange, chain) -> {
        Mono<CsrfToken> csrfToken = exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty());
        return csrfToken.doOnSuccess(token -> {
        }).then(chain.filter(exchange));
    };
}

Upvotes: 2

M. Merim
M. Merim

Reputation: 45

I have a Spring Cloud Gateway application that also handles OAuth2 authentication. Application front end is written in Angular.

I was struggling for a while to implement CSRF protection. I had two issues.

1. CSRF token was not set for user invoking requests. I found out that in Spring Boot 3 Reactive applications, CSRF tokens are not set eagerly for requests. Source: https://github.com/spring-projects/spring-security/issues/6046#issuecomment-437080518

I solved the issue by adding a cookie filter to subscribe to the token for every request.

 @Bean
  public WebFilter csrfCookieWebFilter() {
    return (exchange, chain) -> {
      Mono<CsrfToken> csrfToken = exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty());
      return csrfToken.doOnSuccess(token -> {
        /* Ensures the token is subscribed to. */
      }).then(chain.filter(exchange));
    };
  }

2. CSRF token is not read from cookie and set to header. Angular passes CSRF cookie in X-XSRF-TOKEN header, by default. It took me ages to figure it out that Angular will add the X-XSRF-TOKEN header only if the XSRF-TOKEN cookie was generated server-side with the following options:

  • Path = /
  • httpOnly = false

Source: https://stackoverflow.com/a/50511663/10491248

I did not set path to "/", after adding that to my tokenRepository CSRF token was successfully parsed from header.

This is how I implemented setting CSRF config for ServerHttpSecurity:

@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfig {
...

  @Bean
  public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
    ...
    setCsrfConf(http);

    return http.build();
  }
  ...

  private void setCsrfConf(ServerHttpSecurity http) {
    CookieServerCsrfTokenRepository tokenRepository = CookieServerCsrfTokenRepository.withHttpOnlyFalse();
    tokenRepository.setCookiePath("/");
    XorServerCsrfTokenRequestAttributeHandler delegate = new XorServerCsrfTokenRequestAttributeHandler();
    ServerCsrfTokenRequestHandler requestHandler = delegate::handle;
    http.csrf(csrf -> csrf
        .csrfTokenRepository(tokenRepository)
        .csrfTokenRequestHandler(requestHandler)
    );
  }

Refer to:

Upvotes: 0

Related Questions