1

I'm using Spring Boot 2.1 with Java 11. I have annotated my User model with fasterxml annotations so that my password can be accepted for POST requests, but not returned for other REST requests ...

@Data
@Entity
@Table(name = "Users")
public class User implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private UUID id;
    
    
    private String firstName;
    private String lastName;
    
    @NotBlank(message = "Email is mandatory")
    @Column(unique=true)
    private String email;
    
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    private String password;
    private boolean enabled;
    private boolean tokenExpired;
 
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable( 
        name = "users_roles", 
        joinColumns = @JoinColumn(
          name = "user_id", referencedColumnName = "id"), 
        inverseJoinColumns = @JoinColumn(
          name = "role_id", referencedColumnName = "id")) 
    private Collection<Role> roles;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public String getUsername() {
        return email;
    }

    @Override
    public boolean isAccountNonExpired() {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public boolean isAccountNonLocked() {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        // TODO Auto-generated method stub
        return false;
    }    

    @PrePersist @PreUpdate 
    private void prepare(){
        this.email = this.email.toLowerCase();
    }
}

However, when trying to run an integration test, the password is not getting translated by "objectMapper.writeValueAsString". Here is my test ...

@SpringBootTest(classes = CardmaniaApplication.class, 
webEnvironment = WebEnvironment.RANDOM_PORT)
public class UserControllerIntegrationTest {
    
    @Autowired
private TestRestTemplate restTemplate;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private IUserRepository userRepository;

    @Test
    @WithUserDetails("me@example.com")
    void registrationWorksThroughAllLayers() throws Exception {
        final String email = "newuser@test.com";
        final String firstName = "first";
        final String lastName = "last";
        final String password = "password";
        User user = getTestUser(email, password, firstName, lastName, Name.USER);
    
        ResponseEntity<String> responseEntity = this.restTemplate
            .postForEntity("http://localhost:" + port + "/api/users", user, String.class);
    assertEquals(201, responseEntity.getStatusCodeValue());

        final User createdUser = userRepository.findByEmail(email);
        assertNotNull(createdUser);
        assertNotNull(createdUser.getPassword());
    }

    @Test
    @WithUserDetails("me@example.com")
    void getDetailsAboutMyself() throws JsonProcessingException, JSONException {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        UserDetails user = (UserDetails) authentication.getPrincipal();
        final User foundUser = userRepository.findByEmail(user.getUsername());
        ResponseEntity<String> responseEntity = this.restTemplate
            .getForEntity("http://localhost:" + port + "/api/users/" + foundUser.getId(), String.class);
        assertEquals(200, responseEntity.getStatusCodeValue());
        // assert proper response
        final String userAsJson = objectMapper.writeValueAsString(user);
        assertEquals(userAsJson, responseEntity.getBody());
        JSONObject object = (JSONObject) new JSONTokener(userAsJson).nextValue();
        // Verify no password is returned.
        assertNull(object.getString("password"));
    }
    ...
}

The JSON from the objectMapper.writeValueAsString call is

{"id":null,"firstName":"first","lastName":"last","email":"newuser@test.com","enabled":true,"tokenExpired":false,"roles":null,"username":"newuser@test.com","authorities":null,"accountNonExpired":false,"accountNonLocked":false,"credentialsNonExpired":false}

What's the proper way to get my password included as part of the mapping as well as suppressing the password when requesting my entity from read endpoints?

satish
  • 703
  • 5
  • 23
  • 52
  • You either have an `@SpringBootTest` **or** an `@DataJpaTest` combining both will not work (the first is a full integration test, the other only a JPA based slice of that, so which is it ?). Also what is wrong with the JSON? You specified that the `password` field shouldn't be part of the JSON when JSON is being produced, only when JSON is being received. It basically does **exactly** what you told it to do. – M. Deinum Jun 30 '20 at 05:40

2 Answers2

0

This is a common misunderstanding, there was even a bug report for it and they clarified the documentation.

"READ" and "WRITE" are to be understood from the perspective of the Java Property, i.e., when you serialize an Object, you have to read the property and when you deserialize it, you have to write it.

In your case, you want @JsonProperty(access = JsonProperty.Access.READ_ONLY)

Benjamin Maurer
  • 3,602
  • 5
  • 28
  • 49
  • I changed from WRITE_ONLY to READ_ONLY and back again but in both cases I get the same error when running my integration test. – satish Jun 29 '20 at 22:11
  • Have you verified whether 'password' is part of the JSON produced and what exactly is the error you get? Your question was only about including that data in serialization and that should work. – Benjamin Maurer Jun 30 '20 at 07:54
  • For the "READ_ONLY" the error is 'java.lang.IllegalArgumentException: rawPassword cannot be null' when saving through the POST endpoint (the "registrationWorksThroughAllLayers" test). However the GET endpoint (the "getDetailsAboutMyself" test) fails as well because the password is getting included in the JSON when it shouldn't be. – satish Jul 12 '20 at 18:07
0
  • Using WRITE_ONLY or READ_ONLY will not work in your case. The reason is one call over http needs both. Lets take this.restTemplate.postForEntity as an example. In the sending side, your User java object need to serialised to json so it needs the READ and when the rest endpoint receives the json, it need to deserialise the json into User java object so needs the WRITE. It will be the same scenario for this.restTemplate.getForEntity too

  • One solution is to set the password field of User on the GET endpoint to null before returning

  • Another solution is create a separate UserDto without password field and return it from GET endpoint

  • Another solution is to create two JsonViews where one is with password and other one is without password. Then annotate your endpoints with correct @JsonView