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
+192 -179

No files matched your search

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