Skip to content

GenerateOneTimeTokenWebFilter triggers double execution of the downstream WebFilterChain #16458

Closed
@Kehrlann

Description

@Kehrlann

Describe the bug

Using one-time token login in reactive mode, with the simplest possible configuration, any GET call to / (curl http://localhost:8080/) or any non-spring-security-managed endpoint prints the following message to the console:

2025-01-21T15:25:12.601+01:00 ERROR 54059 --- [     parallel-7] o.s.w.s.adapter.HttpWebHandlerAdapter    : [5e1343e8-3] Error [java.lang.UnsupportedOperationException] for HTTP GET "/", but ServerHttpResponse already committed (200 OK)

But no error or stack-trace.

Reactive stack trace in HttpWebHandlerAdapter:

java.lang.UnsupportedOperationException
	at org.springframework.http.ReadOnlyHttpHeaders.set(ReadOnlyHttpHeaders.java:112)
	Suppressed: The stacktrace has been enhanced by Reactor, refer to additional information below: 
Assembly trace from producer [reactor.core.publisher.MonoFlatMap] :
	reactor.core.publisher.Mono.flatMap(Mono.java:3179)
	org.springframework.http.codec.EncoderHttpMessageWriter.write(EncoderHttpMessageWriter.java:134)
Error has been observed at the following site(s):
	*________Mono.flatMap ⇢ at org.springframework.http.codec.EncoderHttpMessageWriter.write(EncoderHttpMessageWriter.java:134)
	|_   Mono.doOnDiscard ⇢ at org.springframework.http.codec.EncoderHttpMessageWriter.write(EncoderHttpMessageWriter.java:140)
	|_         checkpoint ⇢ Handler wf.garnier.experiments.ott.OttApplication$OttController#index() [DispatcherHandler]
	|_ Mono.onErrorResume ⇢ at org.springframework.web.reactive.DispatcherHandler.lambda$handleResultMono$7(DispatcherHandler.java:176)
	*________Mono.flatMap ⇢ at org.springframework.web.reactive.DispatcherHandler.lambda$handleResultMono$6(DispatcherHandler.java:177)
	*________Mono.flatMap ⇢ at org.springframework.web.reactive.DispatcherHandler.handleResultMono(DispatcherHandler.java:172)
	*________Mono.flatMap ⇢ at org.springframework.web.reactive.DispatcherHandler.handle(DispatcherHandler.java:154)
	*__________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:106)
	*__________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:106)
	*___________Mono.then ⇢ at org.springframework.security.web.server.authentication.logout.LogoutWebFilter.filter(LogoutWebFilter.java:63)
	*__Mono.switchIfEmpty ⇢ at org.springframework.security.web.server.authentication.logout.LogoutWebFilter.filter(LogoutWebFilter.java:63)
	|_           Mono.map ⇢ at org.springframework.security.web.server.authentication.logout.LogoutWebFilter.filter(LogoutWebFilter.java:64)
	|_       Mono.flatMap ⇢ at org.springframework.security.web.server.authentication.logout.LogoutWebFilter.filter(LogoutWebFilter.java:65)
	|_       Mono.flatMap ⇢ at org.springframework.security.web.server.authentication.logout.LogoutWebFilter.filter(LogoutWebFilter.java:66)
	|_         checkpoint ⇢ org.springframework.security.web.server.authentication.logout.LogoutWebFilter [DefaultWebFilterChain]
	*__________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:106)
	*________Mono.flatMap ⇢ at org.springframework.security.web.server.savedrequest.ServerRequestCacheWebFilter.filter(ServerRequestCacheWebFilter.java:41)
	|_         checkpoint ⇢ org.springframework.security.web.server.savedrequest.ServerRequestCacheWebFilter [DefaultWebFilterChain]
	*__________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:106)
	|_         checkpoint ⇢ org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter [DefaultWebFilterChain]
	*__________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:106)
	*___________Mono.then ⇢ at org.springframework.security.web.server.ui.OneTimeTokenSubmitPageGeneratingWebFilter.filter(OneTimeTokenSubmitPageGeneratingWebFilter.java:56)
	*__Mono.switchIfEmpty ⇢ at org.springframework.security.web.server.ui.OneTimeTokenSubmitPageGeneratingWebFilter.filter(OneTimeTokenSubmitPageGeneratingWebFilter.java:56)
	|_       Mono.flatMap ⇢ at org.springframework.security.web.server.ui.OneTimeTokenSubmitPageGeneratingWebFilter.filter(OneTimeTokenSubmitPageGeneratingWebFilter.java:57)
	|_         checkpoint ⇢ org.springframework.security.web.server.ui.OneTimeTokenSubmitPageGeneratingWebFilter [DefaultWebFilterChain]
	*__________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:106)
	*___________Mono.then ⇢ at org.springframework.security.web.server.authentication.ott.GenerateOneTimeTokenWebFilter.filter(GenerateOneTimeTokenWebFilter.java:64)
	*__Mono.switchIfEmpty ⇢ at org.springframework.security.web.server.authentication.ott.GenerateOneTimeTokenWebFilter.filter(GenerateOneTimeTokenWebFilter.java:64)
	|_       Mono.flatMap ⇢ at org.springframework.security.web.server.authentication.ott.GenerateOneTimeTokenWebFilter.filter(GenerateOneTimeTokenWebFilter.java:65)
	|_       Mono.flatMap ⇢ at org.springframework.security.web.server.authentication.ott.GenerateOneTimeTokenWebFilter.filter(GenerateOneTimeTokenWebFilter.java:66)
	|_         checkpoint ⇢ org.springframework.security.web.server.authentication.ott.GenerateOneTimeTokenWebFilter [DefaultWebFilterChain]
	*__________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:106)
	*___________Mono.then ⇢ at org.springframework.security.web.server.authentication.AuthenticationWebFilter.filter(AuthenticationWebFilter.java:114)
	*__Mono.switchIfEmpty ⇢ at org.springframework.security.web.server.authentication.AuthenticationWebFilter.filter(AuthenticationWebFilter.java:114)
	|_       Mono.flatMap ⇢ at org.springframework.security.web.server.authentication.AuthenticationWebFilter.filter(AuthenticationWebFilter.java:115)
	|_ Mono.onErrorResume ⇢ at org.springframework.security.web.server.authentication.AuthenticationWebFilter.filter(AuthenticationWebFilter.java:116)
	|_         checkpoint ⇢ org.springframework.security.web.server.authentication.AuthenticationWebFilter [DefaultWebFilterChain]
	*__________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:106)
	|_  Mono.contextWrite ⇢ at org.springframework.security.web.server.context.ReactorContextWebFilter.filter(ReactorContextWebFilter.java:48)
	|_         checkpoint ⇢ org.springframework.security.web.server.context.ReactorContextWebFilter [DefaultWebFilterChain]
	*__________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:106)
	*__________Mono.defer ⇢ at org.springframework.security.web.server.csrf.CsrfWebFilter.continueFilterChain(CsrfWebFilter.java:148)
	*___________Mono.then ⇢ at org.springframework.security.web.server.csrf.CsrfWebFilter.filter(CsrfWebFilter.java:126)
	*__Mono.switchIfEmpty ⇢ at org.springframework.security.web.server.csrf.CsrfWebFilter.filter(CsrfWebFilter.java:126)
	|_ Mono.onErrorResume ⇢ at org.springframework.security.web.server.csrf.CsrfWebFilter.filter(CsrfWebFilter.java:127)
	|_         checkpoint ⇢ org.springframework.security.web.server.csrf.CsrfWebFilter [DefaultWebFilterChain]
	*__________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:106)
	|_         checkpoint ⇢ org.springframework.security.web.server.header.HttpHeaderWriterWebFilter [DefaultWebFilterChain]
	*__________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:106)
	|_  Mono.contextWrite ⇢ at org.springframework.security.config.web.server.ServerHttpSecurity$ServerWebExchangeReactorContextWebFilter.filter(ServerHttpSecurity.java:4047)
	|_         checkpoint ⇢ org.springframework.security.config.web.server.ServerHttpSecurity$ServerWebExchangeReactorContextWebFilter [DefaultWebFilterChain]
	*__________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:106)
	*________Mono.flatMap ⇢ at org.springframework.security.web.server.WebFilterChainProxy.filterFirewalledExchange(WebFilterChainProxy.java:78)
	*________Mono.flatMap ⇢ at org.springframework.security.web.server.WebFilterChainProxy.filter(WebFilterChainProxy.java:65)
	|_ Mono.onErrorResume ⇢ at org.springframework.security.web.server.WebFilterChainProxy.filter(WebFilterChainProxy.java:66)
	|_         checkpoint ⇢ org.springframework.security.web.server.WebFilterChainProxy [DefaultWebFilterChain]
	*__________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:106)
	|_     Mono.doOnError ⇢ at org.springframework.web.server.handler.ExceptionHandlingWebHandler.handle(ExceptionHandlingWebHandler.java:84)
	|_ Mono.onErrorResume ⇢ at org.springframework.web.server.handler.ExceptionHandlingWebHandler.handle(ExceptionHandlingWebHandler.java:85)
	|_     Mono.doOnError ⇢ at org.springframework.web.server.handler.ExceptionHandlingWebHandler.handle(ExceptionHandlingWebHandler.java:84)
	|_ Mono.onErrorResume ⇢ at org.springframework.web.server.handler.ExceptionHandlingWebHandler.handle(ExceptionHandlingWebHandler.java:85)
	|_     Mono.doOnError ⇢ at org.springframework.web.server.handler.ExceptionHandlingWebHandler.handle(ExceptionHandlingWebHandler.java:84)
	|_ Mono.onErrorResume ⇢ at org.springframework.web.server.handler.ExceptionHandlingWebHandler.handle(ExceptionHandlingWebHandler.java:85)
	|_   Mono.doOnSuccess ⇢ at org.springframework.web.server.adapter.HttpWebHandlerAdapter.handle(HttpWebHandlerAdapter.java:299)
	*__________Mono.error ⇢ at org.springframework.web.server.handler.ExceptionHandlingWebHandler$CheckpointInsertingHandler.handle(ExceptionHandlingWebHandler.java:106)
	|_         checkpoint ⇢ HTTP GET "/" [ExceptionHandlingWebHandler]
	*__________Mono.error ⇢ at org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler.handle(AbstractErrorWebExceptionHandler.java:293)
	*__________Mono.error ⇢ at org.springframework.web.server.handler.ResponseStatusExceptionHandler.handle(ResponseStatusExceptionHandler.java:68)

To Reproduce

Simple project with spring-webflux + spring-security, and the simplest possible OTT configuration:

@Bean
public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
    return http
            .oneTimeTokenLogin(ott -> ott.tokenGenerationSuccessHandler(new ServerRedirectOneTimeTokenGenerationSuccessHandler("/login/ott")))
            .build();
}

the try to reach any non-spring security endpoint, e.g. curl http://localhost:8080/

Analysis

GenerateOneTimeTokenWebFilter triggers a double execution of the filter chain through two .switchIfEmpty(chain.filter(exchange).then(Mono.empty())).

The following configuration:

@Bean
public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) throws Exception {
    return http
            .oneTimeTokenLogin(ott -> ott.tokenGenerationSuccessHandler(new ServerRedirectOneTimeTokenGenerationSuccessHandler("/login/ott")))
            .addFilterAfter(new LoggingFilter(), SecurityWebFiltersOrder.ONE_TIME_TOKEN)
            .build();
}

public static class LoggingFilter implements WebFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        return chain.filter(exchange)
                .doOnSuccess((x) -> {
                    System.out.println("----------> Logging Filter, success");
                })
                .doOnError((x) -> {
                    System.out.println("----------> Logging Filter, error");
                });
    }
}

Prints in the console:

----------> Logging Filter, success
----------> Logging Filter, error

Using .addFilterBefore(new LoggingFilter(), SecurityWebFiltersOrder.ONE_TIME_TOKEN) instead of registering it after only prints the success case.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions