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?
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).
Short answer: RTFM and double check the CsrfToken
you imported (there is one for WebMVC and a different one from another package for WebFlux)
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.
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)
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));
};
}