This commit is contained in:
Stefan Lazic committed 2026-08-03 21:15:24 +01:00
1 parent ec5583f972
commit 546613e677
189 files changed
+9873 -655

No files matched your search

+29 -4
View File
@@ -6,6 +6,17 @@ import Google from "next-auth/providers/google";
import { authConfig } from "@/auth.config";
import { prisma } from "@/lib/db/prisma";
import { signInSchema } from "@/features/auth/schemas";
import { checkRateLimit } from "@/lib/security/rate-limit";
// A dummy hash to compare against when no real user/passwordHash exists,
// so a "no such account" / "Google-only account" attempt still pays
// bcrypt's cost — without this, that branch returns near-instantly while
// a wrong-password-for-a-real-account attempt always waits for a real
// bcrypt.compare, letting a timing measurement distinguish "no such
// account" from "wrong password" (an account-enumeration side channel).
// Computed once at module load (not hand-typed) so it's guaranteed to be
// a valid hash at the same cost factor registration uses.
const DUMMY_HASH = bcrypt.hashSync("nextchair-timing-safety-placeholder", 12);
export const { handlers, auth, signIn, signOut } = NextAuth({
...authConfig,
@@ -28,11 +39,25 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
if (!parsed.success) return null;
const { email, password } = parsed.data;
const user = await prisma.user.findUnique({ where: { email } });
if (!user?.passwordHash) return null;
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) return null;
// Keyed by the attempted email, not IP — this is what actually
// stops credential stuffing against one account regardless of how
// many source addresses it's spread across. 30 attempts per 15
// minutes is well above anything a real forgetful user (or a
// shared/office login) would ever hit, while still making online
// guessing impractical — a real brute-force attempt wants
// thousands of guesses, not tens.
const { allowed } = checkRateLimit(`signin:${email.toLowerCase()}`, {
maxAttempts: 30,
windowMs: 15 * 60 * 1000,
});
if (!allowed) return null;
const user = await prisma.user.findUnique({ where: { email } });
// Always compare against *something* — see DUMMY_HASH above — so
// this call's timing doesn't reveal whether the account exists.
const valid = await bcrypt.compare(password, user?.passwordHash ?? DUMMY_HASH);
if (!user?.passwordHash || !valid) return null;
return { id: user.id, email: user.email, name: user.name, image: user.image };
},