Skip to main content

max dev blog & other stuff

Mocking @AuthenticationPrincipal with custom UserDetails object

Table of Contents

Lately I’ve been working on a project involving Spring Boot and Spring Security. It includes stateless API for authenticating users and restricting access to specific HTTP endpoints. I followed this Stateless Authentication with Spring Security[0] article. Sessions associated with users are stored in the database table, while users authenticate with session ID in Authorization request header. Pretty much what JWT does, except for I didn’t know about its existene at the moment, lol.

NOTE: This article follows my journey of debugging the problem, trying different wrong approaches and eventually finding the solution. If it’s tldr for you, jump straight to the solution.

Anyway, I took an advantage of Authentication storing principal object and created a class implementing UserDetails interface holding my DTO with mapped user entity:

@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {

    @Getter
    private final UserDTO user;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        RoleDTO role = user.getRole();
        Set<GrantedAuthority> rolesSet = new HashSet<>();
        rolesSet.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleName()));
        return rolesSet;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return user.getIsActive();
    }

    @Override
    public boolean isAccountNonLocked() {
        return user.getIsActive();
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return user.getIsActive();
    }

    @Override
    public boolean isEnabled() {
        return user.getIsActive();
    }
}

Essentially, a proxy class implementing UserDetails with a UserDTO composed in and using its properties for resolving UserDetails implemented methods. This approach allowed me to very easily access properties of a user accessing an endpoint. Consider this GET /user/images/ private endpoint, which is supposed to return authenticated user’s uploaded images:

public ResponseEntity<List<ImageDTO>> userImages(@AuthenticationPrincipal CustomUserDetails userDetails) {
       UserDTO user = userDetails.getUser();

       Optional<List<ImageDTO>> images = Optional.ofNullable(imageDTOService.findAllUploadedBy(user));
       if (images.isEmpty()) {
           return ResponseEntity.status(HttpStatus.OK).body(Collections.emptyList());
       }

    return ResponseEntity.status(HttpStatus.OK).body(images.get());
} 

To me it looks nice and elegant. This custom UserDetailsService returns CustomUserDetails based on what persistence layer returns, so the encapsulated DTO object can always be used for interaction with other DTOs or persistence services.

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final UserDTOService userDTOService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        Optional<UserDTO> user = Optional.ofNullable(userDTOService.findByUsername(username));

        if (user.isEmpty()) {
            throw new UsernameNotFoundException("Username " + username + " not found");
        }

        return new CustomUserDetails(user.get());
    }
}

# How do I test this controller?

However, this approach implies some issues when it comes to testing the controller.

While trying to test UserControllerImpl.userImages() I realized I need to somehow mock the parameter @AuthenticationPrincipal CustomUserDetails userDetails. Not knowing much about it, I did some googling and found out, that others[1] use @WithMockUser[2] annotation in situations like that. However, this turned out unsatisfactionary for my case, since the method I want to test doesn’t just restrict an access to some commonly-used resource. If I was, for example, testing the accessibility of resources that should only be used by specified group of users, that would be perfect. Using @WithMockUser I can specify roles and/or authorities required to access the resource, then test if access is granted (or properly rejected for mocked users with insufficient authorities). Or even a simpler case, having an endpoint returning user’s username. Just a quick @WithMockUser(username = "foo") and a job is done!

Meanwhile, my resource uses the principal to pass the encapsulated user DTO to the service. It delegates the job to the persistence layer, which requires DTO object which can be later be mapped to full-fleged entity. Therefore, I needed full-fleged authentication principal mocking.

# Trying to mock the principal by interference in Security Context

What exactly is @AuthenticationPrincipal? According to the Spring Security documentation[3], it ties up a method argument with Authentication.getPrincipal() return object. Authentication object can be easily obtained from SecurityContextHolder, so using some help of this stack overflow answer[4] I came up with these few lines of code:

@SpringBootTest
@AutoConfigureMockMvc
class UserControllerImplTest {

