Preventing Multiple Failed Input Attack using Redis and NodeJS
Tags: webdev, security, node, redis
Published: 2024-01-25 06:00:00

There might have been times when you have failed to login into a website due to wrong username/email and password combinations.

<img src="https://media.giphy.com/media/t0virGpgSlp4mkfiXq/giphy.gif" alt="brute force attack prevention"/>

Well, you might not know that this is a very common way of attacking your web application or server. 

This is known as Brute Force attack.

## Brute Force Attack 🚨
> A brute-force attack is an attempt to discover a password by systematically trying every possible combination of letters, numbers, and symbols until you discover the one correct combination that works. 

If your web site requires user authentication, you are a good target for a brute-force attack. So how do we prevent this? 

## Preventing Brute Force Attacks 🤯
For every attack, there is some way of preventing it. Similarly for brute force attacks there different ways to ensure that your application is not vulnerable to simple attacks.

- Asking the user to not use words or phrases directly related to them in the password.
- Asking the user to not use their Data of births since these are publically available.
- Warning the user to not use Dictionary words or even checking if they using(based on the level of security needed).
- Making the passwords more complicated by requiring usage of special characters and both upper and lower case combinations.

But if the user does not follow the above rules, we as developers are `👉🏻 BURDENED WITH THE GLORIOUS PURPOSE 👈🏻` of safeguarding the user's data and our own services.

<img src="https://media.giphy.com/media/VlrRiZAur3jylN2XKT/giphy.gif" alt="burdened with glorious purpose"/>


So let's explore how the implement one such counter measure.

## The Idea 💡
While some websites prefer blocking the IP or the user completely, unless a manual action is taken, I would not since the sensitivity is not the much. 

A better thing would be to slow down the IP to prevent brute force attack.

For this, blocking the IP for `60 seconds` in case of 5 wrong attempts within 60 seconds is enough. So I decided to use `Redis` as a fast in-memory solution to implement this without causing too much trouble for the user.

### Working of this architecture 👇🏻

![Brute Force Attack Prevention](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/69nnmz99vu5pvpiae5np.png)

Let's implement this now.

## Step 1 : Setup Redis server
Redis is an in-memory caching service that can be used in 2 ways : 
- Same server as the actual backend
- Using the Redis cloud service

For me, I decided to go with the cloud service as it is free to use upto a certain limit.

To setup your service, head on to : [Redis Cloud Console](https://redis.com/try-free/) and you will see multiple options to create your account.

![Redis Cloud](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3q1x931elzrdb3469rf6.png)

Once you sign up, you will get to a page like this. Fill your details and click on create free database : 

![Redis Cloud Setup](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hz6w58cau3sufvcjsamf.png)

Now, select the location of database closest to your location and hit `let's start free`.

And after some Redis magic, your free database is created!!


![Redis Setup](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s30uvg9ov5b02t9b7ncz.png)

Click on the `Connect` button to open a section that looks like this : 


![Redis Connection](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hmfwnk6r8vsojgwf93b9.png)

Clicking on copy will get the code to connect with this database from your NodeJS server.

## Step 2 : Setup NodeJS with Redis
To start using this Redis database, you first need to install Redis in your NodeJS server.

I am using `Redis version 4.6.12` while writing this. Run the following command to install redis : 
```js
npm i redis
```

Now create a new file with the following code in the utilities folder : 
```js
import { createClient } from 'redis';

const getRedisClient = () => {

let password = "", host = "", port = -1, client = null;
try {
    // console.log(process.env.REDIS_PASSWORD,process.env.REDIS_PORT,process.env.REDIS_URL)
    password = process.env.REDIS_PASSWORD;
    host = process.env.REDIS_URL;
    port = parseInt(process.env.REDIS_PORT);
    if(Number.isNaN(port)) throw new Error("Invalid PORT")
    client = createClient({
        password: password,
        socket: {
            host: host,
            port: port
        }
    }).on('error', err => console.log('Redis Client Error', err))
    .connect();
} catch (error) {
    console.log(error)
}
    return client;
}

export default getRedisClient;

```

> 🚨 Remember to always store credentials in `.env` file and import from the file to use.

In the above code, you can see the most of the code is copied from the Cloud Setup page. I only put the code in a `try-catch` block and use secure strings from Environment file. 


Also, I have made the connection function to call only when needed and not keep it on every time the server is running. This `prevents data leaks`.

## Step 3 : The Working Code
Now that the setup is done, head on to the Login controller. 

Here we will write the code for the following steps ; 
- Get the IP address of the user from the Request object
- Check if the IP is already blocked for the time. If so, throw and error.
- If not, check if the credentials are correct. If validated proceed with the refresh token creation.
- If not, use `Redis` to create and store the number of failed login attempts.

The code the same is : 
```js
    const ipObject = req.socket.address() as { address: string, port: number, family: string };
    const ipAddress = ipObject.address
    console.log("ip :", ipAddress);
    const client = await getRedisClient();
.
.
.
.
const salt = user.authentication.salt;
        const hashedPassword = await maskPassword(salt, password);
        if (hashedPassword != user.authentication.password) {
          const requestsMade = await client.incr(ipAddress)
          let ttl = -1;
          if (requestsMade === 1) {
            await client.expire(ipAddress, 60);
            ttl = 60;
          }
          else {
            ttl = await client.ttl(ipAddress);
          }
          if (requestsMade > 5) {
            throw new CustomError(
              "Too many attemps",
              503,
              "Validation Error",
              {
                ttl
              });
          }
```

The `getRedisClient()` function is the one we created earlier. It contains functions provided by Redis, which includes : 
- `incr()` that takes in a key-value pair and checks if the key exists. If key exists, it increases the previous value by 1 and if not, first creates key with value 0 and increases by 1.
- `expire()` which is used to set the duration of expiry for the key provided.

### There you have it, a method of preventing the user from using a Brute Force Attack.

If you like to know more about securing your websites from different attacks, I working on a whole series to securing a Web Application. Follow me for upcoming posts on Refresh Tokens and Access Tokens followed by other ways of securing web applications!!.