* fix: block login for 2fa users with un-verified passkeys * refactor(passkey): show proper login error * refactor: show passkey generic error on all failures
137 lines
4.3 KiB
TypeScript
137 lines
4.3 KiB
TypeScript
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
import { eq } from "drizzle-orm";
|
|
import { db } from "~/server/db/db";
|
|
import { account, passkey, usersTable } from "~/server/db/schema";
|
|
import { createUser, randomId, randomSlug } from "~/test/helpers/user-org";
|
|
import { hasActivePasskeyUser, userHasCredentialPassword, verifyUserPassword } from "../helpers";
|
|
|
|
const { verifyPassword } = vi.hoisted(() => ({
|
|
verifyPassword: vi.fn(async ({ hash }: { hash: string }) => hash === "credential-password-hash"),
|
|
}));
|
|
|
|
vi.mock("better-auth/crypto", () => ({
|
|
verifyPassword,
|
|
}));
|
|
|
|
async function createAccount({
|
|
password,
|
|
providerId,
|
|
userId,
|
|
}: {
|
|
password: string | null;
|
|
providerId: string;
|
|
userId: string;
|
|
}) {
|
|
await db.insert(account).values({
|
|
id: randomId(),
|
|
accountId: randomSlug("account"),
|
|
providerId,
|
|
userId,
|
|
password,
|
|
});
|
|
}
|
|
|
|
async function createPasskey(userId: string) {
|
|
await db.insert(passkey).values({
|
|
id: randomId(),
|
|
name: "Test passkey",
|
|
publicKey: randomSlug("public-key"),
|
|
userId,
|
|
credentialID: randomSlug("credential"),
|
|
counter: 0,
|
|
deviceType: "singleDevice",
|
|
backedUp: false,
|
|
transports: "internal",
|
|
});
|
|
}
|
|
|
|
describe("verifyUserPassword", () => {
|
|
beforeEach(async () => {
|
|
verifyPassword.mockClear();
|
|
await db.delete(passkey);
|
|
await db.delete(account);
|
|
await db.delete(usersTable);
|
|
});
|
|
|
|
test("verifies against the credential account when the user also has SSO accounts", async () => {
|
|
const userId = await createUser(`${randomSlug("user")}@example.com`);
|
|
await createAccount({ userId, providerId: "oidc-acme", password: null });
|
|
await createAccount({ userId, providerId: "credential", password: "credential-password-hash" });
|
|
|
|
const result = await verifyUserPassword({ userId, password: "correct-password" });
|
|
|
|
expect(result).toBe(true);
|
|
expect(verifyPassword).toHaveBeenCalledWith({
|
|
password: "correct-password",
|
|
hash: "credential-password-hash",
|
|
});
|
|
});
|
|
|
|
test("returns false when the user has no credential password account", async () => {
|
|
const userId = await createUser(`${randomSlug("user")}@example.com`);
|
|
await createAccount({ userId, providerId: "oidc-acme", password: null });
|
|
|
|
const result = await verifyUserPassword({ userId, password: "correct-password" });
|
|
|
|
expect(result).toBe(false);
|
|
expect(verifyPassword).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("userHasCredentialPassword", () => {
|
|
beforeEach(async () => {
|
|
await db.delete(passkey);
|
|
await db.delete(account);
|
|
await db.delete(usersTable);
|
|
});
|
|
|
|
test("returns true when the user has a credential account with a password", async () => {
|
|
const userId = await createUser(`${randomSlug("user")}@example.com`);
|
|
await createAccount({ userId, providerId: "credential", password: "credential-password-hash" });
|
|
|
|
await expect(userHasCredentialPassword(userId)).resolves.toBe(true);
|
|
});
|
|
|
|
test("returns false when the user only has SSO accounts", async () => {
|
|
const userId = await createUser(`${randomSlug("user")}@example.com`);
|
|
await createAccount({ userId, providerId: "oidc-acme", password: null });
|
|
|
|
await expect(userHasCredentialPassword(userId)).resolves.toBe(false);
|
|
});
|
|
|
|
test("returns false when the credential account has no password", async () => {
|
|
const userId = await createUser(`${randomSlug("user")}@example.com`);
|
|
await createAccount({ userId, providerId: "credential", password: null });
|
|
|
|
await expect(userHasCredentialPassword(userId)).resolves.toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("hasActivePasskeyUser", () => {
|
|
beforeEach(async () => {
|
|
await db.delete(passkey);
|
|
await db.delete(account);
|
|
await db.delete(usersTable);
|
|
});
|
|
|
|
test("returns true when a non-banned user has a passkey", async () => {
|
|
const userId = await createUser(`${randomSlug("user")}@example.com`);
|
|
await createPasskey(userId);
|
|
|
|
await expect(hasActivePasskeyUser()).resolves.toBe(true);
|
|
});
|
|
|
|
test("returns false when only banned users have passkeys", async () => {
|
|
const userId = await createUser(`${randomSlug("user")}@example.com`);
|
|
await db.update(usersTable).set({ banned: true }).where(eq(usersTable.id, userId));
|
|
await createPasskey(userId);
|
|
|
|
await expect(hasActivePasskeyUser()).resolves.toBe(false);
|
|
});
|
|
|
|
test("returns false when no users have passkeys", async () => {
|
|
await createUser(`${randomSlug("user")}@example.com`);
|
|
|
|
await expect(hasActivePasskeyUser()).resolves.toBe(false);
|
|
});
|
|
});
|