Preventing Brute Force Using Node and Express JS
Asked Answered
L

6

19

I'm building a website using Node and Express JS and would like to throttle invalid login attempts. Both to prevent online cracking and to reduce unnecessary database calls. What are some ways in which I can implement this?

Lemoine answered 30/10, 2013 at 18:24 Comment(0)
C
13

Maybe something like this might help you get started.

var failures = {};

function tryToLogin() {
    var f = failures[remoteIp];
    if (f && Date.now() < f.nextTry) {
        // Throttled. Can't try yet.
        return res.error();
    }

    // Otherwise do login
    ...
}

function onLoginFail() {
    var f = failures[remoteIp] = failures[remoteIp] || {count: 0, nextTry: new Date()};
    ++f.count;
    f.nextTry.setTime(Date.now() + 2000 * f.count); // Wait another two seconds for every failed attempt
}

function onLoginSuccess() { delete failures[remoteIp]; }

// Clean up people that have given up
var MINS10 = 600000, MINS30 = 3 * MINS10;
setInterval(function() {
    for (var ip in failures) {
        if (Date.now() - failures[ip].nextTry > MINS10) {
            delete failures[ip];
        }
    }
}, MINS30);
Cupboard answered 30/10, 2013 at 19:42 Comment(5)
This will slowly fill up your RAM since the failed IPs are never deleted from failures if a login never actually succeeds.Puca
@Puca Good point. Added a function to clean it up every 30 minutes.Cupboard
What do you think of solutions such as express-brute? npmjs.org/package/express-bruteLemoine
I actually like your solution a lot more!Lemoine
The problem with @TrevorDixon solution is the cleanup function is blocking code. Let's say failures object has 2000 entries you have a for loop blocking it until all 2000 entries are processed. Use each with callback to make it non blocking.Wingard
O
15

rate-limiter-flexible package with Redis or Mongo for distributed apps and in-Memory or with Cluster helps

Here is example with Redis

const { RateLimiterRedis } = require('rate-limiter-flexible');
const Redis = require('ioredis');

const redisClient = new Redis({
  options: {
    enableOfflineQueue: false
  }
});

const opts = {
  redis: redisClient,
  points: 5, // 5 points
  duration: 15 * 60, // Per 15 minutes
  blockDuration: 15 * 60, // block for 15 minutes if more than points consumed 
};

const rateLimiter = new RateLimiterRedis(opts);

app.post('/auth', (req, res, next) => {
  // Consume 1 point for each login attempt
  rateLimiter.consume(req.connection.remoteAddress)
    .then((data) => {
      const loggedIn = loginUser();
      if (!loggedIn) {
        // Message to user
        res.status(400).send(data.remainingPoints + ' attempts left');
      } else {
        // successful login
      }
    })
    .catch((rejRes) => {
      // Blocked
      const secBeforeNext = Math.ceil(rejRes.msBeforeNext / 1000) || 1;
      res.set('Retry-After', String(secBeforeNext));
      res.status(429).send('Too Many Requests');
    });
});
Obscuration answered 8/6, 2018 at 6:46 Comment(1)
This package looks to do the job nicely for distributed systems. Though your implementation does not block the user from attempting to login again after hitting the limit. You need to place const loggedIn = loginUser() within the rateLimiter.consume.then() block. This will properly block the login attempt for future requests.Overcloud
C
13

Maybe something like this might help you get started.

var failures = {};

function tryToLogin() {
    var f = failures[remoteIp];
    if (f && Date.now() < f.nextTry) {
        // Throttled. Can't try yet.
        return res.error();
    }

    // Otherwise do login
    ...
}

function onLoginFail() {
    var f = failures[remoteIp] = failures[remoteIp] || {count: 0, nextTry: new Date()};
    ++f.count;
    f.nextTry.setTime(Date.now() + 2000 * f.count); // Wait another two seconds for every failed attempt
}

function onLoginSuccess() { delete failures[remoteIp]; }

// Clean up people that have given up
var MINS10 = 600000, MINS30 = 3 * MINS10;
setInterval(function() {
    for (var ip in failures) {
        if (Date.now() - failures[ip].nextTry > MINS10) {
            delete failures[ip];
        }
    }
}, MINS30);
Cupboard answered 30/10, 2013 at 19:42 Comment(5)
This will slowly fill up your RAM since the failed IPs are never deleted from failures if a login never actually succeeds.Puca
@Puca Good point. Added a function to clean it up every 30 minutes.Cupboard
What do you think of solutions such as express-brute? npmjs.org/package/express-bruteLemoine
I actually like your solution a lot more!Lemoine
The problem with @TrevorDixon solution is the cleanup function is blocking code. Let's say failures object has 2000 entries you have a for loop blocking it until all 2000 entries are processed. Use each with callback to make it non blocking.Wingard
L
12

