Summary

I'm trying to authenticate against specific header in request. My Security config is as follows:

@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfiguration {

    private static final String[] AUTH_WHITELIST = {
            // swagger ui:
            "/swagger-ui.html",
            "/swagger-resources",
            "/swagger-resources/**",
            "/v2/api-docs"
     };

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(final ServerHttpSecurity http,
            final AuthenticationWebFilter authenticationWebFilter,
            final UnauthenticatedEntryPoint entryPoint) {
        http
            .exceptionHandling()
                .authenticationEntryPoint(entryPoint)
            .and()
            .addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
            .authorizeExchange()
                .pathMatchers(AUTH_WHITELIST).permitAll()
                .anyExchange().authenticated()
            .and()
            .httpBasic().disable()
            .formLogin().disable()
            .csrf().disable()
            .logout().disable();
        return http.build();
    }

    @Bean
    public AuthenticationWebFilter authenticationWebFilter(final ReactiveAuthenticationManager authenticationManager,
            final AuthenticationConverter converter,
            final UnauthenticatedEntryPoint entryPoint,
            final HeadersExchangeMatcher matcher) {
        final AuthenticationWebFilter filter = new AuthenticationWebFilter(authenticationManager);
        filter.setAuthenticationConverter(converter);
        filter.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
        filter.setRequiresAuthenticationMatcher(matcher);

        return filter; 
    }
}

Actual Behavior

Every time I make request to the resource that should be protected, both AuthenticationWebFilter#filer and AuthenticationManager#authenticate are called twice in a row. If I comment out .anyExchange().authenticated() - they're called once as expected

Expected Behavior

.anyExchange().authenticated() should not cause AuthenticationManager#authenticate to be called twice.

Version

5.0.5.RELEASE

Comment From: rwinch

Are you using Spring Boot? If so any Filter exposed as a Bean is also registered with the servlet container. You might also post more details on how to reproduce this since AuthenticationWebFilter is a custom filter that we cannot review the code for

Comment From: eximius313

1) Yes, I'm using SpringBoot, but the filter is separate class annotated as @Component. I made it @Bean just in example code 2) I put a breakpoint on http.build() and listed ServerHttpSecurity#webFilters, my filter is there only once: [OrderedWebFilter{webFilter=org.springframework.security.web.server.header.HttpHeaderWriterWebFilter@7fa65318, order=100}, OrderedWebFilter{webFilter=org.springframework.security.web.server.context.ReactorContextWebFilter@75679dff, order=300}, OrderedWebFilter{webFilter=com.example.authorization.AuthenticationWebFilter@11f0552c, order=600}, OrderedWebFilter{webFilter=org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter@61ba2ebc, order=900}, OrderedWebFilter{webFilter=org.springframework.security.web.server.savedrequest.ServerRequestCacheWebFilter@2cc98f00, order=1000}, OrderedWebFilter{webFilter=org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter@65886703, order=1200}, OrderedWebFilter{webFilter=org.springframework.security.web.server.authorization.AuthorizationWebFilter@76c217ab, order=1300}] 3. The whole code for AuthenticationWebFilter is this:

@Component
public final class AuthenticationWebFilter extends org.springframework.security.web.server.authentication.AuthenticationWebFilter {

    public AuthenticationWebFilter(final ReactiveAuthenticationManager authenticationManager,
            final AuthenticationConverter converter,
            final UnauthenticatedEntryPoint entryPoint,
            final HeadersExchangeMatcher matcher) {
        super(authenticationManager);
        setAuthenticationConverter(converter);
        setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
        setRequiresAuthenticationMatcher(matcher);
    }
}

Comment From: eximius313

@rwinch I've prepared sample application for you: SpringSecurityReactiveSample.zip

Just run Postman/Curl and make GET request to /test with header Token set to test and observe console output

By the way - I'd have very kind request to you: this piece of code is result of me trying to understand Spring Security documentation, so if you notice anything that is not "Spring way" of doing it - please let me know.

