Enterprise Java

Password Encoder Migration with Spring Security 5

Recently I was working in a project that used a custom PasswordEncoder and there was a requirement to migrate it to bcrypt. The current passwords are stored as hash which means it’s not possible to revert it to the original String – at least not in an easy way.

The challenge here was how to support both implementations, the old hash solution along with the new bcrypt implementation. After a little research I could find Spring Security 5’s DelegatingPasswordEncoder.

Meet DelegatingPasswordEncoder

The DelegatingPasswordEncoder class makes it possible to support multiple password encoders based on a prefix. The password is stored like this:

{bcrypt}$2a$10$vCXMWCn7fDZWOcLnIEhmK.74dvK1Eh8ae2WrWlhr2ETPLoxQctN4.
{noop}plaintextpassword

Spring Security 5 brings the handy PasswordEncoderFactories class, currently this class supports the following encoders:

public static PasswordEncoder createDelegatingPasswordEncoder() {
    String encodingId = "bcrypt";
    Map<String, PasswordEncoder> encoders = new HashMap<>();
    encoders.put(encodingId, new BCryptPasswordEncoder());
    encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
    encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
    encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
    encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
    encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
    encoders.put("scrypt", new SCryptPasswordEncoder());
    encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
    encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
    encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());

    return new DelegatingPasswordEncoder(encodingId, encoders);
}

Now instead of declaring a single PasswordEncoder we can use the PasswordEncoderFactories, like this snippet of code:

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

Adding a Custom Encoder

Now, getting back to my initial problem, for legacy reasons there is a homegrown password encoding solution, and the handy PasswordEncoderFactories knows nothing about it, to solve that I’ve created a class similar to the PasswordEncoderFactories and I’ve added all the built-in encoders along with my custom one, here’s a sample implementation:

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;

import java.util.HashMap;
import java.util.Map;

class DefaultPasswordEncoderFactories {

    @SuppressWarnings("deprecation")
    static PasswordEncoder createDelegatingPasswordEncoder() {
        String encodingId = "bcrypt";
        Map<String, PasswordEncoder> encoders = new HashMap<>();
        encoders.put(encodingId, new BCryptPasswordEncoder());
        encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
        encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
        encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
        encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
        encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
        encoders.put("scrypt", new SCryptPasswordEncoder());
        encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
        encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
        encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
        encoders.put("custom", new CustomPasswordEncoder());

        return new DelegatingPasswordEncoder(encodingId, encoders);
    }
}

And then I declared my @Bean using the DefaultPasswordEncoderFactories instead.

After my first run I realized another problem, I would have to run a SQL script to update all the existing passwords adding the {custom} prefix so the framework could properly bind the prefix with the right PasswordEncoder, don’t get me wrong it’s an fine solution but I really did not want to mess around with existing passwords in the database and luckily for us the DelegatingPasswordEncoder class allows us to set a default PasswordEncoder, it means whenever the framework tries doesn’t find a prefix in the stored password it will fallback to the default one to try to decode it.

Then I changed my implementation to the following:

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;

import java.util.HashMap;
import java.util.Map;

class DefaultPasswordEncoderFactories {

    @SuppressWarnings("deprecation")
    static PasswordEncoder createDelegatingPasswordEncoder() {
        String encodingId = "bcrypt";
        Map<String, PasswordEncoder> encoders = new HashMap<>();
        encoders.put(encodingId, new BCryptPasswordEncoder());
        encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
        encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
        encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
        encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
        encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
        encoders.put("scrypt", new SCryptPasswordEncoder());
        encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
        encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));

        DelegatingPasswordEncoder delegatingPasswordEncoder = new DelegatingPasswordEncoder(encodingId, encoders);
        delegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(new CustomPasswordEncoder());
        return delegatingPasswordEncoder;
    }

}

And the @Bean declaration is now:

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

Conclusion

Migration password encoders is a real life problem and Spring Security 5 gives a quite handy way to easily handle it by supporting multiple PasswordEncoders at once.

Footnote

Published on Java Code Geeks with permission by Marcos Barbero, partner at our JCG program. See the original article here: Password Encoder Migration with Spring Security 5

Opinions expressed by Java Code Geeks contributors are their own.

Marcos Barbero

Marcos is a hands-on Software Architect who worked on e-commerce domain for several years and currently working in the banking domain. He is very experienced in the Spring Framework ecosystem.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Alam
Alam
5 years ago

Hi, I also have the same problem. Previously I had used PasswordEncoder algorithm and now wanted to migrate to BCryptEncoderPassword as using Spring5 in my project. I tried your solution but it’s not working for me.
As you have mentioned I have added prefix “{bcrypt}………………………” in db and used your solution to set default to match the password.
Now I am trying to use .match method to match the password which is stored in db with the raw password but returning false.
Please guide me to implement the solution.

Ravi
3 years ago
Reply to  Alam

Did you solve this?

Back to top button