Password Hashing in Node.js with Argon2id

Password Hashing in Node.js with Argon2id

Passwords should never be stored as plain text or encrypted in a reversible form. Instead, applications should store a deliberately expensive, one-way hash.

Argon2id is a key derivation function that won the Password Hashing Competition in 2015. It's specifically designed for password hashing and has become the recommended choice for new applications due to its security properties and performance characteristics.

It has three variants:

  • Argon2d prioritizes resistance to GPU cracking.
  • Argon2i prioritizes resistance to side-channel attacks.
  • Argon2id combines properties of both.

For general password storage, Argon2id is the preferred variant. It is standardized in RFC 9106.

You can check out this article for the C# implementation.

1. Install the Node.js package

npm install argon2

The argon2 package provides Node.js bindings for hashing and verifying passwords with Argon2.

Because password hashing is computationally expensive, its API is asynchronous. This prevents the hashing operation from unnecessarily blocking other application work.

2. Create the password functions

Create a file named passwords.js:

import * as argon2 from "argon2";

const hashingOptions = {
  type: argon2.argon2id,
  memoryCost: 64 * 1024,
  timeCost: 3,
  parallelism: 1,
};

export async function hashPassword(password) {
  return argon2.hash(password, hashingOptions);
}

export async function verifyPassword(storedHash, submittedPassword) {
  return argon2.verify(storedHash, submittedPassword);
}

Let’s break this down.

Importing Argon2

import * as argon2 from "argon2";

This imports the package’s hashing function, verification function, and Argon2 variant constants.

This example uses ES modules. If the application uses CommonJS, use:

const argon2 = require("argon2");

Selecting Argon2id

type: argon2.argon2id

The package uses Argon2id by default, but setting it explicitly makes the application’s security decision clear.

Configuring the cost

memoryCost: 64 * 1024,
timeCost: 3,
parallelism: 1,

These settings control how expensive each password guess is, both for your server and for an attacker attempting to crack stolen password hashes.

  • memoryCost specifies how much memory Argon2 uses for each hashing operation, measured in KiB. The value 64 * 1024 equals 65,536 KiB, or 64 MiB of RAM per hash.

This memory requirement is one of Argon2’s most important defenses: an attacker attempting millions of guesses cannot rely only on fast CPU or GPU cores—they must also provide a significant amount of memory for every hash being computed concurrently.

  • timeCost controls how many iterations, or passes, Argon2 performs over that memory. Increasing it makes each hash take longer.
  • parallelism controls the number of computational lanes Argon2 uses during the hashing operation.

For example, if your application processes 20 password hashes concurrently with a memoryCost of 64 MiB, those operations can require roughly 1.25 GiB of memory just for Argon2:

64 MiB × 20 concurrent hashes = 1,280 MiB

That is why increasing memoryCost is not simply a matter of choosing the largest possible value. Higher values make password cracking more expensive, but they also increase your application's memory requirements during bursts of registrations or login attempts.

These values are a reasonable starting point, not a universal configuration. Benchmark them on the production hardware and under realistic concurrent traffic.

The goal is to make each hash expensive enough to discourage brute-force attacks while keeping authentication responsive and preventing concurrent requests from exhausting server memory.

Hashing a password

export async function hashPassword(password) {
  return argon2.hash(password, hashingOptions);
}

Call this function when a user creates or changes their password.

Argon2 generates a random salt automatically. The resulting string contains the algorithm, salt, version, cost parameters, and hash:

$argon2id$v=19$m=65536,t=3,p=1$...

Store this complete string in the database. A separate salt column is unnecessary.

Verifying a password

export async function verifyPassword(storedHash, submittedPassword) {
  return argon2.verify(storedHash, submittedPassword);
}

During login, retrieve the stored hash and pass it to verify along with the password submitted by the user.

The function returns true when they match and false when they do not. The original password cannot be recovered from the stored hash.

Full example

import * as argon2 from "argon2";

const password = "my-secret-password";

const options = {
  type: argon2.argon2id,
  memoryCost: 64 * 1024, // 64 MiB
  timeCost: 3,
  parallelism: 1,
};

const hash = await argon2.hash(password, options);

console.log("Hash:");
console.log(hash);

const isValid = await argon2.verify(hash, password);

console.log("Valid password:", isValid);

const isInvalid = await argon2.verify(hash, "wrong-password");

console.log("Wrong password:", isInvalid);

Production considerations

Before deploying this code:

  • Require HTTPS so passwords are encrypted in transit.
  • Rate-limit registration and login endpoints.
  • Limit password input length to prevent resource-abuse attacks.
  • Never log passwords or include them in error messages.
  • Benchmark the Argon2 settings under realistic concurrent traffic.
  • Rehash passwords after successful login when the application adopts stronger parameters.

With those safeguards, Argon2id provides a strong, modern foundation for password storage in Node.js.

Stay Sharp. Weekly Insights.
New posts, framework updates and weekly software conversations.

No spam. Unsubscribe anytime.
Author profile picture
Walt is a software engineer, startup founder and previous mentor for a coding bootcamp. He has been creating software for the past 20+ years.
No comments posted yet
// Add a comment
// Color Theme

Custom accent
Pick any color
for the accent