spring-security: CookieServerCsrfTokenRepository does not add cookie

Summary

I have modified the https://github.com/rwinch/spring-security-sample boot-webflux branch to add CSRF using the CookieServerCsrfTokenRepository.

Actual Behavior

If I do a GET to localhost:8080 I do not see a CSRF cookie being set.

Expected Behavior

A cookie is set so that on subsequent requests I can extract the CSRF token from there and pass it along using a CSRF header.

Configuration

I have modified the boot-webflux WebSecurityConfig like so:

@EnableWebFluxSecurity
public class WebSecurityConfig {
    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        return http
                .authorizeExchange()
                .anyExchange().permitAll()
                .and()
                .csrf().csrfTokenRepository(new CookieServerCsrfTokenRepository())
                .and()
                .build();
    }

Version

I am using Spring Boot 2.1.0.RC2 which uses Spring Security 5.1.0.RC1.

Sample

See https://github.com/RoyJacobs/spring-security-cookie-repro

About this issue

  • Original URL
  • State: open
  • Created 6 years ago
  • Reactions: 1
  • Comments: 29 (13 by maintainers)

Most upvoted comments

I tend to agree that this should remain open as a bug.

This step seems like it should be unnecessary; correct configuration of the CookieServerCsrfTokenRepository should result in the cookie being utilized in the same manner as it would for WebMVC.

Seems odd to require a workaround to add the CsrfToken for CookieServerCsrfTokenRepository. I’m a reactive rookie but this is my what I currently have working for my functional routes (since @ControllerAdvise won’t work for unless you’re using the annotated programming model)

  @Bean
  WebFilter addCsrfToken() {
    return (exchange, next) -> exchange
        .<Mono<CsrfToken>>getAttribute(CsrfToken.class.getName())
        .doOnSuccess(token -> {}) // do nothing, just subscribe :/
        .then(next.filter(exchange));
  }

@rwinch could we re-open this and think about a more intuitive experience? See upvotes on https://github.com/spring-projects/spring-security/issues/5766#issuecomment-482805601 above (currently 10!) Thanks!

import com.crl.crlproxy.common.Constants
import mu.KotlinLogging
import org.springframework.http.ResponseCookie
import org.springframework.security.web.server.csrf.CsrfToken
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Mono
import java.time.Duration

private val logger = KotlinLogging.logger {}

@Component
class CsrfHelperFilter : WebFilter {

    override fun filter(serverWebExchange: ServerWebExchange,
                        webFilterChain: WebFilterChain): Mono<Void> {
        val key = CsrfToken::class.java.name
        val csrfToken: Mono<CsrfToken> = serverWebExchange.getAttribute(key) ?: Mono.empty()
        return csrfToken.doOnSuccess { token ->
            val cookie = ResponseCookie.from(Constants.CSRF_COOKIE_NAME, token.token)
                    .maxAge(Duration.ofHours(1))
                    .httpOnly(false)
                    .path("/")
                    .build()
            logger.debug { "Cookie: $cookie" }
            serverWebExchange.response.cookies.add(Constants.CSRF_COOKIE_NAME, cookie)
        }.then(webFilterChain.filter(serverWebExchange))
    }
}

Here is the Java Version just for reference:

import java.time.Duration;

import org.springframework.http.ResponseCookie;
import org.springframework.security.web.server.csrf.CsrfToken;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;

import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;

@Component
@Slf4j
public class CsrfHelperFilter implements WebFilter {

	@Override
	public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
		String key = CsrfToken.class.getName();
		Mono<CsrfToken> csrfToken = null != exchange.getAttribute(key) ? exchange.getAttribute(key) : Mono.empty();
		return csrfToken.doOnSuccess(token -> {
			ResponseCookie cookie = ResponseCookie.from("XSRF-TOKEN", token.getToken()).maxAge(Duration.ofHours(1))
					.httpOnly(false).path("/").build();
			log.debug("Cookie: {}", cookie);
			exchange.getResponse().getCookies().add("XSRF-TOKEN", cookie);
		}).then(chain.filter(exchange));
	}

}