    @Test
    public void whenGetUserImages_andUserIsAuthorized_thenReturnListOfImages() throws Exception {
        String username = "username";
        UserDTO userDTO = getSampleUser(username);
        CustomUserDetails userDetails = new CustomUserDetails(userDTO);

        Authentication authentication = Mockito.mock(Authentication.class);
        SecurityContext securityContext = Mockito.mock(SecurityContext.class);
        AuthCookieFilter authCookieFilter = Mockito.mock(AuthCookieFilter.class);


        Mockito.when(securityContext.getAuthentication()).thenReturn(authentication);
        SecurityContextHolder.setContext(securityContext);

        Mockito.when(authentication.getPrincipal()).thenReturn(userDetails);

        Mockito.doNothing().when(authCookieFilter).doFilter(Mockito.any(ServletRequest.class), Mockito.any(ServletResponse.class), Mockito.any(FilterChain.class));

        List<ImageDTO> images = getSampleImages();
        images.forEach(imageDTO -> imageDTO.setUploader(userDTO));

        Mockito.when(imageDTOService.findAllUploadedBy(userDTO)).thenReturn(images);

        mockMvc.perform(get("/user/images/"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(content().json("expected json response content goes here");
    }

    private UserDTO getSampleUser(String username) {
        return UserDTO.builder()
                .username(username)
                .email(username + "@example.com")
                .password("password")
                .registerTime(LocalDateTime.of(2020, 1, 1, 12, 0, 0, 0))
                .isActive(true)
                .build();
    }

    private List<ImageDTO> getSampleImages() {
        ImageDTO image1 = getSampleImage(1L, "token1");
        ImageDTO image2 = getSampleImage(2L, "token2");
        ImageDTO image3 = getSampleImage(3L, "token3");

        return List.of(image1, image2, image3);
    }

    private ImageDTO getSampleImage(Long id, String token) {
        return ImageDTO.builder()
                .id(id)
                .token(token)
                .isActive(true)
                .isPublic(true)
                .uploadTime(LocalDateTime.of(2020, 1, 1, 12, 0, 0, 0))
                .build();
    }
}

AuthCookieFilter is my request filter (GenericFilterBean), scanning requests for Authorization token, looking their content up in database and setting Authentication in SecurityContextHolder if it matches any. I had to silence it, since I’m setting up the Security Context up manually.

Unfortunately, it didn’t work. For some reason, I couldn’t get pass authorization, getting 401 responses all the time. After some time debugging, I found that one of the first culprits of failing the authentication is ProviderManager.authenticate() throwing a ProviderNotFoundException[5]. So, there’s no AuthenticationProvider capable of handling my Authentication, huh? What’s the Authentication now, anyway? According to the debugger: Authentication$MockitoMock. Honestly, I don’t really know much about how mocks work internally. How are public properties and methods made visible, how references to them are being passed and most importantly, how is such an object perceived by other components of the application? My guess was that ProviderManager can’t match any provider with my not-so-ordinary object.

And so, I decided to abadon this idea and keep looking further.

# Injecting custom UserDetailsService

Then I stumbled upon this stack overflow answer[6], that made me aware of @WithUserDetails[7] annotation. Similarly to @WithMockUser it allows to inject a mock user to the request, but delegating the job of creating UserDetails object to the developer. @WithMockUser is higher level functionality, creating a simple UserDetails based on input parameters. With @WithUserDetails one needs to provide own UserDetailsService.

Perfect! I created additional configuration class for such a custom service:

@TestConfiguration
public class SpringSecurityForUserControllerImplTestConfig {

    @Bean
    @Primary
    public UserDetailsService userDetailsService() {

        UserDTO userDTO = UserDTO.builder()
                .username("username")
                .email("username@example.com")
                .password("password")
                .registerTime(LocalDateTime.of(2020, 1, 1, 12, 0, 0, 0))
                .isActive(true)
                .role(RoleDTO.builder().roleName("USER").build())
                .build();
        CustomUserDetails userDetails = new CustomUserDetails(userDTO);

        return new InMemoryUserDetailsManager(List.of(userDetails));
    }
}

added @SpringBootTest(classes = SpringSecurityForUserControllerImplTestConfig.class) above tests class and @WithUserDetails("username") above the test. To retrieve UserDTO object inside test method I used the following line:

UserDTO userDTO = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUser();

which unfortunately resulted in ClassCastException between org.springframework.security.core.userdetails.User and my CustomUserDetails. It turns out, that InMemoryUserDetailsManager method has this method:

    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserDetails user = (UserDetails)this.users.get(username.toLowerCase());
        if (user == null) {
            throw new UsernameNotFoundException(username);
        } else {
            return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
        }
    }

explictly casting found principals to Spring generic User class, despite my custom implementaion of UserDetails put into it. That’s sad.

# Final solution

Here’s test-scope class with custom UserDetailsManager implementation altering InMemoryUserDetailsManager.loadUserByUsername unwanted behavior. It could’ve been done better, this implementation assumes there is only one user object stored inside by the Manager. UserDTO was turned into a Bean, to allow easy access to it via autowiring from the test class.

@TestConfiguration
public class SpringSecurityForUserControllerImplTestConfig {

    @Bean
    public UserDTO testUser() {
        return UserDTO.builder()
                .username("username")
                .email("username@example.com")
                .password("password")
                .registerTime(LocalDateTime.of(2020, 1, 1, 12, 0, 0, 0))
                .isActive(true)
                .role(RoleDTO.builder().roleName("USER").build())
                .build();
    }

    @Bean
    @Primary
    public UserDetailsService userDetailsService() {

        CustomUserDetails userDetails = new CustomUserDetails(testUser());

        return new UserDetailsManager() {

            private final InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager(List.of(userDetails));


            @Override
            public void createUser(UserDetails userDetails) {
                this.inMemoryUserDetailsManager.createUser(userDetails);
            }

            @Override
            public void updateUser(UserDetails userDetails) {
                this.inMemoryUserDetailsManager.updateUser(userDetails);
            }

            @Override
            public void deleteUser(String s) {
                this.inMemoryUserDetailsManager.deleteUser(s);
            }

            @Override
            public void changePassword(String s, String s1) {
                this.inMemoryUserDetailsManager.changePassword(s, s1);
            }

            @Override
            public boolean userExists(String s) {
                return this.inMemoryUserDetailsManager.userExists(s);
            }

            @Override
            public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
                return new CustomUserDetails(testUser());
            }
        };
    }
}

Test class now looks as following:

@SpringBootTest(
        classes = SpringSecurityForUserControllerImplTestConfig.class
)
@AutoConfigureMockMvc
class UserControllerImplTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private ImageDTOService imageDTOService;

