How to Migrate Passwords from Bcrypt to Argon Hashes in AdonisJS

ยท

2 min read

You aren't alone on this. I've had to migrate several production applications which were running with Adonisjs V4 (using the bcrypt hashing algorithm) to Adonisjs V5 (using the argon hashing algorithm). The challenge here is ensuring existing users (before the migration) can still log in seamlessly.

Here is how to ensure that your users whose passwords were hashed with bcrypt can still login while progressively migrating their password hashes to the argon algorithm.

  1. In your User model, add a method to check if the user's password is still hashed with the bcrypt algorithm:
public usingOldPassword(oldPassword?: string) {
    const self = this
    const password = oldPassword ?? self.password
    return !!password && ['$2a$', '$2b$', '$bcrypt'].some((pattern) => password.startsWith(pattern))
  }

In the above usingOldPassword method, self points at this - which is an instance of the User model. The oldPassword parameter is the user's password hash and is optional. If not supplied from the outside, it uses the user's password hash stored on the database.

bcrypt password hashes start with either $2a$ or $2b$, or $bcrypt. See: https://en.wikipedia.org/wiki/Bcrypt

  1. In your password verification logic, verify the user's password with either bcrypt or argon:
/**
 * Verify password
 */
const passwordMatched = user.usingOldPassword()
      ? await bcrypt.compare(this.payload.password,                      user.password)
      : await Hash.verify(user.password, this.payload.password)

if (!passwordMatched) {
      // Handle as your wish
  }

In the above snippet, we check if the user is using the old password. If true, we compare the password hash using bcrypt. If false, we compare using the default Adonisjs hashing driver which is argon.

  1. Before you generate the token, overwrite the bcrypt hash with the argon hash:
  if (user.usingOldPassword()) {
    await user.merge({ password }).save()            
  }

  token = await auth!.use('api').attempt(email, password, {           expiresIn: loginExpiresAt })
  return token

The above snippet will resave the user's password using the argon algorithm if the user is still using the bcrypt hash. Ensure that you have the following beforeSave hook installed:

@beforeSave()
  public static async hashPassword(user: User) {
    if (user.$dirty.password) {
      user.password = await Hash.make(user.$dirty.password)
    }
}

This strategy could be applied for migrating password hashes from any hash algorithm to another with little adaptation - especially for hashing algorithms which require secrets.

If this worked for you or you have improvements, please drop a comment. All the best with your hash migrations ๐Ÿš€

ย