Hashing
You may hash user passwords in your application using the hash
service. AdonisJS has first-class support for bcrypt
, scrypt
, and argon2
hashing algorithms and the ability to add custom drivers.
The hashed values are stored in PHC string format. PHC is a deterministic encoding specification for formatting hashes.
Usage
The hash.make
method accepts a plain string value (the user password input) and returns a hash output.
import hash from '@adonisjs/core/services/hash'
const hash = await hash.make('user_password')
// $scrypt$n=16384,r=8,p=1$iILKD1gVSx6bqualYqyLBQ$DNzIISdmTQS6sFdQ1tJ3UCZ7Uun4uGHNjj0x8FHOqB0pf2LYsu9Xaj5MFhHg21qBz8l5q/oxpeV+ZkgTAj+OzQ
You cannot convert a hash value to plain text, hashing is a one-way process, and there is no way to retrieve the original value after a hash has been generated.
However, hashing provides a way to verify if a given plain text value matches against an existing hash, and you can perform this check using the hash.verify
method.
import hash from '@adonisjs/core/services/hash'
if (await hash.verify(existingHash, plainTextValue)) {
// password is correct
}
Configuration
The configuration for hashing is stored inside the config/hash.ts
file. The default driver is set to scrypt
because scrypt uses the Node.js native crypto module and does not require any third-party packages.
import { defineConfig, drivers } from '@adonisjs/core/hash'
export default defineConfig({
default: 'scrypt',
list: {
scrypt: drivers.scrypt(),
/**
* Uncomment when using argon2
argon: drivers.argon2(),
*/
/**
* Uncomment when using bcrypt
bcrypt: drivers.bcrypt(),
*/
}
})
Argon
Argon is the recommended hashing algorithm to hash user passwords. To use argon with the AdonisJS hash service, you must install the argon2 npm package.
npm i argon2
We configure the argon driver with secure defaults but feel free to tweak the configuration options per your application requirements. Following is the list of available options.
export default defineConfig({
// Make sure to update the default driver to argon
default: 'argon',
list: {
argon: drivers.argon2({
version: 0x13, // hex code for 19
variant: 'id',
iterations: 3,
memory: 65536,
parallelism: 4,
saltSize: 16,
hashLength: 32,
})
}
})
-
variant
-
The argon hash variant to use.
d
is faster and highly resistant against GPU attacks, which is useful for cryptocurrencyi
is slower and resistant against tradeoff attacks, which is preferred for password hashing and key derivation.id
(default) is a hybrid combination of the above, resistant against GPU and tradeoff attacks.
-
version
-
The argon version to use. The available options are
0x10 (1.0)
and0x13 (1.3)
. The latest version should be used by default. -
iterations
-
The
iterations
count increases the hash strength but takes more time to compute.The default value is
3
. -
memory
-
The amount of memory to be used for hashing the value. Each parallel thread will have a memory pool of this size.
The default value is
65536 (KiB)
. -
parallelism
-
The number of threads to use for computing the hash.
The default value is
4
. -
saltSize
-
The length of salt (in bytes). Argon generates a cryptographically secure random salt of this size when computing the hash.
The default and recommended value for password hashing is
16
. -
hashLength
-
Maximum length for the raw hash (in bytes). The output value will be longer than the mentioned hash length because the raw hash output is further encoded to PHC format.
The default value is
32
Bcrypt
To use Bcrypt with the AdonisJS hash service, you must install the bcrypt npm package.
npm i bcrypt
Following is the list of available configuration options.
export default defineConfig({
// Make sure to update the default driver to bcrypt
default: 'bcrypt',
list: {
bcrypt: drivers.bcrypt({
rounds: 10,
saltSize: 16,
version: '2b'
})
}
})
-
rounds
-
The cost for computing the hash. We recommend reading the A Note on Rounds section from Bcrypt docs to learn how the
rounds
value has an impact on the time it takes to compute the hash.The default value is
10
. -
saltSize
-
The length of salt (in bytes). When computing the hash, we generate a cryptographically secure random salt of this size.
The default value is
16
. -
version
-
The version for the hashing algorithm. The supported values are
2a
and2b
. Using the latest version, i.e.,2b
is recommended.
Scrypt
The scrypt driver uses the Node.js crypto module for computing the password hash. The configuration options are the same as those accepted by the Node.js scrypt
method.
export default defineConfig({
// Make sure to update the default driver to scrypt
default: 'scrypt',
list: {
scrypt: drivers.scrypt({
cost: 16384,
blockSize: 8,
parallelization: 1,
saltSize: 16,
maxMemory: 33554432,
keyLength: 64
})
}
})
Using model hooks to hash password
Since you will be using the hash
service to hash user passwords, you may find placing the logic inside the beforeSave
model hook helpful.
import { BaseModel, beforeSave } from '@adonisjs/lucid'
import hash from '@adonisjs/core/services/hash'
export default class User extends BaseModel {
@beforeSave()
static async hashPassword(user: User) {
if (user.$dirty.password) {
user.password = await hash.make(user.password)
}
}
}
Switching between drivers
If your application uses multiple hashing drivers, you can switch between them using the hash.use
method.
The hash.use
method accepts the mapping name from the config file and returns an instance of the matching driver.
import hash from '@adonisjs/core/services/hash'
// uses "list.scrypt" mapping from the config file
await hash.use('scrypt').make('secret')
// uses "list.bcrypt" mapping from the config file
await hash.use('bcrypt').make('secret')
// uses "list.argon" mapping from the config file
await hash.use('argon').make('secret')
Checking if a password needs to be rehashed
The latest configuration options are recommended to keep passwords secure, especially when a vulnerability is reported with an older version of the hashing algorithm.
After you update the config with the latest options, you can use the hash.needsReHash
method to check if a password hash uses old options and perform a re-hash.
The check must be performed during user login because that is the only time you can access the plain text password.
import hash from '@adonisjs/core/services/hash'
if (await hash.needsReHash(user.password)) {
user.password = await hash.make(plainTextPassword)
await user.save()
}
You can assign a plain text value to user.password
if you use model hooks to compute the hash.
if (await hash.needsReHash(user.password)) {
// Let the model hook rehash the password
user.password = plainTextPassword
await user.save()
}
Faking hash service during tests
Hashing a value is usually a slow process, and it will make your tests slow. Therefore, you might consider faking the hash service using the hash.fake
method to disable password hashing.
We create 20 users using the UserFactory
in the following example. Since you are using a model hook to hash passwords, it might take 5-7 seconds (depending on the configuration).
import hash from '@adonisjs/core/services/hash'
test('get users list', async ({ client }) => {
await UserFactory().createMany(20)
const response = await client.get('users')
})
However, once you fake the hash service, the same test will run in order of magnitude faster.
import hash from '@adonisjs/core/services/hash'
test('get users list', async ({ client }) => {
hash.fake()
await UserFactory().createMany(20)
const response = await client.get('users')
hash.restore()
})
Creating a custom hash driver
A hash driver must implement the HashDriverContract interface. Also, the official Hash drivers use PHC format to serialize the hash output for storage. You can check the existing driver's implementation to see how they use the PHC formatter to make and verify hashes.
import {
HashDriverContract,
ManagerDriverFactory
} from '@adonisjs/core/types/hash'
/**
* Config accepted by the hash driver
*/
export type PbkdfConfig = {
}
/**
* Driver implementation
*/
export class Pbkdf2Driver implements HashDriverContract {
constructor(public config: PbkdfConfig) {
}
/**
* Check if the hash value is formatted as per
* the hashing algorithm.
*/
isValidHash(value: string): boolean {
}
/**
* Convert raw value to Hash
*/
async make(value: string): Promise<string> {
}
/**
* Verify if the plain value matches the provided
* hash
*/
async verify(
hashedValue: string,
plainValue: string
): Promise<boolean> {
}
/**
* Check if the hash needs to be re-hashed because
* the config parameters have changed
*/
needsReHash(value: string): boolean {
}
}
/**
* Factory function to reference the driver
* inside the config file.
*/
export function pbkdf2Driver (config: PbkdfConfig): ManagerDriverFactory {
return () => {
return new Pbkdf2Driver(config)
}
}
In the above code example, we export the following values.
-
PbkdfConfig
: TypeScript type for the configuration you want to accept. -
Pbkdf2Driver
: Driver's implementation. It must adhere to theHashDriverContract
interface. -
pbkdf2Driver
: Finally, a factory function to lazily create an instance of the driver.
Using the driver
Once the implementation is completed, you can reference the driver inside the config file using the pbkdf2Driver
factory function.
import { defineConfig } from '@adonisjs/core/hash'
import { pbkdf2Driver } from 'my-custom-package'
export default defineConfig({
list: {
pbkdf2: pbkdf2Driver({
// config goes here
}),
}
})