Pardon our dust here as we do some issue cleanup. Feedback was already provided earlier, and I don’t think the ticket has been fully addressed, yet, so let’s keep the issue open.

I’m not sure I understand why the request shouldn’t be larger when I configure a cookie repo of anything (or the session bigger when I choose a session repo): when I configure a repo, it is to store something and, to me, it seems accepted that this something is going to use some space where it is stored.

In other words, why configuring a cookie repo if nothing is stored in cookies? When can it be of any use? What is the value of requiring an explicit subscription to the CSRF cookie, when I don’t have to subscribe to the session cookie for instance?

My usage of the the CSRF cookie repository is for JS applications (Angular, React, Vue, …). How am I supposed to “not eagerly” provide the CSRF token to such applications?

Thanks for the feedback. Generally speaking the reactive repositories won’t save the token unless something subscribed to the result. This is because if nothing subscribed, there is no way that the token could be known. Has anything subscribed to the CsrfToken? One option is to use something like this:

@ControllerAdvice
public class SecurityControllerAdvice {
	@ModelAttribute
	Mono<CsrfToken> csrfToken(ServerWebExchange exchange) {
		Mono<CsrfToken> csrfToken = exchange.getAttribute(CsrfToken.class.getName());
		return csrfToken.doOnSuccess(token -> exchange.getAttributes()
				.put(CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME, token));
	}
}

This also exposes the token to be automatically available for anything using Spring Security’s CsrfRequestDataValueProcessor which allows frameworks like Thymeleaf to automatically provide the CSRF token.

Hi, I’ve been trying devstartshop’s workaround but it doesn’t work properly. It creates an XSRF-TOKEN but the value never changes. The value of an XSRF-TOKEN should change for each request (or an option should at least allow that). Every query should provide a new value in response headers and the most recent will be used to call the next endpoint. If an xsrf token is session-scoped I really don’t see the added value with a user-session token.

this is really very unintuitive. i was also wondering why the cookie is not set. now i ended up here and i still dont see a clear intuitive solution in this conversation.

A solution in Kotlin that let the backend of my Webflux application work with Angular frontend, maybe someone find it useful:

import com.crl.crlproxy.common.Constants
import mu.KotlinLogging
import org.springframework.http.ResponseCookie
import org.springframework.security.web.server.csrf.CsrfToken
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Mono
import java.time.Duration

private val logger = KotlinLogging.logger {}

@Component
class CsrfHelperFilter : WebFilter {

    override fun filter(serverWebExchange: ServerWebExchange,
                        webFilterChain: WebFilterChain): Mono<Void> {
        val key = CsrfToken::class.java.name
        val csrfToken: Mono<CsrfToken> = serverWebExchange.getAttribute(key) ?: Mono.empty()
        return csrfToken.doOnSuccess { token ->
            val cookie = ResponseCookie.from(Constants.CSRF_COOKIE_NAME, token.token)
                    .maxAge(Duration.ofHours(1))
                    .httpOnly(false)
                    .path("/")
                    .build()
            logger.debug { "Cookie: $cookie" }
            serverWebExchange.response.cookies.add(Constants.CSRF_COOKIE_NAME, cookie)
        }.then(webFilterChain.filter(serverWebExchange))
    }
}

Additionally you need to register CookieServerCsrfTokenRepository with the corresponding cookie name. Important is CsrfToken class package.

Ok. Thanks for the response. I’m going to reopen the issue since we agree it would be nice for some sort of support for this. I still don’t know exactly how we will go about it yet.

Perhaps this does make sense to change the behavior of the cookie based implementation since a user can technically read the cookie directly. The session based implementation makes no sense to write it unless something subscribes because you cannot actually submit the value unless something is trying to use it.

I’m going to need to think about this change a bit.