Skip to content

Fail resolve argument CustomUserDetails when I test in only SecurityAutoConfiguration and @WebMvcTest #17383

Open
@hky035

Description

@hky035

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 because UserDetails starts with the org.springframework package. This causes ProxingHandlerMethodArgumentResolver.supportsParameter(MethodParameter parameter) to return false.

However, I won't suggest registering the CustomUserDetails path 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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions