How to prevent brute force attacks in Node.js
Brute force attacks attempt to gain unauthorized access by systematically trying all possible password combinations. As the creator of CoreUI with 12 years of Node.js backend experience, I’ve implemented brute force protection strategies that successfully blocked millions of attack attempts while maintaining seamless user experience for legitimate users in enterprise applications.
The most effective approach combines rate limiting, account lockout, and CAPTCHA challenges.
Rate Limiting on Login Endpoint
Install dependencies:
npm install express-rate-limit express-slow-down
Implement strict rate limiting:
const express = require('express')
const rateLimit = require('express-rate-limit')
const app = express()
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per window
message: 'Too many login attempts. Please try again later.',
standardHeaders: true,
legacyHeaders: false
})
app.post('/api/login', loginLimiter, async (req, res) => {
const { username, password } = req.body
// Authentication logic here
const user = await authenticateUser(username, password)
if (user) {
res.json({ success: true, token: generateToken(user) })
} else {
res.status(401).json({ error: 'Invalid credentials' })
}
})
app.listen(3000)
Progressive Delays
Slow down requests progressively:
const slowDown = require('express-slow-down')
const speedLimiter = slowDown({
windowMs: 15 * 60 * 1000, // 15 minutes
delayAfter: 3, // Allow 3 requests at full speed
delayMs: 500, // Add 500ms delay per request after delayAfter
maxDelayMs: 5000 // Max delay of 5 seconds
})
app.post('/api/login', speedLimiter, loginLimiter, async (req, res) => {
// Login logic
})
Account Lockout
Implement temporary account lockout:
const loginAttempts = new Map()
async function checkAccountLockout(username) {
const attempts = loginAttempts.get(username)
if (!attempts) {
return false // Not locked
}
if (attempts.count >= 5 && Date.now() < attempts.lockedUntil) {
return true // Account locked
}
if (Date.now() >= attempts.lockedUntil) {
loginAttempts.delete(username) // Unlock account
return false
}
return false
}
function recordFailedAttempt(username) {
const attempts = loginAttempts.get(username) || {
count: 0,
lockedUntil: null
}
attempts.count++
if (attempts.count >= 5) {
attempts.lockedUntil = Date.now() + (30 * 60 * 1000) // Lock for 30 minutes
}
loginAttempts.set(username, attempts)
}
app.post('/api/login', loginLimiter, async (req, res) => {
const { username, password } = req.body
if (await checkAccountLockout(username)) {
return res.status(429).json({
error: 'Account temporarily locked. Please try again later.'
})
}
const user = await authenticateUser(username, password)
if (user) {
loginAttempts.delete(username) // Clear attempts on success
res.json({ success: true, token: generateToken(user) })
} else {
recordFailedAttempt(username)
res.status(401).json({ error: 'Invalid credentials' })
}
})
Database-Based Account Lockout
For production, store lockout data in database:
const User = require('./models/User')
async function checkAndLockAccount(username) {
const user = await User.findOne({ username })
if (!user) {
return null
}
if (user.lockoutUntil && user.lockoutUntil > Date.now()) {
const minutesLeft = Math.ceil((user.lockoutUntil - Date.now()) / 60000)
throw new Error(`Account locked. Try again in ${minutesLeft} minutes`)
}
return user
}
async function recordFailedLogin(user) {
user.failedLoginAttempts = (user.failedLoginAttempts || 0) + 1
if (user.failedLoginAttempts >= 5) {
user.lockoutUntil = Date.now() + (30 * 60 * 1000) // 30 minutes
}
await user.save()
}
async function resetFailedAttempts(user) {
user.failedLoginAttempts = 0
user.lockoutUntil = null
await user.save()
}
app.post('/api/login', loginLimiter, async (req, res) => {
const { username, password } = req.body
try {
const user = await checkAndLockAccount(username)
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' })
}
const isValid = await bcrypt.compare(password, user.password)
if (isValid) {
await resetFailedAttempts(user)
const token = generateToken(user)
res.json({ success: true, token })
} else {
await recordFailedLogin(user)
res.status(401).json({ error: 'Invalid credentials' })
}
} catch (error) {
res.status(429).json({ error: error.message })
}
})
CAPTCHA Integration
Add CAPTCHA after failed attempts:
const axios = require('axios')
async function verifyCaptcha(token) {
const response = await axios.post(
'https://www.google.com/recaptcha/api/siteverify',
null,
{
params: {
secret: process.env.RECAPTCHA_SECRET,
response: token
}
}
)
return response.data.success
}
app.post('/api/login', loginLimiter, async (req, res) => {
const { username, password, captchaToken } = req.body
const user = await User.findOne({ username })
// Require CAPTCHA after 3 failed attempts
if (user && user.failedLoginAttempts >= 3) {
if (!captchaToken) {
return res.status(400).json({
error: 'CAPTCHA required',
requiresCaptcha: true
})
}
const captchaValid = await verifyCaptcha(captchaToken)
if (!captchaValid) {
return res.status(400).json({
error: 'Invalid CAPTCHA'
})
}
}
// Continue with authentication
const isValid = await bcrypt.compare(password, user.password)
if (isValid) {
await resetFailedAttempts(user)
res.json({ success: true, token: generateToken(user) })
} else {
await recordFailedLogin(user)
res.status(401).json({
error: 'Invalid credentials',
requiresCaptcha: user.failedLoginAttempts >= 3
})
}
})
Email Notifications
Alert users about suspicious activity:
const nodemailer = require('nodemailer')
async function sendSecurityAlert(user, ip) {
const transporter = nodemailer.createTransporter({
service: 'gmail',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS
}
})
await transporter.sendMail({
from: process.env.EMAIL_USER,
to: user.email,
subject: 'Security Alert: Multiple Failed Login Attempts',
html: `
<p>We detected multiple failed login attempts on your account.</p>
<p>IP Address: ${ip}</p>
<p>Time: ${new Date().toISOString()}</p>
<p>If this wasn't you, please reset your password immediately.</p>
`
})
}
async function recordFailedLogin(user, ip) {
user.failedLoginAttempts = (user.failedLoginAttempts || 0) + 1
if (user.failedLoginAttempts === 3) {
await sendSecurityAlert(user, ip)
}
if (user.failedLoginAttempts >= 5) {
user.lockoutUntil = Date.now() + (30 * 60 * 1000)
}
await user.save()
}
Best Practice Note
This is the same brute force protection strategy we use in CoreUI enterprise applications to protect millions of user accounts. Always implement progressive delays, temporary account lockouts, and CAPTCHA challenges after repeated failures. Log all failed attempts for security monitoring, and notify users via email when suspicious activity is detected. Use Redis for distributed systems to track attempts across multiple servers.
Related Articles
For related security patterns, check out how to use rate limiting in Node.js and how to implement authentication in Node.js.



