Migrate Adonis.js v4 user passwords to v5

Created on 06 Jul 2021663 words・3 min read

A new version of Adonis.js isn't just a simple update, it is a complete revamp of all the core modules and structure including hashing mechanism.

Prior the update Adonis.js used plain bcrypt hashing implementation but now it became more standartized, the use of PHC string format allows to incorporate different hashers and verify the hashes against the current configuration and then decide if the hash needs to be rehashed or not.

This change leads to a situation when old v4 hashes will not be compatible with v5 and your users will not be able to login.

The way to resolve this problem I'd describe in three steps:

  1. Expand hasher with our own legacy driver
  2. On user authentication attempt check if the password has been hashed using an old hasher, if yes, use our new legacy driver
  3. Authenticate user and rehash password using a new hasher, in my case I'm using argon2

Expanding the hasher

To expand the hasher we have to create a new local provider by running a corresponding command inside our projects folder:

bash
node ace make:provider LegacyHasher

This will generate a new provider file inside /providers folder. After the file has been generated, we have to add it to .adonisrc.json into providers section.

Before actually expending we have to create a new Hash driver, as an example we can use the code provided in an official documentation here.

I created a separate folder inside /providers, named it LegacyHashDriver and placed my legacy driver there (inside an index.ts file).

ts
import bcrypt from 'bcrypt';
import { HashDriverContract } from '@ioc:Adonis/Core/Hash';
/**
 * Implementation of custom bcrypt driver
 */
export class LegacyHashDriver implements HashDriverContract {
  /**
   * Hash value
   */
  public async make(value: string) {
    return bcrypt.hash(value);
  }
  /**
   * Verify value
   */
  public async verify(hashedValue: string, plainValue: string) {
    return bcrypt.compare(plainValue, hashedValue);
  }
}

As you can see, it depends on a bcrypt package, you'll have to install it before running.

Having created a new driver, we can now expand the Hash core library.

ts
import { ApplicationContract } from '@ioc:Adonis/Core/Application';
import { LegacyHashDriver } from './LegacyHashDriver';

export default class LegacyHasherProvider {
  constructor(protected app: ApplicationContract) {}

  public async boot() {
    const Hash = this.app.container.use('Adonis/Core/Hash');

    Hash.extend('legacy', () => {
      return new LegacyHashDriver();
    });
  }
}

There are two additional things we have to do before proceeding to actual testing of implementation. We have to add our new hasher to contracts/hash.ts:

ts
declare module '@ioc:Adonis/Core/Hash' {
  interface HashersList {
    bcrypt: {
      config: BcryptConfig;
      implementation: BcryptContract;
    };
    argon: {
      config: ArgonConfig;
      implementation: ArgonContract;
    };
    legacy: {
      config: {};
      implementation: HashDriverContract;
    };
  }
}

And add it to config/hash.ts:

ts
...
  legacy: {
    driver: 'legacy',
  },
...

Authenticating users with legacy hasher

As user tries to login the first thing you do (after request validation) is user search, by email or username. When you find a corresponding record, you can check if the password hash has been generated using an old method, by testing it agains a simple regex. Then later verify it using the right hash driver.

ts
const usesLegacyHasher = /^\$2[aby]/.test(user.password);
let isMatchedPassword = false;

if (usesLegacyHasher) {
  isMatchedPassword = await Hash.use('legacy').verify(user.password, password);
} else {
  isMatchedPassword = await Hash.verify(user.password, password);
}

Rehashing old user password

Rehashing user password on login is the most convenient way to migrate to a new driver. I do this after I checked all the security things, found the user and know that the password is hashed using an old method.

ts
try {
  const token = await auth.use('api').generate(user);

  // rehash user password
  if (usesLegacyHasher) {
    user.password = await Hash.make(password);
    await user.save();
  }

  return response.ok({
    message: 'ok',
    user,
    token,
  });
} catch (e) {
  return response.internalServerError({ message: e.message });
}

Now you can test it and it should work. You can expand hasher not only to migrate from v4 to v5, but even when you try to build your app on top of existing database.