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