Fail resolve argument CustomUserDetails when I test in only SecurityAutoConfiguration and @WebMvcTest
Describe the bug
When using a CustomUserDetails(interface & extends UserDetails) and testing presentation(controller) layer via @WebMvcTest, org.springframework.data.web.ProxingHandlerMethodArgumentResolver is being used as the ArgumentResolver instead of AuthenticationPrincipalArgumentResolver.
Consequently, a null value is bound to the CustomUserDetails userDetails method parameter in the Controller class.
To Reproduce
Need to use @WebMvcTest and test a controller's handler method that hava CustomUserDetails (interface) as a parameter. Additionally, you must configure springSecurity() when setting mockMvc and conduct the test without importing any custom @EnableWebSecurity classes.
Expected behavior
I expected the CustomUserDetailsImpl value to be bound correctly, but it wasn't.
Sample
@RestController
public class TestController {
@GetMapping("/test")
public ResponseEntity<?> getTest(@AuthenticationPrincipal CustomUserDetails userDetails) {
System.out.println("user Id : " +userDetails.getUserId());
System.out.println("user : " + userDetails);
System.out.println("user name : " + userDetails.getUsername());
return ResponseEntity.ok("success");
}
}
public interface CustomUserDetails extends UserDetails {
Role getRole();
Long getUserId();
}
@Getter
@RequiredArgsConstructor
public class CustomUserDetailsImpl implements CustomUserDetails {
private final User user; // Custom User
/* ... */
}
@WebMvcTest(
controllers = TestController.class
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {OncePerRequestFilter.class})
}
)
public class TestControllerTest {
private MockMvc mockMvc;
private CustomUserDetails userDetails;
void setUpUserDetails(Role role) {
User user = User.builder()
.username("test")
.password("password")
.email("[email protected]")
.role(role)
.nickname("nickname")
.build();
ReflectionTestUtils.setField(user, "id", 1L);
userDetails = new CustomUserDetailsImpl(user);
}
@BeforeEach
void setUp(WebApplicationContext webApplicationContext) {
this.mockMvc = MockMvcBuilders
.webAppContextSetup(webApplicationContext)
.apply(springSecurity())
.build();
}
@Test
@DisplayName("CustomUserDetailsTest")
void testCustomUserDetails() throws Exception {
// given
setUpUserDetails(Role.USER);
// when
ResultActions resultActions = mockMvc.perform(
get("/test/get")
.with(user(userDetails))
);
// then
resultActions.
andExpect(status().isOk);
}
}
Motivation & Solution
I designed my project with the above structure to use polymorphism and use multiple CustomUserDetails implementations for users with various roles.
Upon debugging, I discovered that when running tests with @WebMvcTest without importing a custom @EnableWebSecurity class, SecurityAutoConfiguration.class is imported.
In this scenario, org.springframework.data.web.ProxingHandlerMethodArgumentResolver is positioned before AuthenticationPrincipalArgumentResolver, causing it to be used as the ArgumentResolver.
When using
UserDetails, the issue doesn't occur becauseUserDetailsstarts with theorg.springframeworkpackage. This causes ProxingHandlerMethodArgumentResolver.supportsParameter(MethodParameter parameter) to return false.However, I won't suggest registering the
CustomUserDetailspath as an exception to resolve this. It doesn't seem like a solution that's appropriate for the spring-security project itself.
More precisely, when WebMvcConfigurer is registered, SpringDataWebConfiguration is registered before WebMvcSecurityConfiguration. This results in springframework.data.web related ArgumentResolvers being registered first.
However, when a custom @EnableWebSecurity class is imported, WebMvcSecurityConfiguration is registered first, placing AuthenticationPrincipalArgumentResolver before ProxingHandlerMethodArgumentResolver. This eliminates the issue when CustomUserDetails is used as a method argument.
Therefore, I believe this problem can be resolved by advancing the registration order of WebMvcSecurityConfiguration as a Configurer when SpringAutoConfiguration is used.
Although this issue doesn't seem to be exclusively limited to the spring-security project, I am reporting it there first.