Comment From: taerimmk

@eximius313

here is my code

//@Component <---- remove bean public class JwtAuthenticationWebFilter extends AuthenticationWebFilter { .............

//SecurityConfig.java

public SecurityWebFilterChain springSecurityFilterChain(
        ServerHttpSecurity http,
        JwtReactiveAuthenticationManager authenticationManager,
        JwtAuthenticationConverter converter,
        UnauthorizedAuthenticationEntryPoint entryPoint

) {

    http
            .exceptionHandling()
            .and()
            .addFilterAt(new JwtAuthenticationWebFilter(authenticationManager, converter, entryPoint)
                    , SecurityWebFiltersOrder.AUTHENTICATION)
            .authorizeExchange()
            .pathMatchers(AUTH_WHITELIST).permitAll()
            .anyExchange().authenticated()
            .and()
            .httpBasic().disable()
            .formLogin().disable()
            .csrf().disable()
            .logout().disable();
    return http.build();
}

Comment From: eximius313

@taerimmk, it works indeed, but why? I debugged the code and verified that webFilters list is exactly the same in both solutions: [OrderedWebFilter{webFilter=org.springframework.security.web.server.header.HttpHeaderWriterWebFilter@6f410de3, order=100}, OrderedWebFilter{webFilter=org.springframework.security.web.server.context.ReactorContextWebFilter@8d04dbc, order=300}, OrderedWebFilter{webFilter=showcase.authentication.AuthenticationWebFilter@1360b1d9, order=600}, OrderedWebFilter{webFilter=org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter@538c4daf, order=900}, OrderedWebFilter{webFilter=org.springframework.security.web.server.savedrequest.ServerRequestCacheWebFilter@34457c28, order=1000}, OrderedWebFilter{webFilter=org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter@3e727ab1, order=1200}, OrderedWebFilter{webFilter=org.springframework.security.web.server.authorization.AuthorizationWebFilter@288aeadf, order=1300}]

I think this is not correct behavior

Comment From: rwinch

@Component creates it as a Bean when classpath scanning is added. You can reject it from being added by using FilterRegistrationBean. See https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-disable-registration-of-a-servlet-or-filter

Comment From: eximius313

But where it resides if webFilters is not changed? And why it magically works if this line: .anyExchange().authenticated() is being commented out?

Comment From: ivpal

@eximius313

here is my code

//@component <---- remove bean public class JwtAuthenticationWebFilter extends AuthenticationWebFilter { .............

//SecurityConfig.java

``` public SecurityWebFilterChain springSecurityFilterChain( ServerHttpSecurity http, JwtReactiveAuthenticationManager authenticationManager, JwtAuthenticationConverter converter, UnauthorizedAuthenticationEntryPoint entryPoint

) {

http
        .exceptionHandling()
        .and()
        .addFilterAt(new JwtAuthenticationWebFilter(authenticationManager, converter, entryPoint)
                , SecurityWebFiltersOrder.AUTHENTICATION)
        .authorizeExchange()
        .pathMatchers(AUTH_WHITELIST).permitAll()
        .anyExchange().authenticated()
        .and()
        .httpBasic().disable()
        .formLogin().disable()
        .csrf().disable()
        .logout().disable();
return http.build();

} ```

I have some problem. Remove @Component fix it.

Comment From: eximius313

@rwinch what kind of feedback regarding waiting-for-feedback label do you require?

Comment From: jhamberg

To anyone else who finds their way here because their WebFilter (e.g. AuthenticationWebFilter) is being called twice:

Spring Boot automatically registers any filters marked as a @Bean or @Component which means that if you want more precise control—like with addFilterAt—you should remove the annotation.

I'm only restating here what @rwinch and others have said in an attempt to make the information a bit more available through search engines.

Comment From: rwinch

@jhamberg I'd work with the Spring Boot team on that since it is Spring Boot that adds the WebFilters automatically