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:
legacy
driverlegacy
driverargon2
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',
},
...
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 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.