Skip to content

GenerateOneTimeTokenWebFilter triggers double execution of the downstream WebFilterChain #16458

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Kehrlann opened this issue Jan 21, 2025 · 1 comment
Assignees
Labels
status: duplicate A duplicate of another issue type: bug A general bug

Comments

@Kehrlann
Copy link
Contributor

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.

@Kehrlann Kehrlann added status: waiting-for-triage An issue we've not yet triaged type: bug A general bug labels Jan 21, 2025
Kehrlann added a commit to Kehrlann/spring-security that referenced this issue Jan 21, 2025
Kehrlann added a commit to Kehrlann/spring-security that referenced this issue Jan 21, 2025
@rwinch rwinch closed this as completed in bb8e757 Jan 22, 2025
@rwinch rwinch self-assigned this Jan 22, 2025
@rwinch rwinch added status: duplicate A duplicate of another issue and removed status: waiting-for-triage An issue we've not yet triaged labels Jan 22, 2025
@rwinch
Copy link
Member

rwinch commented Jan 22, 2025

Thanks for the detailed writeup. Closing this as duplicate of the PR you created gh-16459

kwondh5217 pushed a commit to kwondh5217/spring-security that referenced this issue Feb 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: duplicate A duplicate of another issue type: bug A general bug
Projects
None yet
Development

No branches or pull requests

2 participants