So after doing some searching, I wasn't able to find a solution I liked so I wrote my own based on Trevor's solution and express-brute. You can find it here.

Lemoine answered 6/11, 2013 at 23:44 Comment(0)
C
4

okk,i found the solution of max login attemp on wrong password in mongoose and expressjs.there is a solution. *first we will define the user schema *second we will define the max login on wrongpassword handler function. *third when we will create the login api then we will check this function that how many times user login with wrong password.so be ready for code

var config = require('../config');


var userSchema = new mongoose.Schema({
    email: { type: String, unique: true, required: true },
    password: String,
    verificationToken: { type: String, unique: true, required: true },
    isVerified: { type: Boolean, required: true, default: false },
    passwordResetToken: { type: String, unique: true },
    passwordResetExpires: Date,
    loginAttempts: { type: Number, required: true, default: 0 },
    lockUntil: Number,
    role: String
});

userSchema.virtual('isLocked').get(function() {
    return !!(this.lockUntil && this.lockUntil > Date.now());
});
userSchema.methods.incrementLoginAttempts = function(callback) {
    console.log("lock until",this.lockUntil)
    // if we have a previous lock that has expired, restart at 1
    var lockExpired = !!(this.lockUntil && this.lockUntil < Date.now());
console.log("lockExpired",lockExpired)
    if (lockExpired) {
        return this.update({
            $set: { loginAttempts: 1 },
            $unset: { lockUntil: 1 }
        }, callback);
    }
// otherwise we're incrementing
    var updates = { $inc: { loginAttempts: 1 } };
         // lock the account if we've reached max attempts and it's not locked already
    var needToLock = !!(this.loginAttempts + 1 >= config.login.maxAttempts && !this.isLocked);
console.log("needToLock",needToLock)
console.log("loginAttempts",this.loginAttempts)
    if (needToLock) {
        updates.$set = { lockUntil: Date.now() + config.login.lockoutHours };
        console.log("config.login.lockoutHours",Date.now() + config.login.lockoutHours)
    }
//console.log("lockUntil",this.lockUntil)
    return this.update(updates, callback);
};

here is my login function where we have checked the max login attempt on wrong password.so we will call this function

User.findOne({ email: email }, function(err, user) {
        console.log("i am aurhebengdfhdbndbcxnvndcvb")
        if (!user) {
            return done(null, false, { msg: 'No user with the email ' + email + ' was found.' });
        }

        if (user.isLocked) {
            return user.incrementLoginAttempts(function(err) {
                if (err) {
                    return done(err);
                }

                return done(null, false, { msg: 'You have exceeded the maximum number of login attempts.  Your account is locked until ' + moment(user.lockUntil).tz(config.server.timezone).format('LT z') + '.  You may attempt to log in again after that time.' });
            });
        }

        if (!user.isVerified) {
            return done(null, false, { msg: 'Your email has not been verified.  Check your inbox for a verification email.<p><a href="/user/verify-resend/' + email + '" class="btn waves-effect white black-text"><i class="material-icons left">email</i>Re-send verification email</a></p>' });
        }

        user.comparePassword(password, function(err, isMatch) {
            if (isMatch) {
                return done(null, user);
            }
            else {
                user.incrementLoginAttempts(function(err) {
                    if (err) {
                        return done(err);
                    }

                    return done(null, false, { msg: 'Invalid password.  Please try again.' });
                });
            }
        });
    });
}));
Celindaceline answered 4/8, 2016 at 5:24 Comment(1)
my config.js file is hereCelindaceline
T
2

Have a look on this: https://github.com/AdamPflug/express-brute A brute-force protection middleware for express routes that rate-limits incoming requests, increasing the delay with each request in a fibonacci-like sequence.

Thimbleful answered 24/2, 2017 at 14:1 Comment(1)
ohhh this is smartKuth
M
0

I myself wondered how to tackle this, but I tried the following and I am not sure how good is it in terms of performance and good code.

Basically, I created a flag in my Schema called "login attempts" and set it to 0
Then in the login process, I do the following: compare the password, if it's okay then I log in. Else, I increment the login attempt flag in my DB each time the user enters the wrong password. If the login attempts exceed 3, I display an error message saying that you exceeded login attempts.

Now up to this point everything works, the next part is pretty much way of switching that flag to zero.

Now I used setTimeout function to run after 5 mins and switch that flag to 0 and it worked.

My main concern: Is it safe to use setTimeout like this.

the other concern is how is this going to affect the performance.

So in terms of getting the job done, it's working but in terms of performance and best method, I am not sure about that.

Marbut answered 25/4, 2020 at 18:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.