Spring Security and @Async (Authenticated Users mixed up)

SpringAsynchronousSpring Security

Spring Problem Overview


I asynchronously invoke method with Spring, using @Async.This method invokes other method annotated with @PreAuthorize, Spring Security Annotation. To make authorization works I have to set SecurityContextHolder mode to MODE_INHERITABLETHREADLOCAL, so that authentication info is passed to the asynchronous call. Everything works fine so far.

However when I logout and login as a different user, in asynchronous method SecurityContextHolder stores authentication info of the old user, that has bee logged out. It causes of course unwanted AccessDenied exception. There is no such problem with synchronous calls.

I have defined <task:executor id="executors" pool-size="10"/>, so may it be a problem that once thread in executors pool has been initialized it will not override authentication information?

Spring Solutions


Solution 1 - Spring

I guess MODE_INHERITABLETHREADLOCAL doesn't work correctly with thread pool.

As a possible solution you can try to subclass ThreadPoolTaskExecutor and override its methods to propagate SecurityContext manually, and then declare that executor instead of <task:executor>, something like this:

public void execute(final Runnable r) {
    final Authentication a = SecurityContextHolder.getContext().getAuthentication();
    
    super.execute(new Runnable() {
        public void run() {
            try {
                SecurityContext ctx = SecurityContextHolder.createEmptyContext();
                ctx.setAuthentication(a);
                SecurityContextHolder.setContext(ctx);
                r.run();
            } finally {
                SecurityContextHolder.clearContext();
            }
        }
    });
}

Solution 2 - Spring

I also ran into that problem. It is important to configure the ThreadPoolTaskExecutor correctly using the DelegatingSecurityContextAsyncTaskExecutor. Also it is important to call the initialize() method, otherwise an error is thrown.

// define the TaskExecutor as a bean
@Bean("threadPoolTaskExecutor")
public TaskExecutor getAsyncExecutor() {

  ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
  executor.setCorePoolSize(20);
  executor.setMaxPoolSize(1000);
  executor.setWaitForTasksToCompleteOnShutdown(true);
  executor.setThreadNamePrefix("Async-");
  executor.initialize(); // this is important, otherwise an error is thrown
  return new DelegatingSecurityContextAsyncTaskExecutor(executor); // use this special TaskExecuter
}

// the method in your business logic which is called async
@Override
@Async("threadPoolTaskExecutor")
public void yourLogic() {
  [..]
}

Solution 3 - Spring

This is just a hint that needs future investigation (I am too tired, but maybe somebody find this useful for future investigation):

Today I stumbled over org.springframework.security.task.DelegatingSecurityContextAsyncTaskExecutor see GitHub.

it looks like that his designed to delegate the security context so that it is "passed" through the @Async call.

Also have a look at this post: Spring Security 3.2 M1 Highlights, Servlet 3 API Support is sounds like it is strongly related.

Solution 4 - Spring

Using the information from Ralph and Oak -

If you want to get @Async working with the standard task executor tag, you would set up your Spring XML config like this

<task:annotation-driven executor="_importPool"/>
<task:executor id="_importPool" pool-size="5"/>

<bean id="importPool"
          class="org.springframework.security.task.DelegatingSecurityContextAsyncTaskExecutor">
     <constructor-arg ref="_importPool"/>
</bean>

Then in your @Async method, you would specify the pool you want to use

@Async("importPool")
public void run(ImportJob import) {
   ...
}

That should work so when whenever you call your @Async method, the threadpool thread will use the same security context as the calling thread

Solution 5 - Spring

Based on @Ralph answer one can achieve Aync event with Spring with threadpooling and delegate the security using http://docs.spring.io/autorepo/docs/spring-security/4.0.0.M1/apidocs/org/springframework/security/task/DelegatingSecurityContextAsyncTaskExecutor.html

Sample code

<bean id="applicationEventMulticaster"
	class="org.springframework.context.event.SimpleApplicationEventMulticaster">
	<property name="taskExecutor">
		<ref bean="delegateSecurityAsyncThreadPool"/>
	</property>
</bean>

<bean id="threadsPool"
	class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
</bean>


<bean id="delegateSecurityAsyncThreadPool"
	class="org.springframework.security.task.DelegatingSecurityContextTaskExecutor">
	<constructor-arg ref="threadsPool"/>
</bean>

Solution 6 - Spring

As it was already mentioned, for pooled threads environment DelegatingSecurityContextAsyncTaskExecutor should be used, instead of MODE_INHERITABLETHREADLOCAL (read here).

Leaving simple DelegatingSecurityContextAsyncTaskExecutor configuration for Spring Boot projects which will simply use default Spring Boot pool for async tasks:

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
    private final ThreadPoolTaskExecutor defaultSpringBootAsyncExecutor;

    public AsyncConfig(ThreadPoolTaskExecutor defaultSpringBootAsyncExecutor) {
        this.defaultSpringBootAsyncExecutor = defaultSpringBootAsyncExecutor;
    }

    @Override
    public Executor getAsyncExecutor() {
        return new DelegatingSecurityContextAsyncTaskExecutor(defaultSpringBootAsyncExecutor);
    }
}

Solution 7 - Spring

Jus to add to the answer from @axtavt, you would also want to override other method.

@Override
	public <T> Future<T> submit(Callable<T> task) {
		ExecutorService executor = getThreadPoolExecutor();
		final Authentication a = SecurityContextHolder.getContext().getAuthentication();
		try {
			return executor.submit(new Callable<T>() {
				@Override
				public T call() throws Exception {
					try {
						SecurityContext ctx = SecurityContextHolder.createEmptyContext();
						ctx.setAuthentication(a);
						SecurityContextHolder.setContext(ctx);
						return task.call();
					} catch (Exception e) {
						slf4jLogger.error("error invoking async thread. error details : {}", e);
						return null;
					} finally {
						SecurityContextHolder.clearContext();
					}
				}
			});
		} catch (RejectedExecutionException ex) {
			throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex);
		}
	}

Solution 8 - Spring

I use an Enterprise Jboss Server with a JNDI managed thread pool; This is what worked for me:

@Configuration
@EnableAsync
public class AsyncAppConfig {

	public static final String THREAD_POOL_ID = "threadPoolId";

	@Bean(THREAD_POOL_ID)
	public DelegatingSecurityContextAsyncTaskExecutor secureThreadPool(
			DefaultManagedTaskExecutor jbossManagedTaskExecutor) {
		return new DelegatingSecurityContextAsyncTaskExecutor(jbossManagedTaskExecutor);
	}

	@Bean
	public DefaultManagedTaskExecutor jbossManagedTaskExecutor() {
		return new DefaultManagedTaskExecutor() {
			@Override
			public void afterPropertiesSet() throws NamingException {
				// gets the ConcurrentExecutor configured by Jboss
				super.afterPropertiesSet();
				// Wraps the jboss configured ConcurrentExecutor in a securityContext aware delegate
				setConcurrentExecutor(new DelegatingSecurityContextExecutor(getConcurrentExecutor(), getContext()));
			}
		};
	}
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionLukasz MorenView Question on Stackoverflow
Solution 1 - SpringaxtavtView Answer on Stackoverflow
Solution 2 - SpringmaxehView Answer on Stackoverflow
Solution 3 - SpringRalphView Answer on Stackoverflow
Solution 4 - SpringrinceView Answer on Stackoverflow
Solution 5 - SpringoakView Answer on Stackoverflow
Solution 6 - SpringKirillView Answer on Stackoverflow
Solution 7 - SpringAmit ParasharView Answer on Stackoverflow
Solution 8 - SpringRyan McCulloughView Answer on Stackoverflow