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 PasswordEncoder
s at once.
Footnote
- The code used for this tutorial can be found on GitHub.
- DelegatingPasswordEncoder – Spring Docs
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. |
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.
Did you solve this?