In the last months, I met “Security context” and “concurrency” with their friends “threads” and after an initial confrontation, we cleared up and now I want to share this experience with you.
Comic by geek-and-poke
The problem
I needed to store authentication information that must be accessed during all the request flow. After some research I found that the most used approach is to store this information inside a ThreadLocal variable so in that case, it will be available for the thread that serves a particular request. If you are using Spring you can use SucurityContextHolder bean.
If we have a look at this class we can, without any surprise, saw that Spring uses a ThreadLocal variable to store our authentication information. Well, we solved the problem!
Uhmm..are you sure?
One step deep
As you know ‘ThreadLocal’ means exactly what the name says: it’s a variable local to the thread. So..what happens if I start other threads? Are you starting to feel the problem?
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.concurrent.atomic.AtomicReference;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
class SecurityContextDefaultStrategy {
@Test
void singleThreadAllWorks() {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken("user", "pass"));
assertThat(SecurityContextHolder.getContext().getAuthentication().getPrincipal()).isEqualTo("user");
}
@Test
void multiThreadFail() throws InterruptedException {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken("user", "pass"));
AtomicReference<Object> principal = new AtomicReference<>();
Thread user = new Thread(() -> {
// do some stuff
principal.set(SecurityContextHolder.getContext().getAuthentication().getPrincipal());
});
user.start();
user.join();
assertThat(principal.get()).isEqualTo("user");
}
}
Look at the second test, we are getting the authentication context from a new thread and this will FAIL. Why? To better understand I suggest to take always a look at the code. Here we can see that SecurityContextHolder class has different strategies to “store” context:
- MODE_GLOBAL
- MODE_INHERITABLETHREADLOCAL
- MODE_THREADLOCAL
Since the default is MODE_THREADLOCAL the test is expected to fail because the context is local to the main thread. Why is this the default?
MODE_THREADLOCAL, which is backwards compatible, has fewer JVM incompatibilities and is appropriate on servers (whereas MODE_GLOBAL is definitely inappropriate for server use).
This is extracted from spring docs
Inheritable thread local
We can solve the problem of missing context when creating new threads setting the SecurityContextHolder strategy to MODE_INHERITABLETHREADLOCAL
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
this can also be done by setting the property
spring.security.strategy=MODE_INHERITABLETHREADLOCAL
Let’s see the test
import me.mfvitale.securitycontext.helpers.SecurityContextHelper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.concurrent.DelegatingSecurityContextExecutor;
import org.springframework.security.concurrent.DelegatingSecurityContextExecutorService;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
class SecurityContextInheritableStrategy {
@Test
void singleThreadAllWorks() {
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
SecurityContextHolder.setContext(SecurityContextHelper.createContext("user", "pass"));
assertThat(SecurityContextHolder.getContext().getAuthentication().getPrincipal()).isEqualTo("user");
}
@Test
void multiThreadAllWorksNow() throws InterruptedException {
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
SecurityContextHolder.setContext(SecurityContextHelper.createContext("user", "pass"));
AtomicReference<Object> principal = new AtomicReference<>();
Thread user = new Thread(() -> {
// do some stuff
principal.set(SecurityContextHolder.getContext().getAuthentication().getPrincipal());
});
user.start();
user.join();
assertThat(principal.get()).isEqualTo("user");
}
@Test
void threadPoolStillFail() throws InterruptedException {
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
SecurityContextHolder.setContext(SecurityContextHelper.createContext("user1", "pass"));
ExecutorService executor = Executors.newSingleThreadExecutor();
AtomicReference<Object> principal = new AtomicReference<>();
print(SecurityContextHolder.getContext());
executor.execute(() -> {
// do some stuff
print(SecurityContextHolder.getContext());
principal.set(SecurityContextHolder.getContext().getAuthentication().getPrincipal());
});
executor.awaitTermination(100, TimeUnit.MILLISECONDS);
assertThat(principal.get()).isEqualTo("user1");
SecurityContextHolder.setContext(SecurityContextHelper.createContext("user2", "pass"));
print(SecurityContextHolder.getContext());
executor.execute(() -> {
// do some stuff
print(SecurityContextHolder.getContext());
principal.set(SecurityContextHolder.getContext().getAuthentication().getPrincipal());
});
executor.awaitTermination(100, TimeUnit.MILLISECONDS);
assertThat(principal.get()).isEqualTo("user2");
}
@Test
void threadPoolAllWorks() throws InterruptedException {
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
SecurityContextHolder.setContext(SecurityContextHelper.createContext("user1", "pass"));
ExecutorService originalExecutor = Executors.newSingleThreadExecutor();
DelegatingSecurityContextExecutorService executor = new DelegatingSecurityContextExecutorService(originalExecutor);
AtomicReference<Object> principal = new AtomicReference<>();
print(SecurityContextHolder.getContext());
executor.execute(() -> {
// do some stuff
print(SecurityContextHolder.getContext());
principal.set(SecurityContextHolder.getContext().getAuthentication().getPrincipal());
});
executor.awaitTermination(100, TimeUnit.MILLISECONDS);
assertThat(principal.get()).isEqualTo("user1");
SecurityContextHolder.setContext(SecurityContextHelper.createContext("user2", "pass"));
print(SecurityContextHolder.getContext());
executor.execute(() -> {
// do some stuff
print(SecurityContextHolder.getContext());
principal.set(SecurityContextHolder.getContext().getAuthentication().getPrincipal());
});
executor.awaitTermination(100, TimeUnit.MILLISECONDS);
assertThat(principal.get()).isEqualTo("user2");
}
private static void print(SecurityContext securityContext) {
System.out.println(Thread.currentThread().getName() + "="+ securityContext.getAuthentication().getPrincipal());
}
}
Now the first two tests will pass but not the third one…:(
Uhmm…let’s think about it. We are using InheritableThreadLocal which means that the context will be copied to a new thread from the main thread but in our third test we are using a thread pool..so a thread is created once (at this moment the context will be inherited from the main thread) but then will be reused! Did you get the point?
We need something to copy the context before each execution. Spring has the concept of *DelegatingSecurityContext** that do the work for us.
- DelegatingSecurityContextCallable
- DelegatingSecurityContextExecutor
- DelegatingSecurityContextExecutorService
- DelegatingSecurityContextRunnable
- DelegatingSecurityContextScheduledExecutorService
- DelegatingSecurityContextSchedulingTaskExecutor
- DelegatingSecurityContextAsyncTaskExecutor
- DelegatingSecurityContextTaskExecutor
In this case, we need the DelegatingSecurityContextExecutorService that wraps our ExecutorService
ExecutorService originalExecutor = Executors.newSingleThreadExecutor();
DelegatingSecurityContextExecutorService executor = new DelegatingSecurityContextExecutorService(originalExecutor);
Now all the tests will pass! This is because the DelegatingSecurityContextExecutorService will copy the context before thread execution.
Conclusion
We have seen how to configure and use SecurityContextHolder for our scope. I hope the article clarify better the why and how these things work. I used Spring and SecurityContextHolder but this approach can be used in general when you need to store context information in your application in a multi-thread environment.
Check the repo for the full project.