Passwords
WIPAuthScryptHasher (built-in) and AuthArgon2idHasher (lazy peerDep). Hash, verify, and rehash on successful sign-in.
The passwords facet
auth.passwords is a thin wrapper around the configured AuthHasher.IHasher.
It owns three operations:
const stored = await auth.passwords.hash('correct horse battery staple')
const ok = await auth.passwords.verify('correct horse...', stored)
const drift = auth.passwords.needsRehash(stored)
The password provider calls all three automatically. You do not need
to call them yourself unless you are building a custom flow.
Built-in hashers
AuthScryptHasher - zero dependencies
import { AuthScryptHasher } from '@gentleduck/auth/core'
const hasher = new AuthScryptHasher({
N: 2 ** 17, // CPU/memory cost - default
r: 8,
p: 1,
keylen: 64,
saltLen: 16,
})
Uses Node's built-in crypto.scrypt. Zero peer dependencies.
Default for v0.1. Format: scrypt$N$r$p$<salt-b64>$<key-b64>.
AuthArgon2idHasher - required for HIPAA / FIPS
import { AuthArgon2idHasher } from '@gentleduck/auth/core'
const hasher = new AuthArgon2idHasher({
memoryCost: 19456, // OWASP minimum
timeCost: 2,
parallelism: 1,
hashLength: 32,
saltLength: 16,
})
Requires @node-rs/argon2 as a peer dependency. duck-auth lazy-loads it
on first call so apps that never use Argon2 do not pay the cost.
Two parameter presets are exported:
import { ARGON2ID_DEFAULTS, ARGON2ID_COMPLIANCE } from '@gentleduck/auth/core'
new AuthArgon2idHasher(ARGON2ID_DEFAULTS) // OWASP minimum
new AuthArgon2idHasher(ARGON2ID_COMPLIANCE) // HIPAA / FIPS preset
Rehash on sign-in
When you change hasher parameters (e.g. crank N from 2^16 to 2^17 once
servers can afford it), every existing user has a stored hash with the
old parameters. duck-auth handles this transparently:
import { password } from '@gentleduck/auth/providers/password'
const provider = password({
findIdentityByEmail: ...,
passwords: auth.passwords,
autoRehash: true, // <- default. On successful verify, if params drifted,
// the new hash is written back to the credential row.
})
autoRehash checks needsRehash(stored) after every successful verify
and silently updates the row. No downtime, no migration script.
Policy: length and common-password rejection
new AuthEngine({
passwords: {
hasher: new AuthScryptHasher(),
minLength: 12, // default 8
maxLength: 1024, // default 1024
rejectCommon: true, // bundled top-10k list
},
})
rejectCommon consults a bundled list of the 10,000 most common passwords
from public breaches. The list is shipped in-package; no network call.
Set minLength to 14+ for HIPAA / FIPS / SOC2 - the compliance presets
do this automatically when applied.
Writing your own hasher
AuthHasher.IHasher is a three-method interface:
interface IHasher {
hash(plain: string): Promise<string>
verify(plain: string, stored: string): Promise<boolean>
needsRehash(stored: string): boolean
}
Wrap any algorithm (Bcrypt, Yescrypt, PBKDF2) and pass it as
passwords.hasher. The runtime calls needsRehash() synchronously, so
keep that path cheap.
Migrating from another system
If you import pre-hashed credentials from another store, use
identities.bulkCreate and mark the credentials with a kind your
custom hasher recognises:
class BcryptCompatHasher implements AuthHasher.IHasher {
async hash(plain: string) { /* always returns scrypt$... */ }
async verify(plain: string, stored: string) {
if (stored.startsWith('$2b$')) return bcrypt.compare(plain, stored)
return scryptVerify(plain, stored)
}
needsRehash(stored: string) {
return stored.startsWith('$2b$') // every bcrypt hash needs upgrade
}
}
Combined with autoRehash: true, this silently migrates every user from
bcrypt to scrypt the next time they sign in - no batch job needed.