Spring Security's SecurityContext get unexpectedly cleared in a MockMvc Test

Joseph GanJoseph Gan
3 min read

Background

At work, there’s some test written where I wanted to query the database (repository) after mockMvc request has been made to verify the result. This is a very simple illustration of what it looks like.

@SpringBootTest
@AutoConfigureMockMvc
class SomeTests {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private UserRepository repository;

    @Test
    @WithMockUser(value = "bwgjoseph")
    void test() {
        this.mockMvc.perform(post("/api/v1/users/{name}", "bwgjoseph")
            .accept(MediaType.APPLICATION_JSON)
            .contentType(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk());

        // extract data from repository
        var user = this.repository.findByName();

        // verify result
        assertThat(user.get()).extracting("name").isEqualTo("bwgjoseph");
    }
}

Please note that for simplicity, I have configured my Spring Security user.name as bwgjoseph. And that is why the code above for @WithMockUser is also defined as such. This is to facilitate the @Query used in UserRepository to extract the correct username via the principal?.username.

Once run, the following exception will be thrown java.lang.IllegalArgumentException: Authentication object cannot be null.

org.springframework.expression.spel.SpelEvaluationException: EL1021E: A problem occurred whilst attempting to access the property 'principal': 'EL1022E: The function 'principal' mapped to an object of type 'class org.springframework.security.access.expression.SecurityExpressionRoot' cannot be invoked'
<omitted>
Caused by: java.lang.IllegalArgumentException: Authentication object cannot be null
    at org.springframework.util.Assert.notNull(Assert.java:181)
    at org.springframework.security.access.expression.SecurityExpressionRoot.lambda$new$1(SecurityExpressionRoot.java:92)
    at org.springframework.util.function.SingletonSupplier.get(SingletonSupplier.java:106)
    at org.springframework.security.access.expression.SecurityExpressionRoot.getAuthentication(SecurityExpressionRoot.java:130)
    at org.springframework.security.access.expression.SecurityExpressionRoot.getPrincipal(SecurityExpressionRoot.java:170)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
    ... 44 more

After some digging, I found out that the reason was because SecurityContext is cleared at the end of the filter chain after the mockMvc request, hence, if we were to perform any method call to the repository layer which requires principal (Authentication) object, it will fail.

public interface UserRepository extends CrudRepository<User, String> {
    @Query("SELECT * FROM \"USER\" WHERE name = :#{ principal?.username }")
    Optional<User> findByName();
}

To fix this, we simply just need to export the SecurityContext for use after the mockMvc request

import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultHandlers.exportTestSecurityContext;

@Test
@WithMockUser(value = "bwgjoseph")
void test() throws Exception {
    this.mockMvc.perform(post("/api/v1/users/{name}", "bwgjoseph")
                    .accept(MediaType.APPLICATION_JSON)
                    .contentType(MediaType.APPLICATION_JSON))
            .andDo(exportTestSecurityContext()) // to export security context
            .andExpect(status().isCreated());

    var user = this.repository.findByName();

    assertThat(user.get()).extracting("name").isEqualTo("bwgjoseph");
}

Note that one could manually export the context, but I can’t think of any reason why one would want to do it this way.

@Test
@WithMockUser(value = "bwgjoseph")
void test() throws Exception {
    this.mockMvc.perform(post("/api/v1/users/{name}", "bwgjoseph")
            .accept(MediaType.APPLICATION_JSON)
            .contentType(MediaType.APPLICATION_JSON)
            .andExpect(status().isOk());

    SecurityContextHolder.setContext(TestSecurityContextHolder.getContext());

    var user = this.repository.findByName();

    assertThat(user.get()).extracting("name").isEqualTo("bwgjoseph");
}

Tips

If you encounter the following error

org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'principal' cannot be found on object of type 'java.lang.Object[]' - maybe not public or not valid?

Please ensure that you have the following dependency added in your project

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-data</artifactId>
</dependency>

Conclusion

We looked at how to “re-use” the SecurityContext in order to be able to verify our test by querying the database after mockMvc request. As simple as it looks, figuring this out at a larger code base took a while to realize that this is the root cause, but luckily, the solution is pretty straight-forward.

References:

Source Code

As usual, the full source code is available on GitHub

0
Subscribe to my newsletter

Read articles from Joseph Gan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Joseph Gan
Joseph Gan

Advocate for better developer's productivity and experience Documenting my learnings and sharing it