Authentication

Password Hashing

Passwords are the one secret you should never store. A slow, salted hash turns a stolen database into a pile of guesses that never finish.

intermediate14 min readUpdated Sep 20, 2026
passwords.js
js
// passwords.js
import { hash, verify } from "@node-rs/argon2";

export const hashPassword = (password) => hash(password);

export async function verifyPassword(password, stored) {
  try {
    return await verify(stored, password);
  } catch {
    return false;
  }
}
Rule
Hash, never encrypt
Salt
Unique per password
Recommended
Argon2id
Still common
bcrypt
Comparison
Timing-safe
On login
Rehash if needed

Why it matters

Why hashing is not optional

One-way by design

A password hash cannot be reversed, so a leaked table reveals no plaintext and an attacker is forced into offline guessing.

Salts defeat precomputation

A unique random salt per password makes rainbow tables useless and ensures two identical passwords hash differently.

Cost is the point

A work factor makes each guess slow enough that brute force becomes impractical, and it can be raised as hardware gets faster.

The big picture

The three ideas behind safe credentials

Hash, never encrypt; salt every digest; and make each guess deliberately expensive.

Hash

Transform

A key-derivation function turns the password into a fixed-length digest that cannot be reversed.

Salt

Randomise

A random value stored beside the digest makes every hash unique and defeats precomputed tables.

Verify

Compare

On login, re-derive and compare in constant time, then upgrade the hash if the parameters are outdated.

At a glance

What good credential storage looks like

Argon2id

The modern default, with tunable memory, iterations and parallelism.

bcrypt

Battle-tested and built into most stacks, with a cost factor and a 72-byte limit.

scrypt

Memory-hard and standardised, a good fit when Argon2 is unavailable.

Salt

Random per password and stored in the same string as the digest.

Timing-safe compare

Compare digests without leaking information through an early exit.

Rehash on login

Raise the cost or switch algorithms as users sign in.

A short history

From crypt to memory-hard hashes

  1. 1979

    crypt and DES

    Early Unix systems hash passwords with a 25-round DES variant and a 12-bit salt.

    79
  2. 1999

    bcrypt

    Provos and Mazieres introduce an adaptive, Blowfish-based hash with a tunable cost.

    99
  3. 2012

    scrypt

    A memory-hard function is published to make large-scale parallel cracking expensive.

    12
  4. 2015

    Argon2 wins the PHC

    Argon2id is selected as the winner of the Password Hashing Competition.

    15
  5. Today

    Adaptive by default

    Frameworks ship Argon2 or bcrypt and expect you to tune the work factor.

    Today

The complete guide

Password Hashing: Everything you need to know

Why passwords need special handling

Every other secret in your system you can rotate: API keys, tokens, certificates. Passwords are different. You never hold them in a form you can regenerate, users reuse them across sites, and the moment your database leaks, the attacker has all the time in the world to guess offline.

Password hashing is the practice of storing a one-way transformation of the password instead of the password itself. When a user signs in, you apply the same transformation and compare. You never learn the password, and neither does anyone who steals the table.

This is not a place for cleverness. The rules are small and well established, and breaking them is how breaches become catastrophic.

Hashing is not encryption

Encryption is reversible. If you encrypt passwords, the key lives somewhere in your system, and anyone who obtains it — through a leak, a backup, a misconfigured environment variable or an insider — can read every password at once. There is no scenario where you need the original password, so reversibility is pure risk.

A hash function takes input and produces a fixed-length digest, and there is no practical way back. For passwords, a plain hash is still not enough: fast functions like SHA-256 are designed to be quick, which is exactly what an attacker wants. A GPU can try billions of candidates per second.

You need a function that is deliberately slow and memory-hard, so guessing is expensive at scale.

Salt: the same password, a different hash

Without a salt, two users with the password hunter2 produce the same digest. An attacker can precompute a giant table of common passwords and their hashes — a rainbow table — and look up matches instantly. They can also see at a glance which accounts share a password.

A salt is a unique random value generated per password and stored alongside the digest. It is not secret; its only job is to make every hash unique. Now the attacker must attack each account separately, and precomputation is useless.

Anatomy of a stored Argon2 hash
$argon2id$v=19$m=19456,t=2,p=1$c29tZXNhbHQ$aBc...
algorithm + paramsalgorithm, version and work factors
saltunique per password, not secret
digestthe derived key you compare against

Modern libraries generate the salt for you and encode it, with the parameters, into a single string. You store that string verbatim.

Choosing an algorithm

Three functions are worth knowing. All are adaptive: the cost can be raised as hardware improves.

Argon2id

The winner of the 2015 Password Hashing Competition and the current recommendation. It is memory-hard, which means an attacker must buy memory as well as compute, blunting the advantage of GPUs and ASICs. Argon2id balances resistance to side-channel and GPU attacks and is the right default for new systems.

scrypt

Standardised in RFC 7914 and memory-hard, scrypt is a solid choice when Argon2 is unavailable. It exposes cost, block size and parallelism, so it can be tuned, but the parameters are easier to misconfigure than Argon2’s.

bcrypt

Released in 1999 and still everywhere, bcrypt is well understood and battle-tested. Its cost factor doubles the work with each increment. The main caveat is that it truncates input at 72 bytes, so very long passphrases are silently cut — pre-hash if that matters to you.

Whichever you choose, never reach for MD5, SHA-1 or plain SHA-256. They are the wrong tool, and stacking rounds on top of them is a homemade scheme nobody should trust.

