Migrate AdonisJS v4 user passwords to v5
Tags: adonisjs, adonis, javascript
Published: 2021-07-06 16:15:10

A new version of [Adonis.js](https://adonisjs.com) 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](https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md) 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
```

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](https://docs.adonisjs.com/guides/security/hashing#adding-a-custom-driver).

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.