Describe the bug

I read #7719 and added a SubscribeCsrfTokenWebFilter that actively subscribe Mono<CsrfToken>. However, if Mono<CsrfToken> is subscribed to after going through the SubscribeCsrfTokenWebFilter and during view rendering, the CSRF token will regenerated.

To Reproduce

The following code can reproduce the problem.

@Bean
    public WebFilter subscribeCsrfTokenWebFilter() {
        return ((exchange, chain) -> {
            Mono<CsrfToken> csrfToken = exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty());

            CsrfToken token = csrfToken.block(); // Generate CsrfToken.

            return csrfToken // Generate new CsrfToken!
                  .then(chain.filter(exchange));
        });
    }

If the ServerHttpRequest did not have a CSRF token, the Each time Mono<CsrfToken> is subscribed, the repository generates and stores a token.

Expected behavior

The CSRF token should be generated only once during a single request.

Sample

See To Reproduce

Details

The CsrfWebFilter stores Mono<CsrfToken> in ServerWebExchange attributes that executes ServerCsrfTokenRepository#generateToken(ServerWebExchange ) and ServerCsrfTokenRepository#saveToken(ServerWebExchange , CsrfToken) when the request did not have a CSRF token.

https://github.com/spring-projects/spring-security/blob/dbce9b5b66b6a8395b217c58e349dca3379accd1/web/src/main/java/org/springframework/security/web/server/csrf/CsrfWebFilter.java#L159-L174

This Mono<CsrfToken> repeats the generation and saving of a new token for each subscription. CookieCsrfTokenRepository adds Set-Cookie: XSRF-TOKEN=... to the response header as many times as it is called. Set-Cookie should be an override action, so all but the last CSRF token subscribed to in the application will be disabled.

Comment From: tt4g

I believe that adding Mono#cache() to CsrfWebFilter#generateToken(ServerWebExchange) will get around this problem. Is there any reason why this can't be done?

    private Mono<CsrfToken> generateToken(ServerWebExchange exchange) {
        return this.csrfTokenRepository.generateToken(exchange)
-            .delayUntil((token) -> this.csrfTokenRepository.saveToken(exchange, token));
+            .delayUntil((token) -> this.csrfTokenRepository.saveToken(exchange, token))
+            .cache();
    }

Comment From: jzheaux

Hi, @tt4g, thanks for the suggestion. Would you be able to submit a PR along those lines, including a test?

Comment From: tt4g

PR #9760