fix(seed): use better-auth/crypto hashPassword to match verifyPassword params
CI / Lint & Typecheck (pull_request) Failing after 17s
CI / Test (pull_request) Failing after 21s
CI / Build (pull_request) Has been skipped
CI / Build & Push Docker Images (pull_request) Has been skipped
CI / Update Infra Image Tags (pull_request) Has been skipped
CI / Lint & Typecheck (pull_request) Failing after 17s
CI / Test (pull_request) Failing after 21s
CI / Build (pull_request) Has been skipped
CI / Build & Push Docker Images (pull_request) Has been skipped
CI / Update Infra Image Tags (pull_request) Has been skipped
The seed.ts password hashing used N=32768, r=8, p=1 with base64 encoding, which does not match @better-auth/utils@0.4.0's actual implementation (N=16384, r=16, p=1, dkLen=64, hex encoding). This caused every seeded UAT credential to fail verifyPassword at sign-in. Fix: import hashPassword from "better-auth/crypto" in seed.ts and in the test helper. This delegates to Better-Auth's own implementation, guaranteeing parameter and encoding match. Also updates test assertions to expect hex format (saltHex:keyHex) and verifies the hash using the correct scrypt params (N=16384, r=16, p=1). Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -32,12 +32,11 @@ const UAT_ACCOUNTS = [
|
|||||||
|
|
||||||
const TEST_PASSWORD = "test-password-123";
|
const TEST_PASSWORD = "test-password-123";
|
||||||
|
|
||||||
// ─── Password hashing (must match the implementation in seed.ts) ───────────────
|
// ─── Password hashing — must match better-auth/crypto (N=16384, r=16, p=1, dkLen=64, hex) ───
|
||||||
|
|
||||||
function hashPassword(password: string): string {
|
async function hashPassword(password: string): Promise<string> {
|
||||||
const salt = randomBytes(16);
|
const { hashPassword } = await import("better-auth/crypto");
|
||||||
const hashed = scryptSync(password, salt, 64, { N: 4096, r: 8, p: 1 }).toString("base64");
|
return hashPassword(password);
|
||||||
return `${salt.toString("base64")}:${hashed}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Mock DB state ─────────────────────────────────────────────────────────────
|
// ─── Mock DB state ─────────────────────────────────────────────────────────────
|
||||||
@@ -177,9 +176,9 @@ async function seedUatCredentials(
|
|||||||
if (existingAccount) {
|
if (existingAccount) {
|
||||||
// skip — already has credential account
|
// skip — already has credential account
|
||||||
} else {
|
} else {
|
||||||
const salt = randomBytes(16);
|
// Use Better-Auth's hashPassword so test helper matches production seed.ts
|
||||||
const hashed = scryptSync(password, salt, 64, { N: 4096, r: 8, p: 1 }).toString("base64");
|
const { hashPassword } = await import("better-auth/crypto");
|
||||||
const passwordHash = `${salt.toString("base64")}:${hashed}`;
|
const passwordHash = await hashPassword(password);
|
||||||
|
|
||||||
const newAccount: AccountRow = {
|
const newAccount: AccountRow = {
|
||||||
id: mockUuid(),
|
id: mockUuid(),
|
||||||
@@ -236,11 +235,12 @@ describe("seedUatCredentials — credential provisioning logic", () => {
|
|||||||
expect(insertedAccounts).toHaveLength(4);
|
expect(insertedAccounts).toHaveLength(4);
|
||||||
for (const acct of insertedAccounts) {
|
for (const acct of insertedAccounts) {
|
||||||
expect(acct.providerId).toBe("credential");
|
expect(acct.providerId).toBe("credential");
|
||||||
expect(acct.password).toMatch(/^[A-Za-z0-9+/=]+:[A-Za-z0-9+/=]+$/); // salt:hash
|
// Better-Auth uses hex encoding: saltHex:keyHex (both lowercase hex)
|
||||||
// Verify the hash is scrypt with correct params
|
expect(acct.password).toMatch(/^[a-f0-9]+:[a-f0-9]+$/);
|
||||||
const [saltB64, hashB64] = acct.password!.split(":");
|
// Verify the hash is scrypt with correct params (N=16384, r=16, p=1, dkLen=64)
|
||||||
const salt = Buffer.from(saltB64, "base64");
|
const [saltHex, keyHex] = acct.password!.split(":");
|
||||||
const storedHash = Buffer.from(hashB64, "base64");
|
const salt = Buffer.from(saltHex, "hex");
|
||||||
|
const storedHash = Buffer.from(keyHex, "hex");
|
||||||
expect(salt).toHaveLength(16);
|
expect(salt).toHaveLength(16);
|
||||||
expect(storedHash).toHaveLength(64);
|
expect(storedHash).toHaveLength(64);
|
||||||
}
|
}
|
||||||
@@ -271,13 +271,18 @@ describe("seedUatCredentials — credential provisioning logic", () => {
|
|||||||
|
|
||||||
const acct = insertedAccounts[0]!;
|
const acct = insertedAccounts[0]!;
|
||||||
expect(acct.providerId).toBe("credential");
|
expect(acct.providerId).toBe("credential");
|
||||||
const [saltB64, hashB64] = acct.password!.split(":");
|
// Better-Auth uses hex: saltHex (32 chars) : keyHex (128 chars)
|
||||||
expect(() => Buffer.from(saltB64, "base64")).not.toThrow();
|
expect(acct.password).toMatch(/^[a-f0-9]+:[a-f0-9]+$/);
|
||||||
expect(() => Buffer.from(hashB64, "base64")).not.toThrow();
|
const [saltHex, keyHex] = acct.password!.split(":");
|
||||||
// Verify the hash can be verified with the original password
|
expect(() => Buffer.from(saltHex, "hex")).not.toThrow();
|
||||||
const salt = Buffer.from(saltB64, "base64");
|
expect(() => Buffer.from(keyHex, "hex")).not.toThrow();
|
||||||
const storedHash = Buffer.from(hashB64, "base64");
|
const salt = Buffer.from(saltHex, "hex");
|
||||||
const computed = scryptSync(TEST_PASSWORD, salt, 64, { N: 4096, r: 8, p: 1 });
|
const storedHash = Buffer.from(keyHex, "hex");
|
||||||
|
expect(salt).toHaveLength(16);
|
||||||
|
expect(storedHash).toHaveLength(64);
|
||||||
|
// Verify the hash can be verified with the original password using Better-Auth params
|
||||||
|
const { scryptSync } = await import("node:crypto");
|
||||||
|
const computed = scryptSync(TEST_PASSWORD.normalize("NFKC"), saltHex, 64, { N: 16384, r: 16, p: 1 });
|
||||||
expect(computed).toEqual(storedHash);
|
expect(computed).toEqual(storedHash);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -563,13 +563,11 @@ async function seedKnownUsers() {
|
|||||||
if (existingAccount) {
|
if (existingAccount) {
|
||||||
console.log(`✓ Credential account for '${acct.email}' already exists — skipping`);
|
console.log(`✓ Credential account for '${acct.email}' already exists — skipping`);
|
||||||
} else {
|
} else {
|
||||||
// Hash password using the same scrypt derivation as crypto.ts (AES-256-GCM key derivation).
|
// Use Better-Auth's own hashPassword to guarantee parameter/encoding match.
|
||||||
// Better-Auth defaults to scrypt for password hashing; match those parameters here.
|
// better-auth/crypto uses: N=16384, r=16, p=1, dkLen=64, salt as 16-byte random
|
||||||
const { scryptSync, randomBytes } = await import("node:crypto");
|
// hex string, key hex-encoded, format saltHex:keyHex.
|
||||||
const salt = randomBytes(16);
|
const { hashPassword } = await import("better-auth/crypto");
|
||||||
// scryptSync(password, salt, keylen=64, N=32768, r=8, p=1) — matches common better-auth scrypt params
|
const passwordHash = await hashPassword(password);
|
||||||
const hashed = scryptSync(password, salt, 64, { N: 32768, r: 8, p: 1 }).toString("base64");
|
|
||||||
const passwordHash = `${salt.toString("base64")}:${hashed}`;
|
|
||||||
|
|
||||||
await db.insert(schema.account).values({
|
await db.insert(schema.account).values({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
|
|||||||
Reference in New Issue
Block a user