vuk
vuk

Reputation: 91

AbstractAuthenticationProcessingFilter triggered no matter what SecurityFilterChain it is a part of

I am experimenting with spring security and came across a strange behavior. My idea is to create a security filter that authenticates requests based on JWT (or JWS) tokens:

public class JWTokenFilter extends AbstractAuthenticationProcessingFilter {

    public JWTokenFilter(AuthenticationManager authenticationManager) {
        super("/**"); //doesn't have any effect, every request still gets considered by this filter
        setAuthenticationManager(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        String token = request.getHeader("Authorization");
        if (!StringUtils.hasText(token)) {
            throw new TokenException("Token is empty");
        }
        var authentication = determineAuthentication(token.replace("Bearer","").trim());
        //the AbstractAuthenticationProcessingFilter fills the Security context
        return this.getAuthenticationManager().authenticate(authentication);
    }

    @Override
    protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
        System.out.println("Asked for "+request.getRequestURI());
        return request.getHeader("Authorization") != null;
    }


    private TokenAuthentication<UserInfo> determineAuthentication(String token) {
        var split = token.split("\\.");
        if (split.length < 2 || split.length > 3) {
            throw new TokenException("Token malformed");
        }
        if (split.length == 2){
            return new JWTAuthentication<>(token);
        }else {
            return new JWSAuthentication<>(token);
        }
    }

}

I have 3 @RestController classes which have their paths mapped:

  1. @RequestMapping("/admin")
  2. @RequestMapping("/all")
  3. @RequestMapping("/anon")

Along with this, I have the following security configuration:

@Configuration
@Order(98)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.requestMatchers()
                .antMatchers("/all/**","/anon/**")
                .and()
                .authorizeRequests().antMatchers("/all/**").permitAll()
                .and()
                .authorizeRequests().antMatchers("/anon/**").anonymous();
    }

    @Override
    public void configure(WebSecurity web) {
        web.ignoring().mvcMatchers("/webjars/**", "/css/**");
    }



    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    @Configuration
    @Order(99)
    public static class TokenSecurityConfig extends WebSecurityConfigurerAdapter{

        @Lazy
        @Autowired
        private JWTokenFilter tokenFilter;

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests() //having /admin/** or /** makes no difference
                    .anyRequest().authenticated()
                    .and().addFilterBefore(tokenFilter,ExceptionTranslationFilter.class);//put this filter near the end of the chain
        }

        @Bean
        public JWTokenFilter tokenFilter(JWTokenAuthenticationProvider jwTokenAuthenticationProvider,JWSTokenAuthenticationProvider jwsTokenAuthenticationProvider){
            var list = new ArrayList<AuthenticationProvider>();
            list.add(jwsTokenAuthenticationProvider);
            list.add(jwTokenAuthenticationProvider);
            ProviderManager manager = new ProviderManager(list);
            return new JWTokenFilter(manager);
        }
    }
}

From this configuration here we can see that there are 2 SecurityFilterChans (not counting the /webjars and /css ones):

  1. That matches all requests for "/all/**" and "/anon/**" REST routes
  2. That matches any request

Since the 1. chain has lower @Order(98), than 2. @Order(99), that means that the 1. chain will be considered first which is shown by the debugger, enter image description here

and matched if the incoming request looks like:

curl --request GET \
  --url http://localhost:8080/all/hello \

Now what I am experiencing is that the JWTokenFilter method boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) is always called no matter the request path ! And in the console output, I can find Asked for /all/hello.

Edit: My spring boot version is 2.3.6.RELEASE


My question is:

Why is the JWTokenFIlter even asked if it should authenticate requests with paths that are not matched by the SecurityFilterChain it is a part of?

Upvotes: 0

Views: 1192

Answers (1)

Rob Winch
Rob Winch

Reputation: 21720

I believe I have a better answer, but I wanted to answer your original question as well. I split this into two sections.

Improved Answer

I realize this doesn't answer the original question, but I think you may be better off using the built in support for JWT based authentication. I'd check out the OAuth 2.0 Resource Server section of the reference documentation.

Answer to Original Question

Spring Boot will automatically register any Filter exposed as a @Bean for every request directly with the Servlet Container.

You have two options that I see. The first is to avoid exposing the JwtTokenFilter as a @Bean.

@Configuration
@Order(99)
public static class TokenSecurityConfig extends WebSecurityConfigurerAdapter{
    @Autowired
    JWTokenAuthenticationProvider jwTokenAuthenticationProvider;

    @Autowired
    JWSTokenAuthenticationProvider jwsTokenAuthenticationProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests() //having /admin/** or /** makes no difference
                .anyRequest().authenticated()
                .and().addFilterBefore(tokenFilter(),ExceptionTranslationFilter.class);//put this filter near the end of the chain
    }

    
    public JWTokenFilter tokenFilter(){
        var list = new ArrayList<AuthenticationProvider>();
        list.add(jwsTokenAuthenticationProvider);
        list.add(jwTokenAuthenticationProvider);
        ProviderManager manager = new ProviderManager(list);
        return new JWTokenFilter(manager);
    }
}

Alternatively, you can continue exposing JwtTokenFilter as a @Bean and create a FilterRegistrationBean that disables registration.

@Bean
public FilterRegistrationBean registration(JWTokenFilter filter) {
    FilterRegistrationBean registration = new FilterRegistrationBean(filter);
    registration.setEnabled(false);
    return registration;
}

Upvotes: 2

Related Questions