Spring Security Context Propagation to Threads

Learn to pass the Spring SecurityContext instance to new threads either spawned by Spring @Async or created explicitly using new Runnable or Callable instances.

1. Default Strategy is ThreadLocal

Once the AuthenticationManager completes the authentication process successfully, it stores the Authentication instance for the rest of the request in the security context described by the SecurityContext interface.

The SecurityContext provides two convenient methods:

public interface SecurityContext extends Serializable {

  Authentication getAuthentication();
  void setAuthentication(Authentication authentication);
}

To manage SecurityContext, Spring employs SecurityContextHolder in the role of a manager with three possible approaches:

  • MODE_THREADLOCAL: It is the default mode and allows each thread to store its own details. Any new thread does not get the context information from the parent thread.
  • MODE_INHERITABLETHREADLOCAL: It copies the security context to the next thread in case of an asynchronous method.
  • MODE_GLOBAL: It makes all the threads of the application see the same security context instance.

2. Problem with ThreadLocal Context in @Async Methods

Suppose we have a @Async method that we invoke as part of a client request. Note that, for executing @Async methods, Spring internally creates a new thread.

By default, Spring security does not propagate the authentication information to the new thread so if we try to get the logged-in user’s information inside the method, we should get an error.

@RestController
class WebController {

	Logger log = LoggerFactory.getLogger(WebController.class);

	@Autowired
	AsyncService asyncService;

	@GetMapping(value = "/async")
	public void executeWithInternalThread() throws Exception {
	  
	  log.info("In executeWithInternalThread - before call: "
	      + SecurityContextHolder.getContext().getAuthentication().getName());

	  asyncService.checkIfPrincipalPropagated();

	  log.info("In executeWithInternalThread - after call: "
	      + SecurityContextHolder.getContext().getAuthentication().getName());
	}
}
@Service
class AsyncService {
	
	Logger log = LoggerFactory.getLogger(AsyncService.class);

	@Async
	public void checkIfPrincipalPropagated() {
	  String username = SecurityContextHolder.getContext()
	      .getAuthentication()
	      .getName();

	  log.info("In checkIfPrincipalPropagated: "
	      + username);
	}
}

Let us invoke the /async API and verify if the context was propagated correctly or not.

@Test
void expectOK_WhenAccessAsyncAPI() throws Exception {
  mvc.perform(MockMvcRequestBuilders.get("/async")
          .with(httpBasic("user", "password")))
      .andExpect(status().isOk());
}

Notice the logs that complain that Authentication object in the security context is null.

11:50:16.181 [SimpleAsyncTaskExecutor-1] ERROR o.s.a.i.SimpleAsyncUncaughtExceptionHandler - 
Unexpected exception occurred invoking async method: public void com.howtodoinjava.AsyncService.checkIfPrincipalPropagated()

java.lang.NullPointerException: Cannot invoke "org.springframework.security.core.Authentication.getName()" because the return value of "org.springframework.security.core.context.SecurityContext.getAuthentication()" is null

3. Propagating Context to @Async Methods

There are two solutions that we can use for passing the security context to new thread used by the @Async method.

3.1. Using MODE_INHERITABLETHREADLOCAL Strategy

The first solution is pretty simple. We change the mode used by SecurityContextHolder from MODE_THREADLOCAL to MODE_INHERITABLETHREADLOCAL.

@Bean
public InitializingBean initializingBean() {
  return () -> SecurityContextHolder.setStrategyName(
      SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}

Or we can set the system property spring.security.strategy also.

spring.security.strategy = MODE_INHERITABLETHREADLOCAL

3.2. Using DelegatingSecurityContextAsyncTaskExecutor

The second solution is to use DelegatingSecurityContextAsyncTaskExecutor. It creates an AsyncTaskExecutor and wraps each Runnable in a DelegatingSecurityContextRunnable and each Callable in a DelegatingSecurityContextCallable.

@Bean
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
  ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
  executor.setCorePoolSize(10);
  executor.setMaxPoolSize(100);
  executor.setQueueCapacity(50);
  executor.setThreadNamePrefix("async-");
  return executor;
}

@Bean
public DelegatingSecurityContextAsyncTaskExecutor taskExecutor
    (ThreadPoolTaskExecutor delegate) {
  return new DelegatingSecurityContextAsyncTaskExecutor(delegate);
}

Calling the endpoint again, we can see now that the security context is propagated correctly to the next thread by Spring. Additionally, Authentication is not null anymore.

11:59:24.355 [SimpleAsyncTaskExecutor-1] INFO  com.howtodoinjava.AsyncService - In checkIfPrincipalPropagated: user

4. Passing Security Context to User Created New Threads

