Search code examples
spring-securityintegration-testingmockmvc

SecurityContextHolder returns the Wrong Username


While testing REST endpoints using MockMVC, the Spring SecurityContextHolder occasionally returns the wrong username within the same test. I have a service, in which there is a method that returns the username and a JPA repository checks whether the user exists (paraphrased):

String username = SecurityContextHolder.getContext().getAuthentication().getName();
Optional<RemoteUser> foundUser = userRepository.findOneByUsername(username);

Tests are annotated with @WithMockUser(username = "..." roles="...") when necessary. Each test is torn down afterwards and the Spring application context is refreshed.

@RunWith(SpringRunner.class)
@SpringBootTest(classes = IntegrationTestApplication.class)
public abstract class IntegrationTest {

    ...


    @Resource
    private WebApplicationContext applicationContext;

    protected MockMvc mockMvc;

    ...


    @Before
    public void setUp() {
         mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext).build();
    }

    ...
}

About 97-98% of the time, the tests run fine. However, sometimes, the username returned by the Authentication object is not the one that was defined in the @WithMockUser annotation. Using log statements, I have even seen usernames used in other test classes that have already run. Before each test, database users are set up, so if a username is returned that is not in the database, the test will fail depending on the case for which the user is needed.

Even stranger is that this method in this service can be called multiple times over the course of the test and sometimes the username is correct, and then it all of a sudden is wrong. I do not understand how this is possible. My understanding was that the SecurityContextHolder bean was thread-safe, and therefore the flakiness of the tests baffles me. How can this be happening?

Just a few other things to note:

  1. Using Spring Boot 2.2.7
  2. The tests are not run in parallel.
  3. The aforementioned service is always lazily injected via field injection (@Lazy). I don't know if this is an important detail, but this is not my code, and this is the only service that uses this annotation and why this is done this way is unbeknownst to me.
  4. There are a few methods annotated with @Async and I thought maybe there could be side effects, but I have nothing to support that and that line of thinking is pure spitballing.
  5. I have debugged the TestSecurityContextHolder and have seen that the methods that clear the context are indeed called.
  6. I did see this SecurityContextHolder gives wrong User details as well, but one, there is no answer to the question, and two, there are no concurrent requests in my test cases.

Solution

  • After looking into the SecurityContextHolder bean a little bit more, I found out some more information about the different strategies available regarding threads. I discovered a bean that set this:

    SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
    

    This will propagate the security context to all threads spawned off of the main thread. It was supposed to be excluded from the test context, but was not. After having removed this bean from the test application context, one test case that was only intermittently failing before was now failing every single time.

    Long story short, the service method that leverages the SecurityContextHolder object was being called multiple times inside a parallel stream. That brought me to here: Null principal when get security context in parallelStream. Refactoring this to call it only once outside of the stream seems to have solved the issue.