tests/password-hashing.test.ts.beforeCreate / beforeUpdate hook, so the plain textimport {
Prorm, DataTypes, Model, Table, Column, PrimaryKey, AutoIncrement, Secure,
} from 'ts-prorm-orm';
@Secure('password', { method: 'scrypt' })
@Table({ tableName: 'users' })
class User extends Model {
@Column(DataTypes.INTEGER())
@PrimaryKey
@AutoIncrement
declare id: number;
@Column(DataTypes.STRING(255))
declare email: string;
@Column(DataTypes.STRING(255)) // wide enough for the hash, not the password
declare password: string;
}
const prorm = new Prorm({ dialect: 'sqlite', storage: ':memory:' });
const Users = prorm.addModel(User);
await prorm.sync();@Secure is a class decorator naming a field, not a property decorator:@Secure('password') above the class, not @Secure() above the property.method is the whole configuration — there is nothing to register at boot.'scrypt' is the default and needs nothing installed; 'bcrypt' and'argon2' load their package the first time they are used.addModel() wires the hooks for you — there is no applySecureHooks() call toconst Users = prorm.model('User')!; // the model registered above
const user = await Users.create({ email: 'a@b.com', password: 'hunter2' });
user.password; // '$scrypt$…' — the hash, on the instance tooconst Users = prorm.model('User')!;
const user = await Users.findOne({ where: { email } });
if (!user || !(await user.validate(submittedPassword))) {
throw new Error('Invalid credentials');
}validate() on an instance is this check. It used to be the model'svalidateRules(), because checking a password is what theuser.validate(pin, 'pin').| Method | Needs | Notes |
|---|---|---|
'scrypt' | nothing | Node's own crypto. The default. Its parameters are stored with each hash, so raising the cost later does not invalidate existing passwords. |
'bcrypt' | npm install bcrypt, or bcryptjs | Loaded on first use. Cost factor 12. |
'argon2' | npm install argon2 | Loaded on first use. |
method also takes an adapter, which is two functions:@Secure('password', {
method: {
hash: (value) => myLib.hash(value),
verify: (plain, hashed) => myLib.verify(plain, hashed),
},
})
class User extends Model { /* … */ }@Secure('password', { method: 'argon2' })
@Secure('legacyPassword', { method: 'bcrypt' })
class User extends Model { /* … */ }
const user = await prorm.addModel(User).findOne({ where: { email } });
await user.validate(submitted, 'password');
await user.validate(submitted, 'legacyPassword');import {
Prorm, DataTypes, Model, Table, Column, PrimaryKey, AutoIncrement, AllowNull, Secure,
} from 'ts-prorm-orm';
@Secure('password', { method: 'argon2' })
@Secure('legacyPassword', { method: 'bcrypt' })
@Table({ tableName: 'users' })
class User extends Model {
@Column(DataTypes.INTEGER())
@PrimaryKey
@AutoIncrement
declare id: number;
@Column(DataTypes.STRING(255))
declare email: string;
@AllowNull(true)
@Column(DataTypes.STRING(255))
declare password: string | null; // argon2: everyone who has signed in since the move
@AllowNull(true)
@Column(DataTypes.STRING(255))
declare legacyPassword: string | null; // bcrypt: everyone who has not
}
const prorm = new Prorm({ dialect: 'postgres', url: process.env.DATABASE_URL });
const Users = prorm.addModel(User);
export async function signIn(email: string, submitted: string): Promise<User> {
const user = await Users.findOne({ where: { email } });
if (!user) throw new Error('Invalid credentials');
if (user.password) {
if (await user.validate(submitted, 'password')) return user;
} else if (user.legacyPassword && (await user.validate(submitted, 'legacyPassword'))) {
// Right password, old method: write it under the new one and drop the old hash.
await user.update({ password: submitted, legacyPassword: null });
return user;
}
throw new Error('Invalid credentials');
}legacyPassword is null on every row, drop the@Secure.setEncryptionAdapter(), createBcryptAdapter(), createArgon2Adapter() andverifySecureField() still work and are deprecated. They required amethod cannot be forgotten, because it is on the field.$2a$/$2b$/$2y$, argon2's $argon2…, scrypt's $scrypt$. Without that, loading a user,const user = await Users.findByPk(1);
user.email = 'new@example.com';
await user.save(); // password untouched{ skipIfHashed: false } only if you know your stored valuesawait user.update({ password: 'a new one' }); // hashed on the way inprorm.define() declares it on the column, which is the same thing @Securesecure: true is scrypt, which needs nothing installed. The other forms are themethod takes:save() does notawait user.validate(submitted). Two secure columns ondefine() reads the column and registers the hooks,addModel() does for the decorator.[!WARNING]
It does not hide the column.findAll()returns the hash like any other
field, andJSON.stringify(user)includes it. Exclude it
(attributes: { exclude: ['password'] }) or use@CannotViewso it never leaves the process by accident.
@Len([12, 128]) and whatever policy you