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:
- Go through Filters.
- Go through Interceptors.
- Go through Controller.
- Start Async part and exit Filters, Interceptors, Controller.
- 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