Skip to content

GenerateOneTimeTokenWebFilter triggers double execution of the downstream WebFilterChain #16458

@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
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions