refactor: extract shared login credential verification into auth/login-core
CI / check (push) Successful in 35s
CI / release (push) Skipped
CI / deploy (push) Successful in 58s

The username normalization, dummy-hash constant, password check and
email-verification gate were duplicated between precheckLogin and the
NextAuth credentials authorize handler. Move them into a single
login-core module so both paths share one source of truth and stay
consistent.
This commit is contained in:
openhands committed 2026-08-27 15:17:54 +02:00
1 parent e3ed37d170
commit 89c1751e79
4 files changed
+187 -174

No files matched your search

+12 -44
View File
@@ -1,11 +1,14 @@
"use server";
import { env } from "@/env";
import { checkLogin } from "@/lib/auth/password";
import { queryPreparedOne } from "@/lib/db";
import {
getLoginUser,
isEmailUnverified,
normalizeLoginInput,
runDummyHashCheck,
verifyLoginPassword,
} from "@/lib/auth/login-core";
import { clientIp, rateLimit } from "@/lib/rate-limit";
import { captchaConfig, verifyCaptcha } from "@/lib/services/captcha";
import { siteSettings } from "@/lib/services/site-settings";
export type PrecheckResult =
| "ok"
@@ -24,10 +27,7 @@ export async function precheckLogin(
password: string,
captchaToken?: string | null,
): Promise<PrecheckResult> {
const u = String(username ?? "")
.normalize("NFC")
.trim();
const p = String(password ?? "").normalize("NFC");
const { username: u, password: p } = normalizeLoginInput(username, password);
if (!u || !p) return "invalid";
const ip = await clientIp();
@@ -38,49 +38,17 @@ export async function precheckLogin(
if (!(await verifyCaptcha(captchaToken ?? null, ip))) return "captcha";
}
let user: {
password: string;
twoFactorConfirmedAt: Date | null;
mail: string | null;
mailVerified: string;
} | null;
try {
user = await queryPreparedOne<{
password: string;
twoFactorConfirmedAt: Date | null;
mail: string | null;
mailVerified: string;
}>(
`SELECT password, two_factor_confirmed_at AS twoFactorConfirmedAt,
mail, mail_verified AS mailVerified
FROM users WHERE username = ? LIMIT 1`,
[u],
);
} catch {
return "invalid";
}
const user = await getLoginUser(u);
if (!user) {
// Prevent timing-based enumeration: always run a dummy hash check.
await checkLogin(
p,
"$2y$12$abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcd",
{
convertPasswords: false,
},
);
await runDummyHashCheck(p);
return "invalid";
}
const res = await checkLogin(p, user.password, {
convertPasswords: env.CONVERT_PASSWORDS,
});
const res = await verifyLoginPassword(user, p);
if (!res.valid) return "invalid";
if (
(await siteSettings.getBool("require_email_verification", false)) &&
user.mail &&
user.mailVerified !== "1"
) {
if (await isEmailUnverified(user)) {
return "unverified";
}