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>
68 lines
1.8 KiB
TypeScript
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 };
|