From 63c8ef78ca9a5b7419959cbcf3e3fbd1e01fb7a8 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 5 Mar 2024 01:45:43 +0100 Subject: [PATCH] [Task] #43, add slowDown and RateLimit for failed login attempts --- src/controller/read.ts | 41 +++++++++++++++++++++++++++-------------- src/controller/write.ts | 2 +- src/middleware/limit.ts | 31 +++++++++++++++++++++++-------- 3 files changed, 51 insertions(+), 23 deletions(-) diff --git a/src/controller/read.ts b/src/controller/read.ts index 439df34..e3de1c2 100644 --- a/src/controller/read.ts +++ b/src/controller/read.ts @@ -4,7 +4,8 @@ import { create as createError } from '@src/middleware/error'; import { validationResult, query } from 'express-validator'; import jwt from 'jsonwebtoken'; import logger from '@src/scripts/logger'; -import { create } from 'domain'; +import { crypt } from '@src/scripts/crypt'; +import { loginSlowDown, loginLimiter } from '@src/middleware/limit'; const router = express.Router(); @@ -43,29 +44,41 @@ router.get("/login/", async function login(req: Request, res: Response) { res.render("login-form"); }); -router.post("/login/", async function postLogin(req: Request, res: Response) { +router.post("/login/", loginSlowDown, async function postLogin(req: Request, res: Response, next: NextFunction) { logger.log("post login was called"); logger.log(req.body); res.locals.text = "post recieved"; - - // TODO login authentication here - const validLogin = true; - if (!validLogin) { - return res.redirect("/read/login"); - } else { - createToken(req, res); - res.render("login-form"); // TODO Send Token only - } + loginLimiter(req, res, () => { + let validLogin = false; + const password = crypt(req.body.password); + // Loop through all environment variables + for (const key in process.env) { + if (!key.startsWith('USER')) { continue; } + if (key.substring(5) == req.body.user && + process.env[key] == password) { + validLogin = true; + break; + } + } + if (validLogin) { + const token = createToken(req, res); + res.json({ "token": token }); + } else { + res.redirect("/read/login"); + } + }); }); function isLoggedIn(req: Request, res: Response) { + console.log("login check"); const result = validateToken(req, res); if (!result) { - return res.redirect("/read/login"); + loginLimiter(req, res, () => { + res.redirect("/read/login"); + }); } } - function validateToken(req: Request, res: Response) { const key = process.env.KEYB; const header = req.header('Authorization'); @@ -85,7 +98,6 @@ function validateToken(req: Request, res: Response) { } } - function createToken(req: Request, res: Response) { const key = process.env.KEYB; if (!key) { throw new Error('KEYA is not defined in the environment variables'); } @@ -95,6 +107,7 @@ function createToken(req: Request, res: Response) { }; const token = jwt.sign(payload, key, { expiresIn: 60 * 1 }); res.locals.token = token; + return token; } export default router; \ No newline at end of file diff --git a/src/controller/write.ts b/src/controller/write.ts index 7b405ff..28aa439 100644 --- a/src/controller/write.ts +++ b/src/controller/write.ts @@ -43,7 +43,7 @@ async function writeData(req: Request, res: Response, next: NextFunction) { const router = express.Router(); - router.get('/', baseSlowDown, entry.validate, errorChecking, writeData); +router.get('/', baseSlowDown, entry.validate, errorChecking, writeData); router.head('/', baseSlowDown, entry.validate, errorChecking); export default router; \ No newline at end of file diff --git a/src/middleware/limit.ts b/src/middleware/limit.ts index 37100f8..5bdb109 100644 --- a/src/middleware/limit.ts +++ b/src/middleware/limit.ts @@ -8,7 +8,7 @@ import logger from '@src/scripts/logger'; */ const baseOptions: Partial = { windowMs: 30 * 60 * 1000, - skip: (req, res) => (res.locals.ip == "127.0.0.1" || res.locals.ip == "::1") + //skip: (req, res) => (res.locals.ip == "127.0.0.1" || res.locals.ip == "::1") } const baseSlowDownOptions: Partial = { @@ -22,6 +22,14 @@ const baseRateLimitOptions: Partial = { limit: 10, // Limit each IP per window standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers + handler: function rateHandler(req: Request, res: Response, next: NextFunction, options: rateLimiterOptions) { + if (!Object.prototype.hasOwnProperty.call(ipsThatReachedLimit, res.locals.ip)) { + logger.error(`[RateLimit] reached ${req.originalUrl}, ${res.locals.ip}, ${req.get('User-Agent')}`); + ipsThatReachedLimit[res.locals.ip] = { limitReachedOnError: true, time: Date.now() }; + } + res.status(options.statusCode).send(options.message); + } + } @@ -44,14 +52,21 @@ setInterval(() => { */ export const baseSlowDown = slowDown(baseSlowDownOptions); +export const loginSlowDown = slowDown({ + ...baseSlowDownOptions, + delayAfter: 1, // no delay for amount of attempts + delayMs: (used: number) => (used - 1) * 250, // Add delay after delayAfter is reached + }); + + export const errorRateLimiter = rateLimit({ ...baseRateLimitOptions, message: 'Too many requests with errors', - handler: (req: Request, res: Response, next: NextFunction, options: rateLimiterOptions) => { - if (!Object.prototype.hasOwnProperty.call(ipsThatReachedLimit, res.locals.ip)) { - logger.error(`[RateLimit] for invalid requests reached ${res.locals.ip}, ${req.get('User-Agent')}`); - ipsThatReachedLimit[res.locals.ip] = { limitReachedOnError: true, time: Date.now() }; - } - res.status(options.statusCode).send(options.message); - } +}); + +export const loginLimiter = rateLimit({ + ...baseRateLimitOptions, + windowMs: 3 * 60 * 1000, + limit: 3, + message: 'Too many failed login attempts', }); \ No newline at end of file