Search code examples
spring-bootspring-securityjwtmicroservicesspring-cloud-config

Spring boot Security custom filter does not invoke


I am developing an application following microservice architecture using spring boot 3.2.0 and Java 17. I have one service - AuthService for authentication purpose. Users' login request will be routed by API gateway to AuthService and get a JWT token upon providing valid user credentials.

Now, user has a valid JWT token. By putting this token in request header in Bearer token format user can make http request to any other services (i.e. employeeService, hospitalService..; i will refer these services as OtherService). I don't want to burden OtherService with the token validation codes. Rather, there is an API endpoint at AuthService -> /auth/validate. This endpoint will validate the jwt token and returns a cusotm UserDetails object.

In a nutshell, the workflow is something like this - if user want to see list of employees, first user will provide username, password. This will be validated by AuthService and returns a JWT token. then, with this token user will make request to employeeService. There will be filter in EmployeeService which will check if there is any Authoriztion token in the request header and make rest call to AuthService to get a UserDetail object from it.

[P.S. I am new to spring security; I am not sure if this approach is ideal or not]

Bellow, I am providing the code portion of EmployeeService.

  1. ApplicationSecurity
@RefreshScope
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class ApplicationSecurity {

    private final AppSecurityFilter appSecurityFilter;


    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        return http
                .csrf(AbstractHttpConfigurer::disable)
                .cors(AbstractHttpConfigurer::disable)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterBefore(appSecurityFilter, UsernamePasswordAuthenticationFilter.class)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/health")
                        .permitAll()
                        .anyRequest()
                        .authenticated())
                .build();
    }
}

Note: I have not defined any AuthenticationManager or AuthenticationProvider bean here since I am not validating or getting user object from the db here in EmployeeService rather calling AuthService to do this from the AppSecurityFilter.

2. AppSecurityFilter

@AllArgsConstructor
@Component
public class AppSecurityFilter extends OncePerRequestFilter {

    private static String AUTH_SERVER_VALIDATE_TOKEN_URL = "http://AUTH-SERVICE/auth/api/v1/validate";

    private RestTemplate restTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        final String header = request.getHeader(AUTHORIZATION);

        if (isEmpty(header) || !header.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        ResponseEntity<AuthResponseDto> authResponse = // get userDetails form authService via restTeamplate post call

        UserResponseDto user = authResponse.getBody().getUserResponseDto();

        SecurityContextHolder.getContext().setAuthentication(
                new UsernamePasswordAuthenticationToken(
                        user,
                        null,
                        user.getAuthorities()
                )
        );

        filterChain.doFilter(request, response);
    }
}

So, the issue I am facing is; even though I set Authorization token in the request header; EmployeeService application redirects the request to spring's default login page. Furthermore, the AppSecurityFilter never invokes.

What I am missing here, and what are the necessary steps to make this work?


Solution

  • As @FabioFranco mentioned, I didn't miss anything in the security configuration. The problem was occurring due to @RefreshScope annotation added in the ApplicationSecurity class. (p.s. I didn't add this annotation in the question at first thinking that this wasn't related to the issue).

    Adding my findings here if someone stumble upon this type of issue in future.

    After investing the log, I found that if @RefreshScope is added then there were two separate logs of DefaultSecurityFilterChain at the time of application startup -

    line1: o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [
        DisableEncodeUrlFilter, 
        WebAsyncManagerIntegrationFilter,
        SecurityContextHolderFilter,
        HeaderWriterFilter,
        LogoutFilter,
        AppSecurityFilter,  <------------------- my custom filter added
        RequestCacheAwareFilter,
        SecurityContextHolderAwareRequestFilter,
        AnonymousAuthenticationFilter,
        SessionManagementFilter,
        ExceptionTranslationFilter,
        AuthorizationFilter
    ]
    
    line2: o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [
        DisableEncodeUrlFilter,
        WebAsyncManagerIntegrationFilter,
        SecurityContextHolderFilter,
        HeaderWriterFilter,
        CorsFilter,
        CsrfFilter,
        LogoutFilter,
        UsernamePasswordAuthenticationFilter,
        DefaultLoginPageGeneratingFilter,
        DefaultLogoutPageGeneratingFilter,
        BasicAuthenticationFilter,
        RequestCacheAwareFilter,
        SecurityContextHolderAwareRequestFilter,
        AnonymousAuthenticationFilter,
        ExceptionTranslationFilter,
        AuthorizationFilter
    ]
    

    As you can see, the immediate second line of the log doesn't have the custom filter I have configured.

    If I removed the @RefreshScope annotation from the class, there was only one line of log printed for DefaultSecurityFilterChain -

    DefaultSecurityFilterChain : Will secure any request with [
        DisableEncodeUrlFilter,
        WebAsyncManagerIntegrationFilter,
        SecurityContextHolderFilter,
        HeaderWriterFilter,
        LogoutFilter,
        AppSecurityFilter,  <------------------ my custom filter
        RequestCacheAwareFilter,
        SecurityContextHolderAwareRequestFilter,
        AnonymousAuthenticationFilter,
        SessionManagementFilter,
        ExceptionTranslationFilter,
        AuthorizationFilter
    ]
    

    And by removing @RefreshScope from the ApplicationSecurity class, everything works as expected.

    Why I added @RefreshScope at the first place? I was reading some properties form the ConfigServer.

    Why adding @RefreshScope makes spring security to fallback to default configuration? I haven't figured that out yet.