Migrate AdonisJS v4 user passwords to v5

bitkidd

Chirill Ceban

Posted on July 6, 2021

Migrate AdonisJS v4 user passwords to v5

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:

node ace make:provider LegacyHasher
Enter fullscreen mode Exit fullscreen mode

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).

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

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();
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

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

And add it to config/hash.ts:

...
  legacy: {
    driver: 'legacy',
  },
...
Enter fullscreen mode Exit fullscreen mode

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.

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);
}
Enter fullscreen mode Exit fullscreen mode

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.

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 });
}
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
bitkidd
Chirill Ceban

Posted on July 6, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related