Files
diachron/express/auth/password.ts
Michael Wolf c246e0384f Add authentication system with session-based auth
Implements full auth flows with opaque tokens (not JWT) for easy revocation:
- Login/logout with cookie or bearer token support
- Registration with email verification
- Password reset with one-time tokens
- scrypt password hashing (no external deps)

New files in express/auth/:
- token.ts: 256-bit token generation, SHA-256 hashing
- password.ts: scrypt hashing with timing-safe verification
- types.ts: Session schemas, token types, input validation
- store.ts: AuthStore interface + InMemoryAuthStore
- service.ts: AuthService with all auth operations
- routes.ts: 6 auth endpoints

Modified:
- types.ts: Added user field to Call, requireAuth/requirePermission helpers
- app.ts: JSON body parsing, populates call.user, handles auth errors
- services.ts: Added services.auth
- routes.ts: Includes auth routes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 13:59:02 -06:00

68 lines
1.8 KiB
TypeScript

// password.ts
//
// Password hashing using Node.js scrypt (no external dependencies).
// Format: $scrypt$N$r$p$salt$hash (all base64)
import {
randomBytes,
type ScryptOptions,
scrypt,
timingSafeEqual,
} from "node:crypto";
// Configuration
const SALT_LENGTH = 32;
const KEY_LENGTH = 64;
const SCRYPT_PARAMS: ScryptOptions = {
N: 16384, // CPU/memory cost parameter (2^14)
r: 8, // Block size
p: 1, // Parallelization
};
// Promisified scrypt with options support
function scryptAsync(
password: string,
salt: Buffer,
keylen: number,
options: ScryptOptions,
): Promise<Buffer> {
return new Promise((resolve, reject) => {
scrypt(password, salt, keylen, options, (err, derivedKey) => {
if (err) reject(err);
else resolve(derivedKey);
});
});
}
async function hashPassword(password: string): Promise<string> {
const salt = randomBytes(SALT_LENGTH);
const hash = await scryptAsync(password, salt, KEY_LENGTH, SCRYPT_PARAMS);
const { N, r, p } = SCRYPT_PARAMS;
return `$scrypt$${N}$${r}$${p}$${salt.toString("base64")}$${hash.toString("base64")}`;
}
async function verifyPassword(
password: string,
stored: string,
): Promise<boolean> {
const parts = stored.split("$");
if (parts[1] !== "scrypt" || parts.length !== 7) {
throw new Error("Invalid password hash format");
}
const [, , nStr, rStr, pStr, saltB64, hashB64] = parts;
const salt = Buffer.from(saltB64, "base64");
const storedHash = Buffer.from(hashB64, "base64");
const computedHash = await scryptAsync(password, salt, storedHash.length, {
N: parseInt(nStr, 10),
r: parseInt(rStr, 10),
p: parseInt(pStr, 10),
});
return timingSafeEqual(storedHash, computedHash);
}
export { hashPassword, verifyPassword };