    @Autowired
    UserDTO testUser;

    @Test
    @WithUserDetails("username")
    public void whenGetUserImages_andUserIsAuthorized_thenReturnListOfImages() throws Exception {

        List<ImageDTO> images = getSampleImages();
        images.forEach(imageDTO -> imageDTO.setUploader(testUser));

        Mockito.when(imageDTOService.findAllUploadedBy(testUser)).thenReturn(images);

        mockMvc.perform(get("/user/images/"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(content().json(
                        "[\n" +
                                "  {\n" +
                                "    \"id\": 1,\n" +
                                "    \"isActive\": true,\n" +
                                "    \"isPublic\": true,\n" +
                                "    \"title\": null,\n" +
                                "    \"token\": \"token1\",\n" +
                                "    \"uploadTime\": \"2020-01-01T12:00:00\",\n" +
                                "    \"uploader\": \"username\"\n" +
                                "  },\n" +
                                "  {\n" +
                                "    \"id\": 2,\n" +
                                "    \"isActive\": true,\n" +
                                "    \"isPublic\": true,\n" +
                                "    \"title\": null,\n" +
                                "    \"token\": \"token2\",\n" +
                                "    \"uploadTime\": \"2020-01-01T12:00:00\",\n" +
                                "    \"uploader\": \"username\"\n" +
                                "  },\n" +
                                "  {\n" +
                                "    \"id\": 3," +
                                "    \n" +
                                "    \"isActive\":true,\n" +
                                "    \"isPublic\": true,\n" +
                                "    \"title\": null,\n" +
                                "    \"token\": \"token3\",\n" +
                                "    \"uploadTime\": \"2020-01-01T12:00:00\",\n" +
                                "    \"uploader\": \"username\"" +
                                "  }\n" +
                                "]"
                ));
    }

And it passes the test at last! 🎉🥰

[0]: https://golb.hplar.ch/2019/05/stateless.html
[1]: https://stackoverflow.com/questions/46615504/springboottest-mock-authentication-principal-with-a-custom-user-does-not-work
[2]: https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/test/context/support/WithMockUser.html
[3]: https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/core/annotation/AuthenticationPrincipal.html
[4]: https://stackoverflow.com/a/46631015/6158307
[5]: https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/authentication/ProviderNotFoundException.html
[6]: https://stackoverflow.com/a/43920932/6158307
[7]: https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/test/context/support/WithUserDetails.html