From 4aaeb0d5790a7792ea967c6a710aa959654e23f1 Mon Sep 17 00:00:00 2001 From: Joshua Ryder Date: Wed, 5 Nov 2025 18:32:17 -0500 Subject: [PATCH] fix: Align account lockout threshold with IP rate limiting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed inconsistency between IP-based rate limiting and per-account lockout. Previously, users would hit the IP rate limit at 5 attempts (15 min lockout) but the account wouldn't be marked as locked until 10 attempts (30 min). This caused confusion in the admin UI where locked accounts wouldn't show the unlock button until 10 attempts were reached. Changes: - Reduced account lockout threshold from 10 to 5 failed attempts - Reduced account lockout duration from 30 to 15 minutes - Updated error message to reflect 15 minute lockout period - Added detailed logging when account gets locked - Updated README documentation to reflect correct limits Both protection layers now work in harmony: - IP-based rate limiting: 5 attempts = 15 min lockout - Per-account lockout: 5 attempts = 15 min lock This ensures the admin UI accurately shows account lock status and provides the unlock option as soon as users hit the lockout threshold. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 4 ++-- server/api/auth/login.post.ts | 2 +- server/utils/database.ts | 8 +++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f249c7a..cf0392a 100644 --- a/README.md +++ b/README.md @@ -259,8 +259,8 @@ This application implements enterprise-grade security following OWASP best pract - Protection against session fixation attacks 5. **Account Lockout (Dual-Layer Brute Force Protection)** - - IP-based rate limiting (existing) - - Per-account lockout: 10 failed attempts = 30 minute lock + - IP-based rate limiting: 5 failed attempts = 15 minute lockout + - Per-account lockout: 5 failed attempts = 15 minute lock - Automatic unlock after expiration - Admin manual unlock capability via UI diff --git a/server/api/auth/login.post.ts b/server/api/auth/login.post.ts index b8c1940..4b3be2d 100644 --- a/server/api/auth/login.post.ts +++ b/server/api/auth/login.post.ts @@ -57,7 +57,7 @@ export default defineEventHandler(async (event) => { console.log(`[LOGIN BLOCKED] Account locked - User: ${username.toLowerCase()}, IP: ${clientIp}`) throw createError({ statusCode: 403, - message: 'Account temporarily locked due to too many failed login attempts. Please try again in 30 minutes or contact an administrator.' + message: 'Account temporarily locked due to too many failed login attempts. Please try again in 15 minutes or contact an administrator.' }) } diff --git a/server/utils/database.ts b/server/utils/database.ts index 448ed49..392f6fb 100644 --- a/server/utils/database.ts +++ b/server/utils/database.ts @@ -469,17 +469,19 @@ export function incrementFailedAttempts(username: string): void { if (!user) return const newAttempts = (user.failed_login_attempts || 0) + 1 - const maxAttempts = 10 + const maxAttempts = 5 // Aligned with IP rate limiting if (newAttempts >= maxAttempts) { - // Lock account for 30 minutes - const lockUntil = new Date(Date.now() + 30 * 60 * 1000).toISOString() + // Lock account for 15 minutes (aligned with IP rate limiting) + const lockUntil = new Date(Date.now() + 15 * 60 * 1000).toISOString() db.prepare('UPDATE users SET failed_login_attempts = ?, locked_until = ? WHERE username = ?') .run(newAttempts, lockUntil, username) + console.log(`[ACCOUNT LOCKED] User ${username} locked after ${newAttempts} failed attempts until ${lockUntil}`) } else { // Just increment counter db.prepare('UPDATE users SET failed_login_attempts = ? WHERE username = ?') .run(newAttempts, username) + console.log(`[ACCOUNT SECURITY] Failed login attempt #${newAttempts} for user: ${username}`) } }