๐ด The Error You're Seeing
Confirm this matches your console output. If it does, you're in the right place.
org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.credentialsNotFound(AbstractSecurityInterceptor.java:379)
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:222)โก Quick Fix Works 80% of the time
Pass the authentication object explicitly to the async method, or use DelegatingSecurityContextExecutor.
@Async
public void runTask(Authentication auth) {
SecurityContextHolder.getContext().setAuthentication(auth);
// ...
}๐ง Why this Happens
Tap to expand the deep technical explanation
Spring Security stores the logged-in user in a ThreadLocal variable. When you spawn a new thread (like @Async), that new thread does not automatically share the ThreadLocal variable. When the background thread tries to access a secured method, it sees no user and throws this exception.
The HITEC City Parking Spot Analogy:
Imagine a hotel guest asking the concierge to fetch their bags from the room. The concierge goes to the room, but doesn't have the guest's room key (SecurityContext). The concierge cannot prove they are allowed to enter, so hotel security stops them.
๐ How to Reproduce Confirm this is your error
Read SecurityContextHolder.getContext().getAuthentication() inside an @Async method (background thread) in a secured app. The thread has no security context and throws AuthenticationCredentialsNotFoundException.
๐ ๏ธ Solutions (3 Ways to Fix)
Pass Authentication explicitly to @Async method
๐ Use this if you need the user context in a background thread.
Spring's SecurityContext uses ThreadLocal. By default, @Async threads do not inherit this context. You must pass it manually.
@Service
public class ReportService {
public void generateReport() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
self.runAsyncReport(auth);
}
@Async
public void runAsyncReport(Authentication auth) {
SecurityContextHolder.getContext().setAuthentication(auth);
// Now Security works
}
}Use DelegatingSecurityContextExecutor
๐ Use this if you want a clean architecture without passing Authentication objects everywhere.
Create a custom Executor that automatically copies the SecurityContext from the main thread to the background thread.
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.initialize();
return new DelegatingSecurityContextExecutor(executor);
}Set SecurityContextHolder to InheritableThreadLocal
๐ Use this as a quick fix, but be aware it doesn't work with ThreadPool reuse.
Change the strategy of the SecurityContextHolder so child threads inherit it.
@PostConstruct
public void init() {
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}๐ Version Notes
SecurityContext is ThreadLocal by default.
Identical behavior.
๐ก๏ธ How to Prevent This Next Time
Never assume SecurityContext is available in @Async, @Scheduled, or new Thread() blocks. Always propagate it manually or via a delegating executor.