Home > Software design >  How to configure two security configs with two filters in spring boot correctly?
How to configure two security configs with two filters in spring boot correctly?

Time:08-13

I've implmemented security in my spring boot microservices project, the requirment is to have two types of configurations, one for user request (from angular) and one from other services. The design is to use JWT token for user request and API key for system calls.

Here is the config file (one file) but have also try to split it to two files with no impact:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Configuration  
    @Order(1)
    public static class APISecurityConfig extends WebSecurityConfigurerAdapter {
        
        @Value("${my.api.key.header}") 
        private String principalRequestHeader;
        @Value("${my.api.key.token}") 
        private String principalRequestValue;
        
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception {
            httpSecurity
                .cors().disable().csrf().disable();         
            httpSecurity
            .antMatcher("/api/users/**")
            .authorizeRequests() //
                .anyRequest().authenticated() 
                .and()
            .addFilterBefore(new APIKeyAuthFilter(principalRequestHeader, principalRequestValue), UsernamePasswordAuthenticationFilter.class);                                                                   
        }
              
        
    }
    
    @Order(2)
    @Configuration
    public static class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
         
        @Autowired
        UserDetailsService userDetailsService;
            
        @Bean
        public AuthTokenFilter authenticationJwtTokenFilter() {
            return new AuthTokenFilter();
        }
        
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {      
            auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
        }
        
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
        
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
            
        @Override
          public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers("/api/users/**");
          }
        
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception {          
            httpSecurity
                .cors().disable().csrf().disable();         
            httpSecurity
             .authorizeRequests()
             .antMatchers("/users/UserEmailExist", "/users/User/Add", "/users/Authenticate",
                     "/users/User/ChangePassword")
             .permitAll() 
             .and()                                     
             .authorizeRequests()            
             .antMatchers("/users/**").hasAnyRole(ROLE_ADMIN_USER, ROLE_MANAGER_USER)   
             .anyRequest().authenticated()                        
             .and()
             .addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);      
        }
        
    }
    
}

Each config has a filter attached to it, here the api one:

public class APIKeyAuthFilter extends GenericFilterBean  {
    
    private String principalRequestHeader;  
    private String principalRequestValue;
        
    public APIKeyAuthFilter(String principalRequestHeader, String principalRequestValue) {
        super();
        this.principalRequestHeader = principalRequestHeader;
        this.principalRequestValue = principalRequestValue;
    }
            
     @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                throws IOException, ServletException {
         
            if(request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
                String apiKey = getApiKey((HttpServletRequest) request);
                if(apiKey != null) {
                    if(apiKey.equals(principalRequestValue)) {
                        ApiKeyAuthenticationToken apiToken = new ApiKeyAuthenticationToken(apiKey, AuthorityUtils.NO_AUTHORITIES);
                        SecurityContextHolder.getContext().setAuthentication(apiToken);
                    } else {
                        HttpServletResponse httpResponse = (HttpServletResponse) response;
                        httpResponse.setStatus(401);
                        httpResponse.getWriter().write("Invalid API Key");
                        return;
                    }
                }
            }
            
            chain.doFilter(request, response);
            
        }

}

Here is the filter for jwt (normal user from angular):

public class AuthTokenFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private MyUserDetailsService userDetailsService;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        try {
            String jwt = parseJwt(request);
            if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
                String username = jwtUtils.getUserNameFromJwtToken(jwt);

                MSUserDetails userDetails = userDetailsService.loadUserByUsername(username);
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception e) {
            logger.error("Cannot set user authentication: {}", e);
        }

        filterChain.doFilter(request, response);

    }

}

I've created two different controllers, one with prefix /api/users and second /users. Here is what happen in two different scenarios:

  1. The user login from Angular, get jwt token and process request which end up in the Jwt filter, this scenarion looking good with no issues as the user is able to process request as long he is authenticate.
  2. Microservice send a request with api-key to url with /api/users prefix, it ended up on the same filter the normal user ended which is not correct and without JWT token he is actually able to proceed to the controller and process the request without going to the correct filter.

The only solution I have is to have only one filter and process the header for api-key and jwt but it doesn't seem right. I've looked online and try to figure out what I'm doing wrong but no clue as of now.

CodePudding user response:

Because the AuthTokenFilter is instantiated with @Bean, which causes the filter to be added to the ApplicationFilterChain, after the APIKeyAuthFilter is processed, it can also enter the AuthTokenFilter.

CodePudding user response:

An update on this issue so I hope it will help to the community.

Firstly, I removed the following code and this mainly fix the problem:

//      @Override
//        public void configure(WebSecurity web) throws Exception {
//          web.ignoring().antMatchers("/api/users/**");
//        }

The way the solution work as a whole is that the first configuration @Order(1) you define .antMatcher which means the configuration will work only for urls that match the prefix. So now, scenario 1. User from Angular go the the JWT filter only. scenario 2. API user will lend in the API filter first! But once it's done (After succesfull authentication) it still continue to the JWT filter but becuase it doesn't have JWT the filter not doing anything.

I would like to avoid to other filter in case of API call but the solution work, problem solved. I must say that security in spring boot is the most complex I came across so far from other features.

  • Related