From b283dbb670e09112299fb0cf89f3cb054ecc1700 Mon Sep 17 00:00:00 2001 From: Karl Ludwig Weise Date: Tue, 5 May 2026 12:15:10 +0200 Subject: [PATCH] Added throttle for passphrase login --- src/lib/server/services/challenge-throttle.ts | 2 +- src/routes/api/auth/login/+server.ts | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/lib/server/services/challenge-throttle.ts b/src/lib/server/services/challenge-throttle.ts index 6b7710a..5c36b69 100644 --- a/src/lib/server/services/challenge-throttle.ts +++ b/src/lib/server/services/challenge-throttle.ts @@ -12,7 +12,7 @@ import { challengeThrottle } from "$lib/server/db/central-schema"; import { eq, lt, sql } from "drizzle-orm"; import { logger } from "$lib/logger"; -export type ThrottleType = "pin" | "passkey"; +export type ThrottleType = "pin" | "passkey" | "passphrase"; interface ThrottleResult { allowed: boolean; diff --git a/src/routes/api/auth/login/+server.ts b/src/routes/api/auth/login/+server.ts index 2a8016e..ee8bc1f 100644 --- a/src/routes/api/auth/login/+server.ts +++ b/src/routes/api/auth/login/+server.ts @@ -137,6 +137,32 @@ export const POST: RequestHandler = async ({ request, cookies, getClientAddress, authMethod: body.passphrase ? "passphrase" : "webauthn", }); + const throttleResult = await challengeThrottleService.checkThrottle( + body.email, + body.passphrase ? "passphrase" : "passkey", + ); + + if (!throttleResult.allowed) { + logger.warn("Login throttled", { + email: body.email, + retryAfterMs: throttleResult.retryAfterMs, + failedAttempts: throttleResult.failedAttempts, + }); + + return json( + { + error: "Too many failed attempts. Please try again later.", + retryAfterMs: throttleResult.retryAfterMs, + }, + { + status: 429, + headers: { + "Retry-After": Math.ceil(throttleResult.retryAfterMs / 1000).toString(), + }, + }, + ); + } + // Validate that either passphrase or credential is provided if (!body.passphrase && !body.credential) { return json( @@ -222,9 +248,13 @@ export const POST: RequestHandler = async ({ request, cookies, getClientAddress, const isPassphraseValid = await verifyPassphrase(user.passphraseHash, body.passphrase); if (!isPassphraseValid) { + await challengeThrottleService.recordFailedAttempt(body.email, "passphrase"); return json({ error: "Invalid passphrase" }, { status: 401 }); } + // Clear throttle on successful authentication + await challengeThrottleService.clearThrottle(body.email, "passphrase"); + logger.debug("Passphrase authentication successful", { userId: user.id }); }