Skip to main content

Passwords

WIP

AuthScryptHasher (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.