In the previous example, Spring was creating and managing the new thread so we were able to control the context propagation by overriding few bean definitions.

But when the new Thread is created by us then the framework does not know about these threads so we will again face the null issue while accessing Authentication objects in such threads.

Spring provides few specific classes that can help in these cases when we have to create new threads ourselves.

4.1. DelegatingSecurityContextRunnable for New Runnable Instances

The DelegatingSecurityContextRunnable implements the Runnable interface and is used to decorate the tasks we want to execute on a separate thread.

  • We can use it for the asynchronous execution of a task when there is no value expected after the execution.
  • It sets up a SecurityContext before invoking the delegate Runnable and then removes the SecurityContext after the delegate has completed.
@GetMapping(value = "/asyncRunnable")
public void executeWithExternalThread_Runnable() {

  log.info("In executeWithExternalThread_Runnable: "
      + SecurityContextHolder.getContext().getAuthentication().getName());

  asyncService.checkIfPrincipalPropagatedToNewRunnable();
}

The service method creates a new Runnable and submits to an ExecutorService for execution.

To propagate the SecurityContext to the new thread, we must wrap the Runnable instance with new DelegatingSecurityContextRunnable(task). If we fail to wrap the task then we will get NullPointerException inside Runnable while accessing the Authentication object.

public void checkIfPrincipalPropagatedToNewRunnable() {

  Runnable task = () -> {

    String username = SecurityContextHolder.getContext()
        .getAuthentication()
        .getName();

    log.info("In checkIfPrincipalPropagatedToNewRunnable inside Runnable: "
        + username);
  };

  ExecutorService e = Executors.newCachedThreadPool();
  try {
    var contextTask = new DelegatingSecurityContextRunnable(task);
    e.submit(contextTask);
  } finally {
    e.shutdown();
  }
}

Invoke the method to verify the results.

@GetMapping(value = "/asyncRunnable")
public void executeWithExternalThread_Runnable() {

  log.info("In executeWithExternalThread_Runnable: "
      + SecurityContextHolder.getContext().getAuthentication().getName());

  asyncService.checkIfPrincipalPropagatedToNewRunnable();
}

4.2. DelegatingSecurityContextCallable for New Callable Instances

The DelegatingSecurityContextCallable implements Callable interface and is used to decorate the tasks we want to execute on a separate thread.

We can use it for the asynchronous execution of a task when there is a return value after the thread completes its execution.

public String checkIfPrincipalPropagatedToNewCallable()
    throws ExecutionException, InterruptedException {

  Callable<String> task = () -> {

    String username = SecurityContextHolder.getContext()
        .getAuthentication()
        .getName();

    log.info("In checkIfPrincipalPropagatedToNewCallable inside Callable: "
        + username);

    return username;
  };

  ExecutorService e = Executors.newCachedThreadPool();
  try {
    var contextTask = new DelegatingSecurityContextCallable<>(task);
    return "Ciao, " + e.submit(contextTask).get() + "!";
  } finally {
    e.shutdown();
  }
}

4.3. DelegatingSecurityContextExecutorService class

While Spring security provides some great utility classes like DelegatingSecurityContextRunnable and DelegatingSecurityContextCallable that help in copying the details from the security context to new threads, we have another great alternative that manages propagation from the thread pool instead of from the task itself.

One such class DelegatingSecurityContextExecutorService that decorates the ExecutorService.

public void checkIfPrincipalPropagatedToExecutorService() {

  Runnable task = () -> {
    String username = SecurityContextHolder.getContext()
        .getAuthentication()
        .getName();

    log.info("In checkIfPrincipalPropagatedToExecutorService inside " +
        "Runnable: "
        + username);
  };

  ExecutorService e = Executors.newCachedThreadPool();
  e = new DelegatingSecurityContextExecutorService(e);

  try {
    e.submit(task);
  } finally {
    e.shutdown();
  }
}

4.4. More Classes

Additionally, for more flexibility, Spring Security offers few more utility classes to be used in specific times when a suitable usecase needs to be solved.

DelegatingSecurityContextExecutor : Implements the Executor interface and is designed to decorate an Executor object with the capability of forwarding the security context to the threads created by its pool.

DelegatingSecurityContextScheduledExecutorService: Implements the ScheduledExecutorService interface and is designed to decorate a ScheduledExecutorService object used to execute scheduled tasks in the spring framework.

5. Conclusion

In this Spring security tutorial, we learned to propagate or pass the Authentication and SecurityContext to the new threads either created by Spring framework or created by users.

We learned to wrap Runnable and Callable interfaces that help in minimizing the effort of maintaining the session in new threads.

Happy Learning !!

Sourcecode on Github

Comments

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.

Our Blogs

REST API Tutorial

Dark Mode

Dark Mode