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 });
}
import argon2 from 'argon2';
export const ARGON2_OPTIONS: argon2.Options = {
type: argon2.argon2id,
memoryCost: 19456, // 19 MiB
timeCost: 2,
parallelism: 1,
};
export async function hashPassword(plain: string): Promise<string> {
return argon2.hash(plain, ARGON2_OPTIONS);
}
export async function verifyPassword(hash: string, plain: string): Promise<boolean> {
try {
return await argon2.verify(hash, plain);
} catch {
return false;
}
}
export function needsRehash(hash: string): boolean {
return argon2.needsRehash(hash, ARGON2_OPTIONS);
}
import { hashPassword, verifyPassword, needsRehash } from './password';
import { findUserByEmail, updatePasswordHash } from './user.repository';
// Precomputed argon2id hash of a random string used to equalize timing.
const DUMMY_HASH =
'$argon2id$v=19$m=19456,t=2,p=1$c29tZXNhbHR2YWx1ZQ$3f4b8c1d2e5a6b7c8d9e0f1a2b3c4d5e';
export interface AuthResult {
id: string;
email: string;
}
export async function authenticate(email: string, password: string): Promise<AuthResult | null> {
const user = await findUserByEmail(email);
if (!user) {
// Burn equivalent CPU to avoid leaking that the email is unknown.
await verifyPassword(DUMMY_HASH, password);
return null;
}
const ok = await verifyPassword(user.passwordHash, password);
if (!ok) return null;
if (needsRehash(user.passwordHash)) {
const upgraded = await hashPassword(password);
await updatePasswordHash(user.id, upgraded);
}
return { id: user.id, email: user.email };
}
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
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
export type Settled<R> =
| { status: 'fulfilled'; value: R }
| { status: 'rejected'; reason: unknown };
export interface ConcurrencyOptions {
limit: number;
Simple concurrency limiter for batch operations
#!/usr/bin/env bash
set -euo pipefail
export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"
Secrets management with environment isolation and Vault
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";
const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";
JWT access + refresh token rotation (conceptual)
package files
import (
"context"
"time"
Presigned S3 upload URLs (AWS SDK v2)
<%# private stream: turbo signs the serialized record name %>
<%= turbo_stream_from current_user %>
<section class="notifications">
<h1>Notifications</h1>
Turbo Streams + authorization: signed per-user stream name
Share this code
Here's the card — post it anywhere.