Summary
I have a Spring Webflux application secured by Spring Security with CSRF protection enabled by default. In this application, I can't get the CSRF token to be saved in the Websession nor added in the model.
Actual Behavior
After some investigations, I noticed that the problem comes from Spring Security's CsrfWebFilter.class, in which there is the following method:
private Mono<Void> continueFilterChain(ServerWebExchange exchange, WebFilterChain chain) {
return Mono.defer(() -> {
Mono<CsrfToken> csrfToken = this.csrfToken(exchange);
exchange.getAttributes().put(CsrfToken.class.getName(), csrfToken);
return chain.filter(exchange);
});
}
In this method, the CsrfToken Mono is never subscribed, which prevents the token to be generated and added in the Websession.
Moreover, when my page is rendered, the _csrf parameter is null in the view model.
Expected Behavior
The CsrfToken Mono should be subscribed so that the WebSessionServerCsrfTokenRepository could generate and save the token in the Websession. The _csrf parameter should be accessible from the view model (maybe this one is an issue with Thymeleaf).
Workaround
As a workaround, I rewrite the CsrfWebFilter in my application and just override the above method this way:
private Mono<Void> continueFilterChain(ServerWebExchange exchange, WebFilterChain chain) {
return Mono.defer(() -> {
return this.csrfToken(exchange)
.doOnNext(csrfToken -> exchange.getAttributes().put(CsrfToken.class.getName(), csrfToken))
.then(chain.filter(exchange));
});
}
Then, to be able to retrieve the _csrf parameter in the model, I add this method in an abstract controller:
@ModelAttribute("_csrf")
public CsrfToken csrfToken(final ServerWebExchange exchange) {
return exchange.getAttribute(CsrfToken.class.getName());
}
Is it a valid workaround?
Configuration
Here is my Security configuration class:
@EnableWebFluxSecurity
public class SecurityConfiguration {
@Bean
public SecurityWebFilterChain springWebFilterChain(final ServerHttpSecurity http) {
http.authorizeExchange()
.pathMatchers("/**").permitAll()
.and()
.oauth2Login()
.and()
.oauth2Client();
return http.build();
}
@Bean
public ServerOAuth2AuthorizedClientRepository authorizedClientRepository(
final ReactiveOAuth2AuthorizedClientService authorizedClientService) {
return new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(authorizedClientService);
}
}
Version
My application runs on Netty with Spring Boot 2.1.0.RELEASE, Spring Security 5.1.1.RELEASE, and Thymeleaf 3.0.10.RELEASE.
Comment From: rwinch
Thanks for the report.
This is intentional behavior because we don't want to create the token (and thus a session) unless it is necessary.
If you are fine with creating the token eagerly, the recommended workaround is to use ControllerAdvice. Alternatively, newer versions of Thymeleaf automatically subscribe and no additional work is necessary.
I'm going to close this as working as designed. If you find that you have additional questions/concerns, please let me know.
Comment From: adsanche
Thanks for your reply.
I didn't manage to subscribe automatically via Thymeleaf, or even make the CsrfRequestDataValueProcessor handling it automatically. But anyway, it's OK for me to do it via a model attribute, thank you.
Comment From: rwinch
@adsanche This is interesting that it isn't working. Can you put together a sample that reproduces the issue and post to a new ticket? The sample I linked to has tests and is working fine so there must be something strange happening.
Comment From: rwinch
@adsanche
I put together a sample of the Thymeleaf integration working here https://github.com/rwinch/spring-security-sample/tree/gh-6046
Make sure you have the correct dependencies (both spring-boot-starter-thymeleaf and thymeleaf-extras-springsecurity5).
Make sure you use th:action for your form.
The test demos that it is working
Comment From: adsanche
@rwinch I managed to make it work automatically based on your sample project.
I was actually missing the thymeleaf-extras-springsecurity5 dependency to trigger the addition of the CSRF parameter in the model.
This way, I still have to add my token explicitly in the request headers for the Ajax requests, but I can remove the hidden parameter from the forms and the model attribute in the controller advice.
Thanks for your feedbacks and explanations, hope it might help other people.
Comment From: dillius
Is there a way to solve this problem if you are using functional framework in Spring 5? The ControllerAdvice doesn't seem to have any effect if you aren't actually defining Controllers.
Comment From: broth-eu
In case one does not have a controller, the simplest workaround is to implement a custom WebFilter which places the Mono<CsrfToken> inside of the filter chain like so:
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
Mono<CsrfToken> token = (Mono<CsrfToken>) exchange.getAttributes().get(CsrfToken.class.getName());
if (token != null) {
return token.flatMap(t -> chain.filter(exchange));
}
return chain.filter(exchange);
}
Comment From: jndietz
I tried using @broth-eu solution for our project that is a React app paired with Spring Cloud Gateway, but token ends up as null. When I inspect the attributes property of exchange, I don't see the CsrfToken there. Upon inspecting the cookies in the browser session, I did see XSRF-TOKEN.
I ended up using this as a filter in our project:
@Component
@Slf4j
class CsrfHeaderFilter implements WebFilter {
@Override
Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
def xsrfToken = exchange.getRequest().getCookies().getFirst("XSRF-TOKEN").value
exchange = exchange.mutate().request({
it.header("X-XSRF-TOKEN", xsrfToken)
}).build()
log.debug(xsrfToken)
chain.filter(exchange)
}
}
Then modified my security configuration so that it placed this filter before the CsrfWebFilter:
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
class WebSecurityConfiguration {
@Autowired
CsrfHeaderFilter csrfHeaderFilter
@Bean
SecurityWebFilterChain SecurityWebFilterChain(ServerHttpSecurity http) {
http
.addFilterBefore(csrfHeaderFilter, SecurityWebFiltersOrder.CSRF)
.httpBasic().disable()
.formLogin().disable()
.oauth2Login().and()
.csrf({
it.csrfTokenRepository(new CookieServerCsrfTokenRepository())
})
.authorizeExchange()
.pathMatchers("/actuator/health").permitAll()
.pathMatchers("/**").authenticated()
.and().build()
}
}
Something about this still doesn't feel right, but it solved our issues. This worked for us because one of the places CsrfWebFilter expects to find X-XSRF-TOKEN is in the request header, which it never was.
Comment From: rahul6941
This is not working for me . All request block from React App to Server for Okta routing for auth .
Error
Access to XMLHttpRequest at 'https://
Comment From: mihaita-tinta
In case one does not have a controller, the simplest workaround is to implement a custom WebFilter which places the
Mono<CsrfToken>inside of the filter chain like so:
java public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { Mono<CsrfToken> token = (Mono<CsrfToken>) exchange.getAttributes().get(CsrfToken.class.getName()); if (token != null) { return token.flatMap(t -> chain.filter(exchange)); } return chain.filter(exchange); }
This also worked for me. You need to be sure you are importing the right class: import org.springframework.security.web.server.csrf.CsrfToken; ( token ended up null when I imported the wrong one from org.springframework.security.web.csrf.CsrfToken)
Comment From: SadiyaSaad
@rwinch @broth-eu I tried all possible solution suggested in the above chain of comments . Nothing seems to work. Please note : My Springcloudgateway performs a simple task of delegating the request to underlying microservices. All I need is XSRF token to be appended in cookies.
Routes: builder.routes() .route("user-service", r -> r.path("/users*/ ") .filters(f -> f.filter(filter).modifyResponseBody(String.class, String.class, qibRewriteFunction)) .uri("lb://user-service"))
/*
* @author Rob Winch
* @since 5.0
/
@ControllerAdvice
public class CsrfControllerAdvice {
@ModelAttribute
public Mono
Customer Filter is also not working: @Component
public class CsrfHelperFilter implements WebFilter {
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
Mono<CsrfToken> token = (Mono<CsrfToken>) exchange.getAttributes().get(CsrfToken.class.getName()); --this is always null
if (token != null) {
return token.flatMap(t -> chain.filter(exchange));
}
return chain.filter(exchange);
}
Is there any way to make this working , as all the workarounds are failing .
Comment From: varunvarma799
@SadiyaSaad, my use case is the same as yours and I too couldn't find anything working. Were you able to solve this?
Comment From: mihaita-tinta
Maybe this can help. From my tests everything is fine. You can see the csrf token generated. Ignore the webauthn part. The UI can send the token on POST requests.
Comment From: varunvarma799
@mihaita-tinta, thanks for your reply. I tried the same but it didn't work. My use case is this, I have a gateway server that needs to route requests to underlying microservices. When I disable CSRF everything works. When I enable CSRF by adding spring security in the gateway, I'm only able to access methods of type GET for all others it gives this repsones "An expected CSRF token cannot be found". I tried adding filters,@controller advice as suggested in spring-docs but nothing is working.
Comment From: pitprok
In case one does not have a controller, the simplest workaround is to implement a custom WebFilter which places the
Mono<CsrfToken>inside of the filter chain like so:java public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { Mono<CsrfToken> token = (Mono<CsrfToken>) exchange.getAttributes().get(CsrfToken.class.getName()); if (token != null) { return token.flatMap(t -> chain.filter(exchange)); } return chain.filter(exchange); }This also worked for me. You need to be sure you are importing the right class:
import org.springframework.security.web.server.csrf.CsrfToken;( token ended up null when I imported the wrong one fromorg.springframework.security.web.csrf.CsrfToken)
I can't understand why this is working. I did try it and it worked but I have no clue why. I don't see the token being added to the response, it just seems to return chain.filter(exchange). If I remove the if, the token is not added. So the flatmap is definitely what's making it work. Can someone explain how this ends up adding the token to the response?
Comment From: mihaita-tinta
From what I see CsrfWebFilter generates the Mono<CsrfToken> that adds the token to the response with the CookieServerCsrfTokenRepository.saveToken method. The tricky part is related to this Mono because it gets evaluated only if it is subscribed with our custom WebFilter (calling csrfTokenRepository.generateToken -> csrfTokenRepository.saveToken )
Spring's CsrfWebFilter:
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
if (Boolean.TRUE.equals(exchange.getAttribute(SHOULD_NOT_FILTER))) {
return chain.filter(exchange).then(Mono.empty());
}
//check if the request should be validated with the csrf token
return this.requireCsrfProtectionMatcher.matches(exchange).filter(MatchResult::isMatch)
.filter((matchResult) -> !exchange.getAttributes().containsKey(CsrfToken.class.getName()))
.flatMap((m) -> validateToken(exchange)).flatMap((m) -> continueFilterChain(exchange, chain))
// if the csrf validation shouldn't happen, i.e: GET request, continueFilterChain adds the Mono<CsrfToken> as an attribute, but doesn't subscribe to it, we should do this with that flatmap.
.switchIfEmpty(continueFilterChain(exchange, chain).then(Mono.empty()))
.onErrorResume(CsrfException.class, (ex) -> this.accessDeniedHandler.handle(exchange, ex));
}
private Mono<Void> continueFilterChain(ServerWebExchange exchange, WebFilterChain chain) {
return Mono.defer(() -> {
Mono<CsrfToken> csrfToken = csrfToken(exchange);
exchange.getAttributes().put(CsrfToken.class.getName(), csrfToken);
return chain.filter(exchange);
});
}
private Mono<Void> continueFilterChain(ServerWebExchange exchange, WebFilterChain chain) {
return Mono.defer(() -> {
Mono<CsrfToken> csrfToken = csrfToken(exchange);
exchange.getAttributes().put(CsrfToken.class.getName(), csrfToken);
return chain.filter(exchange);
});
}
private Mono<CsrfToken> generateToken(ServerWebExchange exchange) {
return this.csrfTokenRepository.generateToken(exchange)
.delayUntil((token) -> this.csrfTokenRepository.saveToken(exchange, token)).cache();
}
Where the csrf token gets written as a cookie in the response CookieServerCsrfTokenRepository:
@Override
public Mono<CsrfToken> generateToken(ServerWebExchange exchange) {
return Mono.fromCallable(this::createCsrfToken).subscribeOn(Schedulers.boundedElastic());
}
@Override
public Mono<Void> saveToken(ServerWebExchange exchange, CsrfToken token) {
return Mono.fromRunnable(() -> {
String tokenValue = (token != null) ? token.getToken() : "";
// @formatter:off
ResponseCookie cookie = ResponseCookie
.from(this.cookieName, tokenValue)
.domain(this.cookieDomain)
.httpOnly(this.cookieHttpOnly)
.maxAge(!tokenValue.isEmpty() ? this.cookieMaxAge : 0)
.path((this.cookiePath != null) ? this.cookiePath : getRequestContext(exchange.getRequest()))
.secure((this.secure != null) ? this.secure : (exchange.getRequest().getSslInfo() != null))
.build();
// @formatter:on
exchange.getResponse().addCookie(cookie);
});
}
Comment From: manjosh1990
In case one does not have a controller, the simplest workaround is to implement a custom WebFilter which places the
Mono<CsrfToken>inside of the filter chain like so:
java public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { Mono<CsrfToken> token = (Mono<CsrfToken>) exchange.getAttributes().get(CsrfToken.class.getName()); if (token != null) { return token.flatMap(t -> chain.filter(exchange)); } return chain.filter(exchange); }
With this change are you able to make post requests? Because when I implemented this, I was able to add the CSRF token in the response, but my POST requests to the underlying rest services failed. Somehow the formData is lost and the API endpoint throws 400 bad request error. Sample project is here https://github.com/manjosh1990/webgateway-issues
Comment From: zg2pro
Hi, for me following @mihaita-tinta 's code the csrf is generated only once, at the first query. Once the cookie is stored in the browser it is never renewed. So it seems it's not really CSRF, the token should change at least after each POST. You see a way to configure a CSRF different for each request ? Actually, not only that the CSRF is constant but it's not even checked: I changed the value of the cookie from my developer console and my request still went through!