Default password hashing to bcrypt (fits varchar(64) users.password)

Verified against the live AtomCMS DB: users.password is varchar(64), so
argon2id (~97 chars) overflows the column and registration/upgrade fail
with 'value too long'. bcrypt (60-char $2y$) fits and matches the
existing accounts. hashPassword() now emits bcrypt by default; set
PASSWORD_HASH=argon2id to opt back in (needs a widened column).
verifyPassword() still accepts both, so existing logins keep working.

Verified end-to-end against the live DB: bcrypt $2y$ login round-trips
(correct=true, wrong=false). tsc 0, vitest 8/8 (password suite).
This commit is contained in:
Simo committed 2026-06-28 16:26:33 +02:00
1 parent d5efbba9f6
commit 4eccd146ba
3 files changed
+71 -19

No files matched your search

+34 -8
View File
@@ -15,12 +15,36 @@ describe("md5Hex", () => {
});
});
describe("argon2id", () => {
describe("hashPassword (default driver: bcrypt)", () => {
it("emits a PHP-style $2y$ bcrypt hash that fits varchar(64) and round-trips", async () => {
const prev = process.env.PASSWORD_HASH;
delete process.env.PASSWORD_HASH; // exercise the default
try {
const h = await hashPassword("s3cret!");
expect(h).toMatch(/^\$2y\$/);
expect(h.length).toBeLessThanOrEqual(60); // fits varchar(64)
expect(await verifyPassword("s3cret!", h)).toBe(true);
expect(await verifyPassword("wrong", h)).toBe(false);
} finally {
if (prev === undefined) delete process.env.PASSWORD_HASH;
else process.env.PASSWORD_HASH = prev;
}
});
});
describe("hashPassword (PASSWORD_HASH=argon2id)", () => {
it("hashes with the AtomCMS params (m=65536,t=4,p=1) and round-trips", async () => {
const h = await hashPassword("s3cret!");
expect(h).toMatch(/^\$argon2id\$v=19\$m=65536,t=4,p=1\$/);
expect(await verifyPassword("s3cret!", h)).toBe(true);
expect(await verifyPassword("wrong", h)).toBe(false);
const prev = process.env.PASSWORD_HASH;
process.env.PASSWORD_HASH = "argon2id";
try {
const h = await hashPassword("s3cret!");
expect(h).toMatch(/^\$argon2id\$v=19\$m=65536,t=4,p=1\$/);
expect(await verifyPassword("s3cret!", h)).toBe(true);
expect(await verifyPassword("wrong", h)).toBe(false);
} finally {
if (prev === undefined) delete process.env.PASSWORD_HASH;
else process.env.PASSWORD_HASH = prev;
}
});
});
@@ -44,11 +68,13 @@ describe("isMd5Of", () => {
});
describe("checkLogin", () => {
it("upgrades a legacy md5 hash to argon2id when conversion is enabled", async () => {
it("upgrades a legacy md5 hash to the configured hash when conversion is enabled", async () => {
const stored = md5Hex("oldpass");
const res = await checkLogin("oldpass", stored, { convertPasswords: true });
expect(res.valid).toBe(true);
expect(res.upgradedHash).toMatch(/^\$argon2id\$/);
// Default driver is bcrypt — the upgraded hash must fit varchar(64).
expect(res.upgradedHash).toMatch(/^\$2y\$/);
expect((res.upgradedHash as string).length).toBeLessThanOrEqual(60);
// The upgraded hash verifies the same password.
expect(await verifyPassword("oldpass", res.upgradedHash as string)).toBe(true);
});
@@ -60,7 +86,7 @@ describe("checkLogin", () => {
expect(res.upgradedHash).toBeUndefined();
});
it("validates an existing argon2id hash with no upgrade", async () => {
it("validates an existing modern hash with no upgrade", async () => {
const stored = await hashPassword("modern");
const res = await checkLogin("modern", stored, { convertPasswords: true });
expect(res.valid).toBe(true);