Securing Fastify Apps: Rate Limiting and Account Lockout Methods

66 views

To protect your Node.js Fastify application from brute-force attacks on the login form, you can implement several strategies. Here are some methods that you can consider:

  1. Rate Limiting: Limit the number of requests a single user can make to the login endpoint within a given timeframe. This way, you can slow down brute-force attacks by making them less effective.

  2. Account Lockout Policy: Temporarily lock out accounts after a certain number of failed login attempts. This discourages continued brute-force attempts on a single account.

  3. CAPTCHA: Implement CAPTCHAs after several failed login attempts to ensure that a human is performing the login attempts.

  4. IP Blocking: Block IPs that are making too many failed login attempts.

  5. Use Strong Passwords: Encourage or enforce strong password policies to make password guessing more difficult.

Here's a basic example of how you can implement rate limiting and account lockout in a Fastify application:

First, install a rate-limiting plugin such as fastify-rate-limit:

npm install fastify-rate-limit

Then, apply it to your Fastify instance:

const fastify = require('fastify')();
const fastifyRateLimit = require('fastify-rate-limit');

fastify.register(fastifyRateLimit, {
  max: 5, // maximum number of requests for a window
  timeWindow: '1 minute', // resets every minute
  keyGenerator: (req) => req.ip // use IP address as key
});

let failedAttempts = {}; // Track failed attempts

const MAX_FAILED_ATTEMPTS = 5; // Max failed attempts before lockout
const LOCK_TIME = 10 * 60 * 1000; // Lockout duration in milliseconds

fastify.post('/login', async (request, reply) => {
  const { username, password } = request.body;

  if (failedAttempts[username] && failedAttempts[username].blockUntil > Date.now()) {
    return reply.status(429).send({ error: 'Account temporarily locked due to too many failed login attempts. Please try again later.' });
  }

  const isAuthenticated = await authenticateUser(username, password);

  if (isAuthenticated) {
    failedAttempts[username] = { count: 0, blockUntil: null }; // reset attempts on successful login
    return reply.send({ success: true, message: 'Logged in successfully!' });
  } else {
    failedAttempts[username] = failedAttempts[username] || { count: 0, blockUntil: null };
    failedAttempts[username].count++;
    if (failedAttempts[username].count >= MAX_FAILED_ATTEMPTS) {
      failedAttempts[username].blockUntil = Date.now() + LOCK_TIME;
      return reply.status(429).send({ error: 'Too many failed login attempts, account is locked.' });
    }
    return reply.status(401).send({ error: 'Invalid username or password.' });
  }
});

async function authenticateUser(username, password) {
  // Replace with actual authentication logic against your user database
  // Return true if successful, false otherwise
  return false;
}

fastify.listen(3000, (err, address) => {
  if (err) throw err;
  console.log(`Server listening on ${address}`);
});

Key Points:

  • Rate Limiting: Limits the number of requests per IP to slow down brute force attacks.
  • Account Lockout: Temporarily locks an account after a certain number of failed attempts to prevent further attempts for a set duration.
  • Clean Up: Consider adding logic to periodically clean up the failedAttempts object to prevent memory leaks, especially for usernames that do not exist.

By applying these strategies, you'll enhance the security of your login form against brute-force attacks. Remember to tailor these strategies according to your specific requirements and user base.