Password Migration from Grails 2 to Grails 4. (Spring-security 3 to Spring-security 5)

Prabin Upreti
Sep 30, 2020 · 7 min read

Disclaimer : This is my first post. You have to bear it. ;D

I want to jump in straight. I was going through an upgrade of a backend system which was on grails version 2.5.1 and we wanted to upgrade to the latest grails version. I picked up version 4.0.3.

Grails plugin being used for spring-security-core for grails 2.5.1 is spring-security-core:2.0-RC6 which uses spring-security-core-3.2.9 (in spring perspective). The new app : Grails 4.0.2 uses spring-security-core:4.0.2 which in turn uses spring-security-core:5.1.8 .
So now we are using Spring-security 5.

I started upgrading the app by migrating the database at first and then spring security core. I think these are the core things to be migrated first because these are the fundamentals of an app and we wouldn’t want to go back and forth to change the codes for spring security in particular while we are upgrading other stuffs like controllers and services.

Here is the official documentation of spring-security plugin for grails.

As soon as I installed the newer version of spring-security, I came up to know that the newer version has totally changed how password is encrypted.

At the time of this writing the encryption algorithm being used is BCrypt which is considered to be the most secured among all the previous algorithms.

In the previous version of spring-security-core, it has provided some basic hashing algorithms like MD5, SHA-1, SHA-256, SHA-512, Bcrypt, Pbkdf2.
Here we can choose only one hashing algorithm. In my older version we are using SHA-512 which is mentioned in spring config in config.groovy as :

grails.plugin.springsecurity.password.algorithm = 'SHA-512'

Now whenever we save password or validate the user given password spring uses respective password encoder. In above case it uses MessageDigestPasswordEncoder. i.e :
Whenever we are doing this :


we are using MessageDigestPasswordEncoder to encode passwords.

For matching the user given input, we first encode the raw text by the same code above and just compare the encoded hash with the saved hash in database. Like this:

springSecurityService.encodePassword('randomPassword') == userPasswordHash // password from db

Enough of the background stuffs, lets jump in straight to what my problem was and how i solved it.
I still want my existing users to log in into the app. (If they can’t whats the point of doing all these :D). The problem is the new spring version doesn’t recognize the old passwords and thus it can’t validate any old password.

Why is that:
The older passwords look something like this :

The new passwords look something bit different like this:

Notice any thing different? There are many things but one thing that is clear is the prefix in the newer version which is here {bcrypt}. It simply tells that the password is hashed using BCrypt algorithm. And when this is being validated, first the prefix is checked which delegates the respective encoder which is BCryptPasswordEncoder in this case.

Remember one thing that I’ve mentioned above about using just one algorithm? I mentioned in previous spring version we can give just one hashing algorithm among the defined list and hence we used sha-512 right? But just in above paragraph i have mentioned respective encoder is delegated based on the prefix. Yes this is a huge change made by spring. Previously we can select just one algorithm but since the hashing algorithm keeps on changing may be in a longer period of time but it keeps on changing for sure. So now spring has made provision that we can assign respective password encoder based on how password is hashed by checking the prefix in run time.
Because of this provision now we can even add our custom encoder in the already existing encoders list and use our own hashing algorithm.
Bcrypt is the default one. This process of delegating password encoder is documented here.

For example : if the password is hashed like this:


then at the run time respective LdapShaPasswordEncoder is used because of the prefix.

Now going back to the problem, one of our old passwords look like this:

Here there is no prefix as such. When we try with the raw password of this hash while login we get IllegalArgumentException in backend because spring can’t find which password encoder to encode this. Which is also documented here.

I went on to find some document to solve this situation and i found this:

Here it describes exactly what the problem is and how it is solved but for a core spring app. But by this only it was bit hard for me to solve the problem in grails.

I had to do some experiments to solve this that is why i wanted to write this stuff hoping grails developers like me won’t be stuck like i was.

Now how i solved it with the help of above referenced doc:

Spring provides a PasswordEncoderFactories class which has this one method :

This method is called initially when app starts to provide the list of encoders that can be used while saving and validating passwords. Here, clearly we don’t see any encoder that supports our old password. That is because in our old password we don’t have any prefix. And here each encoder is using a prefix.

