Understanding Password Encoders in Spring Security

Since Spring Security 5, numerous changes happened to how passwords are handled within the security context. The major change was how the framework started making developers encode or hash the passwords when storing and validating them.

If passwords are stored in plain text, the security would be compromised by anyone who has access to the database. So it makes sense why Spring people chose to make these changes.

Password Hashing

Hashing algorithms take a sequence of bytes and turn into a unique fixed-length hash string.

Plain Text to Hash using password encoders

Hashing algorithms are one-way functions and cannot be reversed. This means the original plain text cannot be generated back from a hash.

This property makes the hashing viable for storing passwords.

Spring Security Password Encoder

For the password encoding/hashing, Spring Security expects a password encoder implementation. Also, it provides dogmatic implementations based on industry standards. These encoders will be used in the password storing phases and validation phase of authentication.

The passwordEncoders have two main tasks. They are,

  1. encoder.encode(String rawPassword) – Which is to convert a given plaintext password into an encoded password. How it converts is up to the implementation. This part happens at the time when the password is stored in the DB. Usually when registering a user or changing the password.
  2. encoder.matches(rawPassword, encodedPassword) – Used whenever login happens. Security context will load the encrypted password from the database and use this method to compare against the raw password. The idea here is that the same passwordEncoder can recompute the hash for the submitted login password and see if it matches the one that we stored already.

Here is how the encoders play role in the registration process.

The following diagram illustrates how Spring Security uses encoder for validating the login password.

now let’s see some examples.

Registration with password encoding

At the time of registration, you need to encode the password before storing it to the database. For this, you need to define a bean of type PasswordEncoder. At the time of writing the best implementation is to use the BCrypt algorithm. Here is how you can do it.

    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

And somewhere in your registration API endpoint, you will have to autowire this bean. Here is a simple example.

@RestController
public class RegistrationController {


    private final UserAccountRepository userAccountRepository;
    private final PasswordEncoder passwordEncoder;

    public UserController(UserAccountRepository userAccountRepository, PasswordEncoder passwordEncoder) {
        this.userAccountRepository = userAccountRepository;
        this.passwordEncoder = passwordEncoder;
    }


    @PostMapping("/register")
    public UserAccount register(@RequestParam("username") String username, @RequestParam("password") String password, @RequestParam("firstName") String firstName, @RequestParam("lastName") String lastName) {
        UserAccount userAccount = new UserAccount();
        userAccount.setFirstName(firstName);
        userAccount.setLastName(lastName);
        userAccount.setUsername(username);
        userAccount.setActive(true);
        userAccount.setPassword(passwordEncoder.encode(password));
        return userAccountRepository.save(userAccount);
    }
}

You can check out git repository for the full implementation at the end of this post. In this example we registered the user with the help of BCryptPasswordEncoder. You can see how the passwords had been encoded in the below picture.

Migrating passwords from Spring Security 4

In case if you have been using older spring boot applications that use spring security version 4, then you will get an error for missing encoder while upgrading to spring security 5. In this case, you need to follow the above steps to define a password encoder. Along with this, you need to make sure that the current passwords are encrypted using the same algorithm. Here is a sample code to convert plaintext passwords to hashes.

public class BCryptConverter {
    public static void main(String[] args) {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        System.out.println(bCryptPasswordEncoder.encode("Hello@123"));
        System.out.println(bCryptPasswordEncoder.encode("Hello#123"));
    }
}

DelegatingPasswordEncoder

There may be situations if you wanted to use multiple types of encoders within the same data source. For example, MD5, SHA-256, pbkdf2 are some common password hashing functions. To make your application to have a wide range of support for the password encoding, and this is where DelegatingPasswordEncoder comes into the picture.

This encoder relies on other password encoders by routing the requests based on a password prefix. To use this, you need to make some changes to our previous arrangement.

You need to create the bean for type DelegatingPasswordEncoder instead of BCryptPasswordEncoder and you can do this easily with the help of PasswordEncoderFactories class.

    @Bean
    PasswordEncoder passwordEncoder(){
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

This delegating encoder encodes with bcrypt algorithm by default. This is why the password stored in the database will be prepended with the text {bcrypt}. This prepended information will be used to identify the appropriate passwordEncoder when encoder.matches() method is called.

At the time of writing the default mapping for encoding type is as shown below.

EncodingIdImplementation to match
bcryptnew org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder();
ldapnew org.springframework.security.crypto.password.LdapShaPasswordEncoder();
MD4new org.springframework.security.crypto.password.Md4PasswordEncoder();
MD5new org.springframework.security.crypto.password.MessageDigestPasswordEncoder(“MD5”);
noopnew org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance();
pbkdf2new org.springframework.security.crypto.password.Pbkdf2PasswordEncoder();
scryptnew org.springframework.security.crypto.scrypt.SCryptPasswordEncoder();
SHA-1new org.springframework.security.crypto.password.MessageDigestPasswordEncoder(“SHA-1”);
SHA-256new org.springframework.security.crypto.password.MessageDigestPasswordEncoder(“SHA-256”);
sha256new org.springframework.security.crypto.password.StandardPasswordEncoder();
argon2new org.springframework.security.crypto.argon2.Argon2PasswordEncoder();

Customizing DelegatingPasswordEncoder

You can customize the list of supported encoding types by creating the DelegatingPasswordEncoder by your own. For example, The following would only support MD5, bcrypt and plainText(noop) encoding.

    @Bean
    PasswordEncoder passwordEncoder() {
        Map<String, PasswordEncoder> encoders = new HashMap<>();
        encoders.put("noop", NoOpPasswordEncoder.getInstance());
        encoders.put("bcrypt", new BCryptPasswordEncoder());
        encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
        return new DelegatingPasswordEncoder("bcrypt", encoders);
    }

Migrating old plaintext passwords to bcyrpt

The delegating encoder will allow both encoded and plain text passwords to co-exist. However, there is still a security risk for those passwords which are not encoded. In these situations, Write a program that will convert all plain text passwords to encoded strings. Here is a sample for you to try with.

public class BCryptConvert {
    public static void main(String[] args) {
        PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
        System.out.println(passwordEncoder.encode("Hello@123"));
        System.out.println(passwordEncoder.encode("Hello#123"));
    }
}

Leave a Comment