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:
- Expand hasher with our own
legacy
driver - On user authentication attempt check if the password has been hashed using an old hasher, if yes, use our new
legacy
driver - 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.
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);
}
}
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();
});
}
}
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;
};
}
}
And add it to config/hash.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.
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.
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.