Spring-security: CookieServerCsrfTokenRepository does not add cookie

Created on 4 Sep 2018  路  23Comments  路  Source: spring-projects/spring-security

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

waiting-for-feedback

Most helpful comment

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.

All 23 comments

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.

@rwinch I can confirm that the Mono<CsrfToken> returned by generateToken method in the repository is never subscribed to (and therefore createCsrfToken is never called). I was under the assumption that configuring the tokenrepository would basically add the CSRF cookie to any request automatically.

I don't mind adding the controller advice but perhaps it would be good to clarify this in the documentation? In general there's no real documentation about how to obtain the CSRF token, which is why I assumed it would be added to any response.

Edit: On second thought, isn't the whole idea of the CookieServerCsrfTokenRepository that it would add the CSRF token to the response, without requiring a controller advice or additional setup? The Javadoc even says: "A ServerCsrfTokenRepository that persists the CSRF token in a cookie named "XSRF-TOKEN" and reads from the header "X-XSRF-TOKEN" following the conventions of AngularJS".

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.

What could perhaps work if one could configure a CSRF URL which you can GET and which returns an empty response, apart from whatever is needed by CSRF. In this case this would be a cookie, in other TokenRepositories it could be a response header, etc

I'm not sure I understand the suggestion. User's can already provide a URL that subscribes to the CSRF token which would write it out, so this would be a nothing to change.

Something else you could do is to create a ServerCsrfTokenRepository that delegates to another implementation and subscribes on creation of the token. This would ensure that the value is saved eagerly.

Ah, I was unaware that this URL could already be configured. I didn't see anything in CsrfSpec that would allow me to set this.

Anyway, for now I will create the customized TokenRepository, that should definitely work. That's a satisfactory solution for now, but I'm still finding myself a bit confused that just enabling the CookieServerCsrfTokenRepository doesn't (without any additional configuration) simply add the token to a response cookie.

I think of the custom repository as more of a work around.

In regards to the URL, I'm just speaking of something like the Controller Advice I provided. you could do something similar with a specific URL that resolves the token for you. This is not something Spring Security needs to do for you.

Sorry, that's what I meant as well: The workaround is fine for now. If in the future the behaviour is changed to make this functionality a bit more intuitive (well...at least to the likes of me :D) then that would certainly be welcomed.

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.

For a RESTful service I implemented this as a ResponseBodyResultHandler.

public class ServerCsrfTokenSubscribingResponseModifier extends ResponseBodyResultHandler {
    public ServerCsrfTokenSubscribingResponseModifier(List<HttpMessageWriter<?>> writers, RequestedContentTypeResolver resolver, ReactiveAdapterRegistry registry) {
        super(writers, resolver, registry);
        setOrder(99);
    }

    @Override
    public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
        return Optional.ofNullable(exchange.getAttribute(CsrfToken.class.getName()))
            .filter(Mono.class::isInstance)
            .map(Mono.class::cast)
            .orElseGet(Mono::empty)
            .then(super.handleResult(exchange, result));
    }
}

I agree though it would be much better for this to work out of the box.

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.

The non-reactive version of the csrf filter generates the token even if the request doesn't match requireCsrfProtectionMatcher. Shouldn't it be done the same way on the reactive part ?

@rwinch I am a new spring user. How would I trigger a subscription when using functional end points. I believe @Controlleradvice will not work with Webflux. Or how can I retrieve the csrf token using router functions?
Will the spring security team provide a new tutorial to spring security + angular 7+?

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.

I am in the same boat. I have been trying for more than a day now to find a solution that works to includes CSRF information for ajax requests from a non-templated front end, and using functional routing.

There doesn't seem to be a good example or pattern, though I'm about to give @plewand 's CsrfHelperFilter a try.

@plewand 's solution did finally result in my having access to a CSRF token in a cookie for my frontend, though I'm still working out having Spring Security actually accept it on a subsequent POST request.

The workaround works fine, even if you don't explicitly set the cookie yourself. Just subscribing is enough, i.e. (Kotlin, sorry):

csrfToken.doOnSuccess { }.then(webFilterChain.filter(serverWebExchange))

Why not implementing the same behavior in Webflux as in Webmvc ?

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!

I spent about 3 hours debugging the source code trying to understand why no cookie was set until i stumbled upon this issue. This seems still be the case, there should be some form of documentation about this in the official documentation.

This should be fixed in general, as it is counter intuitive.

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

}

This is the big WebFlux and MVC difference.
I wasted a lot of time debugging the reason why CSRF tokens were not found in the cookie.

Was this page helpful?
0 / 5 - 0 ratings