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:///oauth2/default/v1/authorize?response_type=code&client_id=&scope=profile%20email%20openid&state=z1O38w7b_pnI6Kgsg5ZSe5jtZV0O94Qica20RVENYA0%3D&redirect_uri=http://localhost:8080/login/oauth2/code/okta&nonce=GkkLlebukLfJSTis_oCVBWtBSylg-MKXmFbc_KPOjUo' (redirected from 'http://localhost:8080/api/v1/') from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

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 csrfToken(ServerWebExchange exchange) { Mono csrfToken = exchange.getAttribute(CsrfToken.class.getName()); return csrfToken.doOnSuccess(token -> exchange.getAttributes().put(DEFAULT_CSRF_ATTR_NAME, token)); } } -- Not working

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 from org.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!