From 59313cf0038a63a43eac1d3d26a54cc835f6071e Mon Sep 17 00:00:00 2001 From: Abdou-Raouf ATARMLA Date: Fri, 19 Jul 2024 19:14:12 +0000 Subject: [PATCH] sec: bruteforce and api rate limit enhanced --- package.json | 5 +- src/apps/auth/routes/auth.routes.ts | 6 +- src/apps/shared/middlewares/bruteforce.ts | 69 ++++++++++++++------- src/apps/shared/middlewares/index.ts | 2 +- src/apps/shared/middlewares/rate-limiter.ts | 3 +- src/framework/webserver/express.ts | 4 ++ 6 files changed, 60 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 7ded0c7..4e4b469 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "ejs": "^3.1.10", "express": "^4.19.2", "express-brute": "^1.0.1", - "express-brute-mongo": "^1.0.0", "express-brute-redis": "^0.0.1", "express-list-endpoints": "^7.1.0", "express-rate-limit": "^7.3.1", @@ -54,7 +53,9 @@ "minio": "^8.0.0", "mongoose": "^8.4.3", "morgan": "^1.10.0", - "nodemailer": "^6.9.14" + "nodemailer": "^6.9.14", + "rate-limiter-flexible": "^5.0.3", + "winston": "^3.13.1" }, "devDependencies": { "@commitlint/cli": "^19.3.0", diff --git a/src/apps/auth/routes/auth.routes.ts b/src/apps/auth/routes/auth.routes.ts index 24f7ed0..33038fb 100644 --- a/src/apps/auth/routes/auth.routes.ts +++ b/src/apps/auth/routes/auth.routes.ts @@ -1,6 +1,6 @@ import { Router } from 'express'; -import { bruteForce, validate } from '../../shared'; +import { bruteForceMiddleware, validate } from '../../shared'; import { AuthController } from '../controllers'; import { forgotPasswordSchema, @@ -30,13 +30,13 @@ router.post( router.post( '/login-with-password', validate(loginWithPasswordSchema), - bruteForce.prevent, + bruteForceMiddleware, AuthController.loginWithPassword, ); router.post( '/login-with-otp', validate(loginWithOtpSchema), - bruteForce.prevent, + bruteForceMiddleware, AuthController.loginWithOtp, ); router.post( diff --git a/src/apps/shared/middlewares/bruteforce.ts b/src/apps/shared/middlewares/bruteforce.ts index 29c9df5..33f002a 100644 --- a/src/apps/shared/middlewares/bruteforce.ts +++ b/src/apps/shared/middlewares/bruteforce.ts @@ -1,27 +1,52 @@ -import Brute from 'express-brute'; -import MongooseStore from 'express-brute-mongo'; -import mongoose from 'mongoose'; +import { RateLimiterMongo } from 'rate-limiter-flexible'; +import { Request, Response, NextFunction } from 'express'; import config from '../../../config'; +import { DB } from '../../../framework'; -const store = new MongooseStore(function (ready) { - mongoose.connect(config.db.uri, { - dbName: config.db.name, - }); - ready(mongoose.connection.collection('bruteforce-store')); -}); - -const bruteForce = new Brute(store, { - freeRetries: config.bruteForce.freeRetries, // Number of allowed retries before applying restrictions - minWait: config.bruteForce.minWait, // Minimum wait time (in milliseconds) - maxWait: config.bruteForce.maxWait, // Maximum wait time (in milliseconds) - lifetime: config.bruteForce.lifetime, // Lifetime (in seconds) - failCallback: function (req, res, next, nextValidRequestDate) { +let bruteForceLimiter: RateLimiterMongo | undefined; + +const setupRateLimiter = async (): Promise => { + try { + await DB.mongo.init(config.db.uri, config.db.name); + const mongoConn = await DB.mongo.getClient(); + + bruteForceLimiter = new RateLimiterMongo({ + storeClient: mongoConn, + points: config.bruteForce.freeRetries, // Nombre de tentatives autorisées + duration: Math.ceil(config.bruteForce.lifetime / 1000), // Durée de vie en secondes + blockDuration: Math.ceil(config.bruteForce.maxWait / 1000), // Durée de blocage en secondes + }); + + console.log('Rate limiter configured.'); + } catch (error) { + console.error('Error setting up rate limiter:', error); + } +}; + +setupRateLimiter(); + +const bruteForceMiddleware = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise => { + if (!bruteForceLimiter) { + res.status(500).json({ + message: 'Rate limiter not configured yet. Please try again later.', + }); + return; + } + + try { + await bruteForceLimiter.consume(req.ip as string); + next(); + } catch (rejRes: any) { + const retrySecs = Math.ceil(rejRes.msBeforeNext / 1000) || 1; + !config.runningProd && res.set('Retry-After', String(retrySecs)); //Send Retry-After only in dev mode res.status(429).json({ - message: `Too many attempts, please try again after ${Math.ceil( - (nextValidRequestDate.getTime() - Date.now()) / 60000, - )} minutes.`, + message: `Too many attempts, please try again after ${Math.ceil(rejRes.msBeforeNext / 60000)} minutes.`, }); - }, -}); + } +}; -export default bruteForce; +export default bruteForceMiddleware; diff --git a/src/apps/shared/middlewares/index.ts b/src/apps/shared/middlewares/index.ts index 3c0d723..198088f 100644 --- a/src/apps/shared/middlewares/index.ts +++ b/src/apps/shared/middlewares/index.ts @@ -1,5 +1,5 @@ export * from './client-authentication'; export { default as authenticateRequest } from './authenticate-request'; -export { default as bruteForce } from './bruteforce'; +export { default as bruteForceMiddleware } from './bruteforce'; export * from './rate-limiter'; export * from './validate'; diff --git a/src/apps/shared/middlewares/rate-limiter.ts b/src/apps/shared/middlewares/rate-limiter.ts index 2155177..f16f8d6 100644 --- a/src/apps/shared/middlewares/rate-limiter.ts +++ b/src/apps/shared/middlewares/rate-limiter.ts @@ -6,8 +6,9 @@ const msToMinutes = (ms: number): number => { return Math.ceil(ms / 60000); }; -export const apiLimiter = rateLimit({ +export const apiRateLimiter = rateLimit({ windowMs: config.rate.limit, // Time window in milliseconds max: config.rate.max, // Maximum number of requests + standardHeaders: !config.runningProd, //Show ratelimit headers when not in production message: `Too many requests from this IP, please try again after ${msToMinutes(config.rate.limit)} minutes.`, }); diff --git a/src/framework/webserver/express.ts b/src/framework/webserver/express.ts index 4032cf8..e175b0c 100644 --- a/src/framework/webserver/express.ts +++ b/src/framework/webserver/express.ts @@ -11,6 +11,7 @@ import { GlobalErrorHandler, NotFoundHandler, Routes as AllRoutes, + apiRateLimiter, } from '../../apps'; const app = express(); @@ -57,6 +58,9 @@ initializeViewEngine(app); // Client authentication middleware app.use(clientAuthentication); +// Client authentication middleware +app.use(apiRateLimiter); + // API Routes app.use('/api/v1', AllRoutes);