Fix Null SecurityContext In Spring Multi-Threading (@Async)

Overview

When you start using multiple threads in Spring applications, many funny things happen. At least this was my experience.

My use case is I have one endpoint that takes an audio file. The file would go through two services:

  • Speech to Text
  • Upload (S3)

As I want to return the result of the speech-to-text service immediately, the upload part needs to run in a different thread (using @Async).

In the upload method, I need to call to the security context to get the current user.

The code is something like this:

     private String getUserSubjectOnKeycloak() {
        Authentication authentication = SecurityContextHolder
                .getContext()
                .getAuthentication();
        if (authentication != null && authentication.isAuthenticated()) return authentication.getName();
        log.error("Authentication is null, user is not logged in");
        throw new RuntimeException("Authentication is null, user is not logged in");
    }

Even when the user is logged in, this call results in the RuntimeException thrown!

How to fix this?

Why did this happen?

If you are curious, this article will provide more details. Basically,

Security is stored on a per Thread basis. This means that when work is done on a new Thread, the SecurityContext is lost.

Spring docs

Thus, you need to configure a mechanism to propagate security context between threads.

The fix

To mitigate the issue, you need to create a bean of class DelegatingSecurityContextAsyncTaskExecutor

Here is one example method you can put into a @Configuration class:

    public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
        final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setThreadNamePrefix("ukata-");
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(100);
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(60);
        executor.initialize();
        return executor;
    }

    @Bean
    public TaskExecutor taskExecutor() {
        return new DelegatingSecurityContextAsyncTaskExecutor(threadPoolTaskExecutor()) {
        };
    }

Restart your application. The security context should be working as expected.

Conclusion

In this post, I’ve shown you how to configure your Spring boot application to have security context propagate between threads so you will not have surprises when start using @Async.

Leave a Comment