SecurityContextHolder is not populated in `@BeforeAll/PostConstruct` within `@WithUserDetails`
Describe the bug
I'm not sure if this is the intended behavior where SecurityContextHolder is not populated or accessible within @BeforeAll/PostConstruct. I searched the repository and found https://github.com/spring-projects/spring-security/issues/6591 is quite close to what I experience/encounter.
This is reproduced using Spring Boot 3.3.4 but I encountered it in my project which is using Spring Boot 3.2.5, with Java 21 and JUnit 5
To Reproduce
This is the code snippet to reproduce
@ExtendWith(SpringExtension.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DemoBugTests {
SoftAssertions softAssertions = new SoftAssertions();
@PostConstruct
void postConstruct() {
// authentication object is null
// SecurityContextImpl [Null authentication]
softAssertions.assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull(); // this fail
}
@BeforeAll
void beforeAll() {
// authentication object is null
// SecurityContextImpl [Null authentication]
softAssertions.assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull(); // this fail
}
@BeforeEach
void beforeEach() {
// SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=bwgjoseph, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, CredentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[admin]], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[admin]]]
softAssertions.assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull(); // this pass
}
@Test
@WithUserDetails
void validateAuthentication() {
// SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=bwgjoseph, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, CredentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[admin]], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[admin]]]
softAssertions.assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull(); // this pass
softAssertions.assertAll();
}
@TestConfiguration
static class SecurityConfiguration {
@Bean
UserDetailsService userDetailsService() {
return new TestUserDetails();
}
}
static class TestUserDetails implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return new User("bwgjoseph", "pass", List.of(new SimpleGrantedAuthority("admin")));
}
}
}
Expected behavior
I would expect that the authentication object is available within @BeforeAll and PostConstruct method.
I'm not using
@PostConstructactually, but added in this test to see the behavior
Extra Note
I discovered this as my current test is failing where there's a method call to insert certain data in @BeforeAll which uses SpEL within the repository method
@Query(...#{principal}...)
Data findById(...)
So the stack trace looks like the following...
... SpelEvaluationException: EL1022E: A problem occurred whilst attempting to access the property 'principal': The function 'principal' mapped to an object of type 'class org.springframework.security.access.expression.SecurityExpressionRoot' cannot be invoked
// omitted
... SpelEvaluationException: EL1022E: The function 'principal' mapped to an object of type 'class org.springframework.security.access.expression.SecurityExpressionRoot' cannot be invoked
So after some tracing, I found out that it was because the authentication object was null when called in @BeforeAll, and thus, causing the test to fail.
I'm also using custom @WithSecurityContext to provide my own @WithMockXXXUser annotation if that matters.
Let me know if more information is required.
Thanks!