Skip to content

Commit

Permalink
Merge pull request #17 from fless-lab/secutiry/enhance-app-security
Browse files Browse the repository at this point in the history
sec: bruteforce and api rate limit enhanced
  • Loading branch information
fless-lab authored Jul 19, 2024
2 parents 1210ea1 + 59313cf commit 1f2617a
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 29 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
6 changes: 3 additions & 3 deletions src/apps/auth/routes/auth.routes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Router } from 'express';

import { bruteForce, validate } from '../../shared';
import { bruteForceMiddleware, validate } from '../../shared';
import { AuthController } from '../controllers';
import {
forgotPasswordSchema,
Expand Down Expand Up @@ -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(
Expand Down
69 changes: 47 additions & 22 deletions src/apps/shared/middlewares/bruteforce.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
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<void> => {
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;
2 changes: 1 addition & 1 deletion src/apps/shared/middlewares/index.ts
Original file line number Diff line number Diff line change
@@ -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';
3 changes: 2 additions & 1 deletion src/apps/shared/middlewares/rate-limiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.`,
});
4 changes: 4 additions & 0 deletions src/framework/webserver/express.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
GlobalErrorHandler,
NotFoundHandler,
Routes as AllRoutes,
apiRateLimiter,
} from '../../apps';

const app = express();
Expand Down Expand Up @@ -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);

Expand Down

0 comments on commit 1f2617a

Please sign in to comment.