Now we need to override this class to add our custom encoder as a fallback encoder. That is : when the framework doesn’t find any prefixed password then use that custom encoder which is actually an encoder which uses old version hashing algorithm as a default encoder.

We need to create our custom password encoder factory for this. Which i created like this :

class CustomPasswordEncoderFactories {

static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt"
Map<String, PasswordEncoder> encoders = new HashMap<>()
encoders.put(encodingId, new BCryptPasswordEncoder())

DelegatingPasswordEncoder delegatingPasswordEncoder = new DelegatingPasswordEncoder(encodingId, encoders)
//setting custom encoder as a default encoder delegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(new CustomLegacyPasswordEncoder("SHA-512"))
return delegatingPasswordEncoder

private CustomPasswordEncoderFactories() {}

We have created our CustomPasswordEncoderFactories which basically returns a respective password encoder in run time.

Now we have to tell grails and spring to use the password encoder bean from this factory class replacing the existing one. How we do that? We have to define a passwordEncoder bean. Defining a bean with the existing bean name replaces the existing bean. Our bean is not a normal bean but a bean returned from a factory class. Grails has a provision to define a bean of such nature. We define beans in resources.groovy file as like this: (Doc link)

beans = {
passwordEncoder(CustomPasswordEncoderFactories){bean ->
bean.factoryMethod = "createDelegatingPasswordEncoder"

We are almost at the end. But we haven’t yet created our custom encoder. We have set up to use this encoder as a fallback now lets see how we create this.

Since my older version’s algorithm is SHA-512. The newer version has a encoder class that supports SHA-512 which is
But the implementation in older version and newer version is a bit different that is why using this class didn’t give me the desired result. The hashed password didn’t match to the older one. So what I did I created a custom class similar to but with all the implementation copied from previous version and named it

class CustomLegacyPasswordEncoder implements PasswordEncoder{

private String algorithm
private int iterations = 10000
boolean encodeHashAsBase64 = false

CustomLegacyPasswordEncoder(String algorithm) {
this.algorithm = algorithm
this.encodeHashAsBase64 = false

String mergePasswordAndSalt(String password, Object salt, boolean strict) {
if (password == null) {
password = ""

if (strict && (salt != null)) {
if ((salt.toString().lastIndexOf("{") != -1) || (salt.toString().lastIndexOf("}") != -1)) {
throw new IllegalArgumentException("Cannot use { or } in salt.toString()")

if ((salt == null) || "".equals(salt)) {
return password
} else {
return password + "{" + salt.toString() + "}"

MessageDigest getMessageDigest() throws IllegalArgumentException {
try {
return MessageDigest.getInstance(algorithm)
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("No such algorithm [" + algorithm + "]")

String encode(CharSequence rawPassword) {
rawPassword = rawPassword.toString()
Object salt = null
String saltedPass = mergePasswordAndSalt(rawPassword, salt, false)

MessageDigest messageDigest = getMessageDigest()

byte[] digest = messageDigest.digest(Utf8.encode(saltedPass))

// "stretch" the encoded value if configured to do so
for (int i = 1; i < iterations; i++) {
digest = messageDigest.digest(digest)

if (getEncodeHashAsBase64()) {
return Utf8.decode(Base64.encode(digest))
} else {
return new String(Hex.encode(digest))

boolean matches(CharSequence rawPassword, String encodedPassword) {
String pass1 = "" + encodedPassword
String pass2 = encode(rawPassword)

return PasswordEncoderUtils.equals(pass1,pass2)

boolean getEncodeHashAsBase64() {
return encodeHashAsBase64

In above class, i had to change the method names from encodePassword to encode and isPasswordvalid to matches because new version’s password encoder interface’s method names are changed.

Now we have either Bcrypt based new passwords or the existing one. We have provided encoders for both of them.
All of these hassle can be prevented by forcing user to change their password. But that might not be the case for every project.

This worked for my implementation. Your implementation of custom encoder might be different than this but i am sure the idea is moreover same.

I hope this helps !!!
Happy Coding :D

The Startup

Medium's largest active publication, followed by +773K people. Follow to join our community.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store