skip to content

How do you propagate the SecurityContext to code running on another thread (@Async, an ExecutorService, or MVC async)?

level: seniorimportance: must knowfreq 55%

answer

  1. capture at submit, set on worker, restore in finally
  2. Runnable/Callable = one task; Executor* = wrap pool
  3. @Async -> DelegatingSecurityContextAsyncTaskExecutor
  4. MVC async auto: WebAsyncManagerIntegrationFilter
  5. createEmptyContext for system-account jobs

basics

~20 s

Wrap the task or executor with Spring's delegating helpers: DelegatingSecurityContextRunnable / Callable for a single task, or DelegatingSecurityContextExecutor / DelegatingSecurityContextAsyncTaskExecutor for @Async. They capture the caller's context at submit time and install it on the worker thread for that task.

solid answer

~40 s

The default MODE_THREADLOCAL means a worker thread starts with an empty SecurityContext, so you must propagate explicitly. Spring Security provides delegating wrappers that capture the SecurityContext at submission and set it on the executing thread, restoring the previous value afterward. For a one-off task, wrap it in DelegatingSecurityContextRunnable or DelegatingSecurityContextCallable. For an executor, wrap it in DelegatingSecurityContextExecutor (or DelegatingSecurityContextExecutorService, or DelegatingSecurityContextScheduledExecutorService). For @Async, expose a DelegatingSecurityContextAsyncTaskExecutor as the async executor. For Spring MVC async (Callable return, DeferredResult, WebAsyncTask), the framework's WebAsyncManager integration already propagates the context via SecurityContextCallableProcessingInterceptor when Spring Security's filter is in place. This per-task capture avoids the thread-pool staleness problem that MODE_INHERITABLETHREADLOCAL suffers.

code

java · 12 lines
java
// Run a background task under an explicit (system) identity
SecurityContext ctx = SecurityContextHolder.createEmptyContext();
ctx.setAuthentication(new UsernamePasswordAuthenticationToken(
        "system", null, List.of(new SimpleGrantedAuthority("ROLE_SYSTEM"))));

Callable<Report> task = () -> reportService.generate();
Callable<Report> propagated = new DelegatingSecurityContextCallable<>(task, ctx);
Future<Report> future = executorService.submit(propagated);

// Or wrap the whole executor so EVERY submitted task inherits the caller's context
ExecutorService secured = new DelegatingSecurityContextExecutorService(
        Executors.newFixedThreadPool(4));

go deeper

for a junior

Know that spawned threads lose the security context and Spring has helpers to carry it over.

for a middle

Name the Runnable/Callable wrappers and the executor wrappers and roughly how they work.

for a senior

Configure @Async propagation via DelegatingSecurityContextAsyncTaskExecutor and explain the capture/set/restore lifecycle and MVC async auto-propagation.

for a principal

Design cross-cutting propagation policy (system-account contexts via createEmptyContext, servlet vs reactive boundary) and avoid mixing inheritance with delegation.

Because the default strategy is thread-isolated, any work you hand to another thread loses the current identity unless you carry it over. Spring Security ships a family of **delegating** helpers in `org.springframework.security.concurrent` (and `task`) precisely for this. They all follow the same pattern: **capture** `SecurityContextHolder.getContext()` on the *submitting* thread, and when the task runs, **set** it on the worker thread, run the delegate, then **restore** the worker's prior context in a finally block. Capture-at-submit-time is what makes them correct on pooled executors, unlike `InheritableThreadLocal`. **Single task wrappers:** - `DelegatingSecurityContextRunnable` — wraps a `Runnable`. - `DelegatingSecurityContextCallable<T>` — wraps a `Callable<T>`. By default they snapshot the current context at construction; you can also pass an explicit `SecurityContext`. ```java Runnable original = () -> service.doWork(); Runnable wrapped = new DelegatingSecurityContextRunnable(original); someExecutor.execute(wrapped); ``` **Executor wrappers (wrap once, every submitted task is propagated):** - `DelegatingSecurityContextExecutor` — wraps `Executor`. - `DelegatingSecurityContextExecutorService` — wraps `ExecutorService`. - `DelegatingSecurityContextScheduledExecutorService` — wraps `ScheduledExecutorService`. - `DelegatingSecurityContextAsyncTaskExecutor` — wraps Spring's `AsyncTaskExecutor`, the one used by `@Async`. **Enabling it for `@Async`:** define your async executor as a delegating one so every `@Async` method inherits the caller's context: ```java @Configuration @EnableAsync class AsyncConfig implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor(); exec.initialize(); return new DelegatingSecurityContextAsyncTaskExecutor(exec); } } ``` **Spring MVC async requests:** for controller methods returning `Callable<T>`, `DeferredResult<T>`, or `WebAsyncTask<T>`, Spring Security's `WebAsyncManagerIntegrationFilter` (part of the default chain) registers a `SecurityContextCallableProcessingInterceptor`, which propagates the `SecurityContext` from the initial request thread onto the async thread automatically. You generally don't need to wrap anything here — just keep the security filter in the chain. **Programmatic bootstrapping:** `SecurityContextHolder.getContextHolderStrategy()` returns the current `SecurityContextHolderStrategy`; modern code should read/write through the strategy object rather than the static methods for testability. `SecurityContextHolder.createEmptyContext()` gives you a fresh context to populate before submitting a task under a *different* identity (e.g., a system/service account for a background job): create an empty context, set an `Authentication` on it, and pass it into a `DelegatingSecurityContextCallable(task, thatContext)`. **Reactive note (scope boundary):** in WebFlux there is no ThreadLocal model; the context lives in the Reactor `Context` and is accessed via `ReactiveSecurityContextHolder.getContext()`. The ThreadLocal strategies and the delegating executors above are the *servlet* story — don't conflate the two. **Gotchas:** - Wrap at **submit** time on the caller thread; if you construct the wrapper on the worker thread you capture the wrong (or empty) context. - The wrappers restore the previous context afterward, so they're safe on pools. - If the response has already been committed / the request completed, the original context may have been cleared — capture it while it's still live. - Don't combine these with MODE_INHERITABLETHREADLOCAL expecting extra safety; the delegating wrappers alone are the correct and sufficient mechanism.

  • Do you need to wrap anything for a controller returning Callable<T> or DeferredResult?
    No. Spring Security's WebAsyncManagerIntegrationFilter registers a SecurityContextCallableProcessingInterceptor that propagates the context to the async thread automatically, as long as the security filter chain is active.
  • Why is capturing the context at submit time better than InheritableThreadLocal for pools?
    InheritableThreadLocal copies once when the worker thread is created, so pooled workers go stale. The delegating wrappers snapshot the context on the caller at each submit and install it per task, then restore it — correct regardless of pool reuse.

saying these in an interview costs you the question

  • Constructing the delegating wrapper on the worker thread instead of the caller
  • Using MODE_INHERITABLETHREADLOCAL for @Async pools instead of delegating wrappers
  • Manually wrapping MVC Callable/DeferredResult when the filter already propagates it
  • Confusing servlet ThreadLocal propagation with WebFlux ReactiveSecurityContextHolder

context