ADR-0002: Authentication & Password Hashing
- Status: Accepted
- Date: 2026-06-04
- Deciders: Architecture Sentinel
- Depends on: ADR-0001
Context
The RealWorld API is stateless and token-based: clients authenticate once and present a bearer token (Authorization: Token <jwt>) on subsequent requests. We need (1) a token mechanism that requires no server-side session store and (2) a password-hashing scheme that resists offline brute-force and GPU attacks, while keeping the dependency surface free of known-vulnerable transitive packages (the constitution mandates npm audit --audit-level=high as a commit gate).
Decision
- Tokens: JSON Web Tokens (JWT) via
jsonwebtoken@^9, signed with HS256 using aJWT_SECRETinjected from the environment and validated at startup (fail fast if absent). Tokens are stateless, carrying the user id and a short expiry; no session table is required. Becausejsonwebtokenis a CommonJS package, it is consumed via a default import under our ESM/NodeNext setup:import pkg from 'jsonwebtoken'; const { sign } = pkg;in the signing service andconst { verify } = pkg;in the auth middleware. Named ESM imports from this package break at runtime and are prohibited. - Password hashing:
argon2@^0.40using the Argon2id variant. Argon2id won the Password Hashing Competition and is memory-hard, providing strong resistance to GPU/ASIC cracking with tunable memory, time, and parallelism cost parameters. Hashes are self-describing (parameters embedded), simplifying future cost increases.
Alternatives Considered
- bcrypt — rejected. The common
bcryptnpm package compiles native code vianode-pre-gyp, which has pulled in a chain of advisories (thenode-pre-gyp→tar/node-tarpath traversal CVE line, plus repeatednpm audithigh findings through transitive build deps). That conflicts directly with our high-severity audit gate. bcrypt is also limited to a 72-byte input and is only CPU-hard, not memory-hard.argon2avoids thenode-pre-gypchain and offers a stronger security profile. - Server-side sessions (cookies + store): rejected — adds a stateful store and does not match the RealWorld token contract.
- PBKDF2 / scrypt: acceptable fallbacks but weaker (PBKDF2 is not memory-hard) or less ergonomic than Argon2id; not chosen.
Consequences
Authentication scales horizontally with no shared session state. The auth boundary lives in a thin middleware adapter that verifies the token and attaches the user id; services depend only on that resolved identity, never on HTTP. The argon2 native dependency must build on CI runners (Node 20 prebuilds are available). The HS256 secret is a single point of trust and must be managed as a secret in every environment; rotating it invalidates outstanding tokens, which is acceptable given their short lifetime.