简体   繁体   中英

CSRF token not generated with Webflux

I have a Webflux application secured by Spring Security in which the CSRF protection is enabled by default. However, I can't get the the CSRF token to be saved in the session.

After some investigations, I noticed that it may come from WebSessionServerCsrfTokenRepository.class . In this class, there is the generateToken method that should create a Mono from a generated CSRF token:

public Mono<CsrfToken> generateToken(ServerWebExchange exchange) {
        return Mono.fromCallable(() -> {
            return this.createCsrfToken();
        });
    }

private CsrfToken createCsrfToken() {
        return new DefaultCsrfToken(this.headerName, this.parameterName, this.createNewToken());
    }

    private String createNewToken() {
        return UUID.randomUUID().toString();
    }

However, even if the generateToken method is called by the CsrfWebFilter , the createCsrfToken method is never called, and I never get the CSRF token to be saved in the session. My breakpoint never goes into the createCsrfToken method, that could mean that it is never subscribed.

I'm running on Netty with Spring Boot 2.1.0.RELEASE and Spring Security 5.1.1.RELEASE .

I reproduced the problem on an empty sample application simply containing the following dependencies:

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </dependency>

Am I missing something or is there a problem with Spring Security?

UPDATE

From further investigations, I think that the problem comes from this method in the Spring Security CsrfWebFilter.class :

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

Here, the csrfToken Mono is never subscribed. When I rewrite the filter this way, I manage to get the token added in the session:

private Mono<Void> continueFilterChain(ServerWebExchange exchange, WebFilterChain chain) {
        return Mono.defer(() -> {
            return this.csrfToken(exchange)
                    .map(csrfToken -> exchange.getAttributes().put(CsrfToken.class.getName(), csrfToken))
                    .then(chain.filter(exchange));
        });
    }

However, the _csrf parameter is never added in my Thymeleaf model, so the following test doesn't work:

<form name="test-csrf" action="/test" method="post">
            <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
            <button type="submit">Escape!</button>
        </form>

In case somebody runs into this issue, I had a discussion with someone from Spring Team.

It is actually intended not to subscribe to the csrfToken Mono directly to do it only when needed. It is the responsability of the developper to trigger the subscription in the application, and there are two ways of doing it.

Method 1 : explicit subscription.

Provide a subscription via @ModelAttribute in some @ControllerAdvice or abstract controller class:

@ModelAttribute(CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME)
    public Mono<CsrfToken> getCsrfToken(final ServerWebExchange exchange) {

        return exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty());
    }

Method 2: use Thymeleaf to handle CSRF automatically.

Make sure you have the following dependencies in your POM to use Thymeleaf along with Spring Security:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

This will add the CSRF token to your model automatically, and pass it through the forms hidden inputs (still need to add it in the headers for POST Ajax request).

For more information, here's the issue I opened at Spring: https://github.com/spring-projects/spring-security/issues/6046

When I inspect the attributes property of exchange , I don't see the CsrfToken there, so I'm not able to subscribe to it. 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()
    }
}

This worked for us because one of the places CsrfWebFilter expects to find the token is in the request header X-XSRF-TOKEN .

create the webfilter bean as below in the Springboot application class.

@Bean
public WebFilter addCsrfTokenFilter() {
     return (exchange, next) -> Mono.just(exchange)
        .flatMap(ex -> ex. 
                  <Mono<CsrfToken>>getAttribute(CsrfToken.class.getName()))
        .doOnNext(ex -> {
        })
        .then(next.filter(exchange));

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM