Reputation: 12754
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).
Upvotes: 1
Views: 2407
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)
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));
};
}
Upvotes: 2
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:
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