Password Encoder Migration With Spring Security 5
Recently, I was working on a project that used the custom PasswordEncoder
, and there was a requirement to migrate it to bcrypt. The current passwords are stored as a hash
, which means that 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’sDelegatingPasswordEncoder
.
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 thePasswordEncoderFactories
, 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 a 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 defaultPasswordEncoder
. It means that whenever the framework tries and doesn’t find a prefix in the stored password, it will fall back 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