From b77b33e790620b9b94f1f09c188ecc6e4f71d70e Mon Sep 17 00:00:00 2001 From: Louis Lam Date: Sat, 23 Oct 2021 16:35:13 +0800 Subject: [PATCH] add login rate limiter --- server/auth.js | 32 +++++++++++++++++++++----------- server/rate-limiter.js | 39 +++++++++++++++++++++++++++++++++++++++ server/server.js | 6 ++++++ 3 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 server/rate-limiter.js diff --git a/server/auth.js b/server/auth.js index 35d2a080..c476ea1e 100644 --- a/server/auth.js +++ b/server/auth.js @@ -1,8 +1,9 @@ -const basicAuth = require("express-basic-auth") +const basicAuth = require("express-basic-auth"); const passwordHash = require("./password-hash"); const { R } = require("redbean-node"); const { setting } = require("./util-server"); const { debug } = require("../src/util"); +const { loginRateLimiter } = require("./rate-limiter"); /** * @@ -13,7 +14,7 @@ const { debug } = require("../src/util"); exports.login = async function (username, password) { let user = await R.findOne("user", " username = ? AND active = 1 ", [ username, - ]) + ]); if (user && passwordHash.verify(password, user.password)) { // Upgrade the hash to bcrypt @@ -27,21 +28,30 @@ exports.login = async function (username, password) { } return null; -} +}; function myAuthorizer(username, password, callback) { - setting("disableAuth").then((result) => { - if (result) { - callback(null, true) + callback(null, true); } else { - exports.login(username, password).then((user) => { - callback(null, user != null) - }) - } - }) + // Login Rate Limit + loginRateLimiter.pass(null, 0).then((pass) => { + if (pass) { + exports.login(username, password).then((user) => { + callback(null, user != null); + if (user == null) { + loginRateLimiter.removeTokens(1); + } + }); + } else { + callback(null, false); + } + }); + + } + }); } exports.basicAuth = basicAuth({ diff --git a/server/rate-limiter.js b/server/rate-limiter.js new file mode 100644 index 00000000..0bacc14c --- /dev/null +++ b/server/rate-limiter.js @@ -0,0 +1,39 @@ +const { RateLimiter } = require("limiter"); +const { debug } = require("../src/util"); + +class KumaRateLimiter { + constructor(config) { + this.errorMessage = config.errorMessage; + this.rateLimiter = new RateLimiter(config); + } + + async pass(callback, num = 1) { + const remainingRequests = await this.removeTokens(num); + debug("Rate Limit (remainingRequests):" + remainingRequests); + if (remainingRequests < 0) { + if (callback) { + callback({ + ok: false, + msg: this.errorMessage, + }); + } + return false; + } + return true; + } + + async removeTokens(num = 1) { + return await this.rateLimiter.removeTokens(num); + } +} + +const loginRateLimiter = new KumaRateLimiter({ + tokensPerInterval: 20, + interval: "minute", + fireImmediately: true, + errorMessage: "Too frequently, try again later." +}); + +module.exports = { + loginRateLimiter +}; diff --git a/server/server.js b/server/server.js index 11f03061..b88c2f64 100644 --- a/server/server.js +++ b/server/server.js @@ -52,6 +52,7 @@ const Database = require("./database"); debug("Importing Background Jobs"); const { initBackgroundJobs } = require("./jobs"); +const { loginRateLimiter } = require("./rate-limiter"); const { basicAuth } = require("./auth"); const { login } = require("./auth"); @@ -281,6 +282,11 @@ exports.entryPage = "dashboard"; socket.on("login", async (data, callback) => { console.log("Login"); + // Login Rate Limit + if (! await loginRateLimiter.pass(callback)) { + return; + } + let user = await login(data.username, data.password); if (user) {