Mario Fiore Vitale

Security context and concurrency

24 Feb 2021

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.

concurrency

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.