typescript 77 lines · 3 tabs

Password hashing with Argon2

Shared by codesnips Jan 2026
3 tabs
import { Request, Response } from 'express';
import { authenticate } from './auth.service';

export async function login(req: Request, res: Response): Promise<void> {
  const { email, password } = req.body ?? {};

  if (typeof email !== 'string' || typeof password !== 'string') {
    res.status(400).json({ error: 'Email and password are required.' });
    return;
  }

  const result = await authenticate(email.trim().toLowerCase(), password);

  if (!result) {
    // Same message for missing user and wrong password.
    res.status(401).json({ error: 'Invalid email or password.' });
    return;
  }

  req.session.userId = result.id;
  res.status(200).json({ id: result.id, email: result.email });
}
3 files · typescript Explain with highlit

This snippet shows how password hashing is isolated behind a small module so the rest of an application never touches raw hashes, and how a login flow transparently upgrades stored hashes when hashing parameters change.

In password.ts, argon2id is chosen deliberately: it is the hybrid variant recommended by the OWASP password storage guidance because it combines resistance to GPU cracking (data-dependent memory access) with resistance to side-channel attacks (data-independent access). The ARGON2_OPTIONS object pins memoryCost, timeCost, and parallelism so hashes are reproducible and tunable — these are the knobs that make Argon2 expensive for attackers. hashPassword simply delegates to argon2.hash, which encodes the algorithm, version, parameters, and a per-hash random salt into the returned PHC string, so no separate salt column is needed.

verifyPassword wraps argon2.verify in a try/catch that swallows errors and returns false. This matters: a malformed stored hash should read as a failed login, not a 500. The exported needsRehash calls argon2.needsRehash with the same options, which returns true when a stored hash was produced with weaker parameters than the current policy — the signal that drives seamless migration.

auth.service.ts orchestrates the flow. authenticate first loads the user, then does an important defensive step: even when no user exists it runs a hash against a DUMMY_HASH. This keeps the response time roughly constant whether or not the email exists, blunting user-enumeration timing attacks. When verification succeeds and needsRehash reports the hash is stale, authenticate recomputes a fresh hash with current parameters and persists it via updatePasswordHash — the plaintext is already in hand during login, which is the only moment a rehash is possible.

auth.controller.ts is a thin Express handler that maps a successful result to a session and a failure to a generic 401 with a deliberately vague message, avoiding hints about which field was wrong. Together the files demonstrate a durable pattern: centralize the cost parameters, verify defensively, equalize timing, and let ordinary logins slowly re-encrypt the whole user base as policy tightens over time.


Related snips

Share this code

Here's the card — post it anywhere.

Password hashing with Argon2 — share card
Link copied