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

Table of contents

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
asbwgjoseph
. And that is why the code above for@WithMockUser
is also defined as such. This is to facilitate the@Query
used inUserRepository
to extract the correct username via theprincipal?.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:
https://github.com/spring-projects/spring-data-mongodb/issues/2906
https://github.com/spring-projects/spring-security/issues/9565
Source Code
As usual, the full source code is available on GitHub
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