SecurityFilterChain filter 2 times and lose authentication

Before async everything worked fine. So the problem is securityFilterChain filters 2 times and I think it should filter just once because filtering 2 times it doesn’t make sense and even if it’s correct to filter 2 times, it lose the authentication which throws the AuthenticationEntryPoint. I’ll paste here some junks of my code along with some debugging prints in the console.

    @Bean("AsyncTask")
    @Primary
    public Executor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(100);
        executor.setMaxPoolSize(100);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("template-thread-");
        executor.initialize();
        return new DelegatingSecurityContextAsyncTaskExecutor(executor);
    }
    @GetMapping("/get_test")
    public CompletableFuture<String> getTest() throws InterruptedException {

        System.out.println("Entered in controller");

        long i = Thread.currentThread().getId();
        System.out.println("Thread id: " + i);

        SecurityContext preContext = SecurityContextHolder.getContext();
        System.out.println("Before Async - Authenticated user: " + (preContext.getAuthentication() != null ? preContext.getAuthentication().getName() : "null"));

        return testService.asyncMethod();
    }
    @Async("AsyncTask")
    public CompletableFuture<String> asyncMethod() throws InterruptedException {

        System.out.println("Entered in service");

        System.out.println("Thread id: " + Thread.currentThread().getId());

        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        System.out.println("In async method - Authenticated user: " + (auth != null ? auth.getName() : "none"));

        return CompletableFuture.completedFuture("Hello World");
    }
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig{

    private final JWTAuthenticationFilter jwtAuthenticationFilter;
    private final PermissionMapping permissionMapping;
    private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
    private final CustomAccessDeniedHandler customAccessDeniedHandler;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception{
        httpSecurity
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)

                .authorizeHttpRequests(auth -> auth
                        .anyRequest().access((authenticationSupplier, context) -> {
                            System.out.println("Entered in security filter");
                            Authentication authentication = authenticationSupplier.get();


                            System.out.println("Thread id: " + Thread.currentThread().getId());
                            System.out.println("Security - Authenticated user: " + authentication.getName());

                            String currentUri = context.getRequest().getRequestURI();
                            System.out.println("Security - Current URI: " + currentUri);
                            List<String> requiredPermissions = permissionMapping.getPermissions(currentUri);

                            if (requiredPermissions != null) {
                                boolean hasPermission = authentication.getAuthorities().stream()
                                        .anyMatch(grantedAuthority -> requiredPermissions.contains(grantedAuthority.getAuthority()));

                                return new AuthorizationDecision(hasPermission);
                            }
                            return new AuthorizationDecision(true);
                        })
                )

                .exceptionHandling(exceptionHandling -> exceptionHandling
                        .authenticationEntryPoint(customAuthenticationEntryPoint) // Handle unauthenticated access
                        .accessDeniedHandler(customAccessDeniedHandler) // Handle unauthorized access
                );

        return httpSecurity.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
@Component
@RequiredArgsConstructor
public class JWTAuthenticationFilter extends OncePerRequestFilter {
    private final JWTUtils jwtUtils;
    private final CustomUserService customUserService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        System.out.println("Entered in JWT Filter");
        System.out.println("Thread id: " + Thread.currentThread().getId());
        Cookie jwtCookie = WebUtils.getCookie(request, "jwt");
        String jwt = jwtCookie != null ? jwtCookie.getValue() : null;

        if(jwt == null){
            request.setAttribute("jwtStatus", "invalid"); // This attribute will be used in another filter
            filterChain.doFilter(request, response);
            return;
        }

        try {
            String username = jwtUtils.getUsernameFromJWT(jwt);

            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = customUserService.loadUserByUsername(username);

                if (jwtUtils.isTokenValid(jwt, userDetails.getUsername())) {
                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                            userDetails, null, userDetails.getAuthorities()
                    );

                    request.setAttribute("jwtStatus", "valid");
                    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
                else {
                    jwtCookie.setMaxAge(0);
                    request.setAttribute("jwtStatus", "invalid");
                    response.addCookie(jwtCookie);
                }
            }
            else {
                jwtCookie.setMaxAge(0);
                request.setAttribute("jwtStatus", "invalid");
                response.addCookie(jwtCookie);
            }
        }
        catch (Exception e){
            jwtCookie.setMaxAge(0);
            request.setAttribute("jwtStatus", "invalid");
            response.addCookie(jwtCookie);
        }

        filterChain.doFilter(request, response);
    }
}

Prints:
Entered in JWT Filter
Thread id: 42
Entered in security filter
Thread id: 42
Security – Authenticated user: dev
Security – Current URI: /get_test
Entered in controller
Thread id: 42
Before Async – Authenticated user: dev
Entered in service
Thread id: 66
In async method – Authenticated user: dev
Entered in security filter
Thread id: 42
Security – Authenticated user: anonymousUser
Security – Current URI: /get_test

I tried to change the strategy of context holder: SecurityContextHolder.MODE_INHERITABLETHREADLOCAL but that didn’t work too.

2

According to Spring documentation when you use async requests Spring it follow this algorithm:

  1. Go through Filters.
  2. Go through Interceptors.
  3. Go through Controller.
  4. Start Async part and exit Filters, Interceptors, Controller.
  5. When Async part is finished Spring goes through Interceptors again, and skips Filters and Controller.

Here is quote from Spring docs (it’s clearly emphasized that Controllers are skipped, but the behavior of Filters and Interceptors is not fully explained, but according to my tests algorithm above is correct):

When a controller returns a DeferredResult, the Filter-Servlet chain is exited, and the Servlet container thread is released. Later, when the DeferredResult is set, an ASYNC dispatch (to the same URL) is made, during which the controller is mapped again but, rather than invoking it, the DeferredResult value is used (as if the controller returned it) to resume processing.

2

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật