7

How should I encode automatically the subbmitted plain password field of my entity with Spring Data REST?

I'm using BCrypt encoder and I want to automatically encode the request's password field, when the client send it via POST, PUT and PATCH.

@Entity
public class User {
  @NotNull
  private String username;
  @NotNull
  private String passwordHash;
  ...
  getters/setters/etc
  ...
}

First I tried to solve with @HandleBeforeCreate and @HandleBeforeSave event listeners but the User in it's argument is already merged, so I can't make any difference between the User's new password, or the old passwordHash:

@HandleBeforeSave
protected void onBeforeSave(User user) {
    if (user.getPassword() != null) {
        account.setPassword(passwordEncoder.encode(account.getPassword()));
    }
    super.onBeforeSave(account);
}

Is that possible, to use @Projection and SpEL on a setter method?

Plutoz
  • 694
  • 9
  • 13

3 Answers3

14

You can implement a Jackson JsonDeserializer:

public class BCryptPasswordDeserializer extends JsonDeserializer<String> {

    public String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
        ObjectCodec oc = jsonParser.getCodec();
        JsonNode node = oc.readTree(jsonParser);
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        String encodedPassword = encoder.encode(node.asText());
        return encodedPassword;
    }
}

And apply it to your JPA Entity property:

// The value of the password will always have a length of 
// 60 thanks to BCrypt
@Size(min = 60, max = 60)
@Column(name="password", nullable = false, length = 60)
@JsonDeserialize(using = BCryptPasswordDeserializer.class )
private String password;
robgmills
  • 360
  • 2
  • 9
  • This is a really great answer - for this question. However, this works only when deserializing an entity into a Java object. For a REST api, this is sufficient, but if your api is fluent too, or somehow attempted to save entities within your code using JPA repositories, you can no longer benefit from this. The passwords are then persisted as is. Any idea how we can achieve the later? – KareemJ Jan 05 '21 at 10:01
3

Modifying setter method of password field is sufficient, as shown below:

public void setPassword(String password) {
        PasswordEncoder encoder = new BCryptPasswordEncoder();
        this.password = encoder.encode(password);
    }

Refer: https://github.com/charybr/spring-data-rest-acl/blob/master/bookstore/src/main/java/sample/sdr/auth/bean/UserEntity.java

charybr
  • 1,888
  • 24
  • 29
  • 2
    It will not work because I don't use separated DTO but my app shares Entities with the persistance layer, so the password hash setted by ORM (in this case Hibernate) will be encoded again. – Plutoz May 18 '15 at 06:15
  • But @javax.persistence.Access(AccessType.FIELD) seems solve Hibernate conflict with password setter. – Grigory Kislin Nov 20 '15 at 10:22
  • This probably doesn't work because if your `AuthenticationProvider` also uses a password encoder (and it should), you'll have a mismatch, because then the `AuthenticationProvider` would attempt encoding an already encoded password, and thus the authentication fails. No downvotes though. No hard feelings. Good suggestion, but won't work. – KareemJ Jan 05 '21 at 10:04
1

Some enhancement to @robgmills JsonDeserializer solution:

  • In Spring 5 introduce DelegatingPasswordEncoder. It is more flexible, see spring docs.
  • It is not nesessary to create PasswordEncoder every time at deserialization.
  • A big projects may has several JsonDeserializer's - better make them inner classes.
  • Usually encoding password hidden for get request. I've used @JsonProperty(access = JsonProperty.Access.WRITE_ONLY), see https://stackoverflow.com/a/12505165/548473

For Spring Boot code looks like:

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    public static final PasswordEncoder PASSWORD_ENCODER = PasswordEncoderFactories.createDelegatingPasswordEncoder();

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
       auth.userDetailsService(userDetailsService()).passwordEncoder(PASSWORD_ENCODER);
    }
    ....

public class JsonDeserializers {
    public static class PasswordDeserializer extends JsonDeserializer<String> {
        public String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
            ObjectCodec oc = jsonParser.getCodec();
            JsonNode node = oc.readTree(jsonParser);
            String rawPassword = node.asText();
            return WebSecurityConfig.PASSWORD_ENCODER.encode(rawPassword);
        }
    }
    ...

@Entity
public class User ...

    @Column(name = "password")
    @Size(max = 256)
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    @JsonDeserialize(using = JsonDeserializers.PasswordDeserializer.class)
    private String password;
    ...
Grigory Kislin
  • 16,647
  • 10
  • 125
  • 197