Search code examples
spring-bootspring-securityspring-webfluxspring-cloud-gatewaycsrf-token

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).


Solution

  • 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));
        };
    }