Tuning the work factor

The cost must balance two pressures. Too low and the hash falls to brute force; too high and a burst of login attempts becomes a denial-of-service vector against your own CPU.

Tune it so a single hash takes roughly 100 to 500 milliseconds on your production hardware. Measure the login endpoint under realistic load, not just a benchmark loop, and set the number from data. Then revisit it periodically: the same parameters get weaker every year as hardware improves.

// Argon2id parameters (memory in KiB, iterations, parallelism)
await hash(password, { memoryCost: 19456, timeCost: 2, parallelism: 1 });

Storing and verifying

Registration and login are the only two places you touch a password.

import { hash, verify } from "@node-rs/argon2";

// Registration: hash before the password ever reaches the database.
const digest = await hash(password);
await db.users.insert({ email, passwordHash: digest });

// Login: verify against the stored digest.
const user = await db.users.findByEmail(email);
const ok = user && (await verify(user.passwordHash, password));
if (!ok) return res.status(401).json({ error: "invalid_credentials" });

Return the same error whether the email or the password was wrong. A message like “no such user” hands an attacker a way to enumerate accounts.

Timing attacks and safe comparison

Comparing two hashes with === stops at the first differing byte. The time it takes therefore depends on how much of the guess was correct, and a patient attacker can measure that to reconstruct a value. It is a narrow channel, but password comparison is exactly where it matters.

Use the library’s verification function, which compares in constant time. Never compare digests yourself, and never compare the raw password.

Upgrading hashes over time

You will change your mind: the cost goes up, or you migrate from bcrypt to Argon2. Because you never stored the plaintext, you can only rehash at login, when the password is briefly in memory.

Store the algorithm and parameters with each digest — the encoded string already does — and check them on every successful login:

if (await verify(stored, password)) {
  if (needsRehash(stored)) {
    await db.users.update(user.id, { passwordHash: await hash(password) });
  }
  // continue the login
}

Users who never sign in again keep their old hash, which is fine; it still protects them. Over time the active accounts migrate naturally.

Password policy in practice

The algorithm protects you after a breach. Policy reduces the chance of one.

  • Prefer length over complexity. A passphrase beats P@ssw0rd!.
  • Check new passwords against known-breached lists such as Have I Been Pwned’s k-anonymity API.
  • Do not impose arbitrary rotation. Forced expiry leads to Summer2026! patterns.
  • Rate-limit login and reset attempts, and add exponential backoff or a lockout.
  • Use a generic reset flow with single-use, expiring tokens, and never email a password.

Best practices

  • Hash with Argon2id, scrypt or bcrypt — never a fast general-purpose hash.
  • Let the library generate and store the salt.
  • Tune the cost to 100–500 ms and raise it over time.
  • Verify with the library’s constant-time function.
  • Rehash on successful login when parameters change.
  • Return generic errors and rate-limit attempts.
  • Keep hashing on the server; never in the client.

Common mistakes

  • Encrypting passwords or storing them in plaintext “temporarily”.
  • Using SHA-256 or MD5 with a few rounds and calling it hashing.
  • A single global salt, or reusing one salt across accounts.
  • Comparing hashes with == or ===.
  • Logging passwords or including them in error reports.
  • Capping password length at a low value, which hurts passphrases.
  • Forgetting to upgrade the cost factor for years.

Where to go next

Credentials are only the first gate. Once a user is verified, you need somewhere to keep that state: read the Session Auth guide for cookie-based sessions, or the JWT guide for stateless tokens. When you would rather not handle passwords at all, OAuth 2.0 and OpenID Connect delegate login to an identity provider.

In practice

Hash, store, verify

The same three steps in every stack. The library handles the salt and encodes it alongside the digest.

hash.js
import { hash } from "@node-rs/argon2";

const digest = await hash(password);
// $argon2id$v=19$m=19456,t=2,p=1$...$...

Hashing a password

Use a purpose-built password hash with a work factor. A fast general-purpose hash is a cracker's favourite gift.

Prefer
import { hash } from "@node-rs/argon2";

const digest = await hash(password);
Avoid
import { createHash } from "node:crypto";

const digest = createHash("sha256")
  .update(password)
  .digest("hex");

Comparing a password

Compare through the library's constant-time check. Plain string equality can leak content and length through timing.

Prefer
const ok = await verify(stored, password);
Avoid
const ok = stored === (await hash(password));

Trade-offs

Argon2 or bcrypt?

Both are safe when tuned. Argon2 is the modern default; bcrypt remains a perfectly good choice with years of production use.

Strengths

  • Argon2id is memory-hard

    It resists GPU and ASIC cracking by requiring memory as well as time, and it is the current recommendation.

  • bcrypt is everywhere

    It is built into most frameworks, has a long track record, and needs no extra native dependency.

  • Both are tunable

    The work factor can be raised over time, so stored hashes can be strengthened without changing users' passwords.

Trade-offs

  • bcrypt truncates at 72 bytes

    Long passphrases are silently cut, so pre-hash or prefer Argon2 when that matters.

  • Tuning needs care

    Too low and it is crackable; too high and login becomes a denial-of-service vector under load.

  • Hashing is not the whole story

    Rate limiting, breach checks and a safe reset flow matter just as much as the algorithm.

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning Password Hashing & Credentials?

Our interactive tutorial walks you through Password Hashing & Credentials step by step — with quizzes and real code you can run in the browser.