Safer web: why does brute-force protection of login endpoints so important?
Tags: security, bruteforce, node, javascript
Published: 2019-02-02 12:20:26

---
title: Safer web: why does brute-force protection of login endpoints so important?
published: true
description: Secure web applications against brute-force attacks helps to make internet more secure.
tags: security, brute-force, nodejs, javascript
---

We all know why. Because it saves private data and money. But that is not all. The most important, that it makes the internet safer place over all, so users can get better experience and be happier with web services.

Some time ago I've created a Node.js package [rate-limiter-flexible](https://github.com/animir/node-rate-limiter-flexible), which provides tools against DoS and brute-force attacks with many features. I dived into this topic and discovered, that some javascript open-source projects don't care much about security. I am not sure about projects on other languages, but guess it is the same. There are many e-commerce projects, which don't care much too.

I've recently posted an article about brute-force protection with analysis and examples. You can read full version [here](https://medium.com/@animirr/secure-web-applications-against-brute-force-b910263de2ab).

Here is one example, first of all as a reminder, that we (developers, PMs, CEOs, etc) should take care of it. No time to write extra code? No worries, it is easy.

The main idea of protection is risk minimisation. Login endpoint limits number of allowed requests and block extra requests. 
We should create 2 different limiters:
1. The first counts number of consecutive failed attempts and allows maximum 10 by Username+IP pair. 
2. The second blocks IP for 1 day on 100 failed attempts per day.

```javascript
const http = require('http');
const express = require('express');
const redis = require('redis');
const { RateLimiterRedis } = require('rate-limiter-flexible');
const redisClient = redis.createClient({
  enable_offline_queue: false,
});

const maxWrongAttemptsByIPperDay = 100;
const maxConsecutiveFailsByUsernameAndIP = 10;

const limiterSlowBruteByIP = new RateLimiterRedis({
  redis: redisClient,
  keyPrefix: 'login_fail_ip_per_day',
  points: maxWrongAttemptsByIPperDay,
  duration: 60 * 60 * 24,
  blockDuration: 60 * 60 * 24, // Block for 1 day, if 100 wrong attempts per day
});

const limiterConsecutiveFailsByUsernameAndIP = new RateLimiterRedis({
  redis: redisClient,
  keyPrefix: 'login_fail_consecutive_username_and_ip',
  points: maxConsecutiveFailsByUsernameAndIP,
  duration: 60 * 60 * 24 * 90, // Store number for 90 days since first fail
  blockDuration: 60 * 60 * 24 * 365 * 20, // Block for infinity after consecutive fails
});

const getUsernameIPkey = (username, ip) => `${username}_${ip}`;

async function loginRoute(req, res) {
  const ipAddr = req.connection.remoteAddress;
  const usernameIPkey = getUsernameIPkey(req.body.email, ipAddr);

  const [resUsernameAndIP, resSlowByIP] = await Promise.all([
    limiterConsecutiveFailsByUsernameAndIP.get(usernameIPkey),
    limiterSlowBruteByIP.get(ipAddr),
  ]);

  let retrySecs = 0;

  // Check if IP or Username + IP is already blocked
  if (resSlowByIP !== null && resSlowByIP.remainingPoints <= 0) {
    retrySecs = Math.round(resSlowByIP.msBeforeNext / 1000) || 1;
  } else if (resUsernameAndIP !== null && resUsernameAndIP.remainingPoints <= 0) {
    retrySecs = Math.round(resUsernameAndIP.msBeforeNext / 1000) || 1;
  }

  if (retrySecs > 0) {
    res.set('Retry-After', String(retrySecs));
    res.status(429).send('Too Many Requests');
  } else {
    const user = authorise(req.body.email, req.body.password);
    if (!user.isLoggedIn) {
      // Consume 1 point from limiters on wrong attempt and block if limits reached
      try {
        const promises = [limiterSlowBruteByIP.consume(ipAddr)];
        if (user.exists) {
          // Count failed attempts by Username + IP only for registered users
          promises.push(limiterConsecutiveFailsByUsernameAndIP.consume(usernameIPkey));
        }

        await promises;

        res.status(400).end('email or password is wrong');
      } catch (rlRejected) {
        if (rlRejected instanceof Error) {
          throw rlRejected;
        } else {
          res.set('Retry-After', String(Math.round(rlRejected.msBeforeNext / 1000)) || 1);
          res.status(429).send('Too Many Requests');
        }
      }
    }

    if (user.isLoggedIn) {
      if (resUsernameAndIP !== null && resUsernameAndIP.consumedPoints > 0) {
        // Reset on successful authorisation
        await limiterConsecutiveFailsByUsernameAndIP.delete(usernameIPkey);
      }

      res.end('authorized');
    }
  }
}

const app = express();

app.post('/login', async (req, res) => {
  try {
    await loginRoute(req, res);
  } catch (err) {
    res.status(500).end();
  }
});
```

Implementation of unblocking is up to you, there is suitable `delete(key)` method.

More examples in [this article](https://medium.com/@animirr/brute-force-protection-node-js-examples-cd58e8bd9b8d) and in [official docs](https://github.com/animir/node-rate-limiter-flexible/wiki)