How do you make SecurityContextHolder's authenticated user available inside @Async / thread-pool work, and why isn't it automatic?
answer
- MODE_THREADLOCAL — not inherited by pool threads
- DelegatingSecurityContextAsyncTaskExecutor / Runnable / Callable
- TaskDecorator: capture getContext(), finally clearContext()
- MODE_INHERITABLETHREADLOCAL unreliable + leaks on reuse
- capture at submit before filter chain clears it
basics
~20 sSecurityContextHolder uses a thread-local (MODE_THREADLOCAL) that isn't shared with pool threads, so the async thread sees no user. Fix it with Spring Security's DelegatingSecurityContextAsyncTaskExecutor, or a TaskDecorator that copies the SecurityContext and clears it afterward.
solid answer
~40 sSecurityContextHolder stores the authentication in a thread-local; the default strategy is MODE_THREADLOCAL, which pool threads don't inherit, so on an @Async thread SecurityContextHolder.getContext() is empty and @PreAuthorize/method security see an anonymous caller. Idiomatic fix: wrap the executor with Spring Security's DelegatingSecurityContextAsyncTaskExecutor (or use DelegatingSecurityContextRunnable/Callable), which snapshots the caller's SecurityContext and installs it around execution, clearing it after. Equivalently, register a TaskDecorator that captures SecurityContextHolder.getContext() on the caller thread and, on the pool thread, setContext(...) in a try / clearContext() in finally. Avoid relying on MODE_INHERITABLETHREADLOCAL: it only propagates to threads spawned from the parent and, with a reused pool, leaks a stale principal. Cleanup in finally is essential.
code
java · 14 linesimport org.springframework.security.task.DelegatingSecurityContextAsyncTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.context.annotation.Bean;
// Idiomatic: let Spring Security wrap the delegate executor.
@Bean
public AsyncTaskExecutor applicationTaskExecutor() {
ThreadPoolTaskExecutor delegate = new ThreadPoolTaskExecutor();
delegate.setCorePoolSize(8);
delegate.initialize();
// captures the caller's SecurityContext and restores it around each task
return new DelegatingSecurityContextAsyncTaskExecutor(delegate);
}go deeper
Know the async thread sees no user because SecurityContextHolder is thread-local.
Name DelegatingSecurityContextAsyncTaskExecutor / a TaskDecorator and the capture-then-clear pattern.
Explain why InheritableThreadLocal fails on pools and how Micrometer accessors unify propagation.
Weigh per-executor wrapping vs. a single context-propagation strategy, and reason about reused-thread security-leak risk.
## Why it isn't automatic Spring Security stores the current authentication in `SecurityContextHolder`, backed by a **thread-local**. The default strategy is `SecurityContextHolder.MODE_THREADLOCAL`: the value is bound to the thread that authenticated the request (the Servlet container thread). When you offload work to a **pool** (`ThreadPoolTaskExecutor`, `@Async`), the worker is a different thread with its own empty holder. So: ```java SecurityContextHolder.getContext().getAuthentication(); // null / anonymous on the pool thread ``` Method security (`@PreAuthorize`, `@PostAuthorize`) evaluated inside the async task then sees no principal and denies or treats the call as anonymous. ## Option 1 — Spring Security's delegating executors (idiomatic) Spring Security ships wrappers that snapshot the caller's `SecurityContext` and re-establish it on the worker: - `DelegatingSecurityContextExecutor` - `DelegatingSecurityContextAsyncTaskExecutor` (wraps an `AsyncTaskExecutor`) - `DelegatingSecurityContextRunnable` / `DelegatingSecurityContextCallable` Wrap your `ThreadPoolTaskExecutor`: ```java @Bean public AsyncTaskExecutor securityAwareExecutor(ThreadPoolTaskExecutor delegate) { return new DelegatingSecurityContextAsyncTaskExecutor(delegate); } ``` These classes handle capture and — importantly — cleanup after the task. ## Option 2 — a TaskDecorator A decorator gives you the same effect and composes with MDC/other propagation on one executor: ```java public class SecurityContextTaskDecorator implements TaskDecorator { public Runnable decorate(Runnable runnable) { SecurityContext ctx = SecurityContextHolder.getContext(); // caller thread return () -> { SecurityContext previous = SecurityContextHolder.getContext(); SecurityContextHolder.setContext(ctx); try { runnable.run(); } finally { SecurityContextHolder.setContext(previous); /* or clearContext() */ } }; } } ``` ## The MODE_INHERITABLETHREADLOCAL trap `SecurityContextHolder.setStrategyName(MODE_INHERITABLETHREADLOCAL)` makes **child threads spawned from the current thread** inherit the context (Java `InheritableThreadLocal`). But: - It only works when the pool thread is **created** by the request thread — with a pre-warmed/reused pool that's usually **not** the case. - Even when a thread is inherited, a **reused** pooled thread keeps the value it first inherited, leaking a stale user to later tasks. So `MODE_INHERITABLETHREADLOCAL` is unreliable for executors and can be a security bug. Prefer explicit propagation with cleanup. ## Unified approach Spring Security registers a Micrometer `ThreadLocalAccessor` for the security context, so **Micrometer context-propagation** (and Spring 6.1's `ContextPropagatingTaskDecorator`) can carry the `SecurityContext` alongside MDC and Observation in one mechanism — the preferred modern strategy when you need several contexts at once. ## Gotchas - The security context must be captured at **submit time** on the request thread — the filter chain clears it once the request completes, so late capture yields nothing. - Always **clear/restore in finally** on pooled threads. - WebFlux uses a Reactor `Context`, not a thread-local, so this specifically concerns the blocking/Servlet + thread-pool model.
- Why is MODE_INHERITABLETHREADLOCAL a poor fix for thread pools?InheritableThreadLocal only copies to threads created by the parent. Pool threads are usually pre-created and reused, so they don't inherit fresh context per task and retain a stale principal — both a correctness and a security leak.
- If you already have an MDC TaskDecorator, how do you also propagate SecurityContext?Compose them into one decorator (capture both, restore both in finally), or switch to Micrometer context-propagation / Spring 6.1 ContextPropagatingTaskDecorator, which propagates all registered thread-locals (security, MDC, observation) uniformly.
saying these in an interview costs you the question
- Claiming SecurityContext propagates to async threads automatically.
- Recommending MODE_INHERITABLETHREADLOCAL as the correct general fix for pools.
- Capturing the SecurityContext on the pool thread instead of the request thread.
- Forgetting to clear the context, leaking the principal to the next pooled task.