diff --git a/server/apiServer.ts b/server/apiServer.ts index ace27e9b..75b23ea9 100644 --- a/server/apiServer.ts +++ b/server/apiServer.ts @@ -6,13 +6,15 @@ import logger from "@server/logger"; import { errorHandlerMiddleware, notFoundMiddleware, - rateLimitMiddleware } from "@server/middlewares"; import { authenticated, unauthenticated } from "@server/routers/external"; import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws"; import { logIncomingMiddleware } from "./middlewares/logIncoming"; import { csrfProtectionMiddleware } from "./middlewares/csrfProtection"; import helmet from "helmet"; +import rateLimit from "express-rate-limit"; +import createHttpError from "http-errors"; +import HttpCode from "./types/HttpCode"; const dev = config.isDev; const externalPort = config.getRawConfig().server.external_port; @@ -56,11 +58,19 @@ export function createApiServer() { if (!dev) { apiServer.use( - rateLimitMiddleware({ - windowMin: - config.getRawConfig().rate_limits.global.window_minutes, + rateLimit({ + windowMs: + config.getRawConfig().rate_limits.global.window_minutes * + 60 * + 1000, max: config.getRawConfig().rate_limits.global.max_requests, - type: "IP_AND_PATH" + keyGenerator: (req) => `apiServerGlobal:${req.ip}:${req.path}`, + handler: (req, res, next) => { + const message = `Rate limit exceeded. You can make ${config.getRawConfig().rate_limits.global.max_requests} requests every ${config.getRawConfig().rate_limits.global.window_minutes} minute(s).`; + return next( + createHttpError(HttpCode.TOO_MANY_REQUESTS, message) + ); + } }) ); } diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index 0058127f..e98695c5 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -3,8 +3,6 @@ import yaml from "js-yaml"; import { configFilePath1, configFilePath2 } from "./consts"; import { z } from "zod"; import stoi from "./stoi"; -import { passwordSchema } from "@server/auth/passwordSchema"; -import { fromError } from "zod-validation-error"; const portSchema = z.number().positive().gt(0).lte(65535); @@ -179,10 +177,21 @@ export const configSchema = z.object({ .default({}), auth: z .object({ - window_minutes: z.number().positive().gt(0), - max_requests: z.number().positive().gt(0) + window_minutes: z + .number() + .positive() + .gt(0) + .optional() + .default(1), + max_requests: z + .number() + .positive() + .gt(0) + .optional() + .default(500) }) .optional() + .default({}), }) .optional() .default({}), diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts index 03d6f3bb..01355f97 100644 --- a/server/middlewares/index.ts +++ b/server/middlewares/index.ts @@ -1,5 +1,4 @@ export * from "./notFound"; -export * from "./rateLimit"; export * from "./formatError"; export * from "./verifySession"; export * from "./verifyUser"; diff --git a/server/middlewares/rateLimit.ts b/server/middlewares/rateLimit.ts deleted file mode 100644 index 2098288f..00000000 --- a/server/middlewares/rateLimit.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { rateLimit } from "express-rate-limit"; -import createHttpError from "http-errors"; -import { NextFunction, Request, Response } from "express"; -import logger from "@server/logger"; -import HttpCode from "@server/types/HttpCode"; - -export function rateLimitMiddleware({ - windowMin, - max, - type, - skipCondition, -}: { - windowMin: number; - max: number; - type: "IP_ONLY" | "IP_AND_PATH"; - skipCondition?: (req: Request, res: Response) => boolean; -}) { - if (type === "IP_AND_PATH") { - return rateLimit({ - windowMs: windowMin * 60 * 1000, - max, - skip: skipCondition, - keyGenerator: (req: Request) => { - return `${req.ip}-${req.path}`; - }, - handler: (req: Request, res: Response, next: NextFunction) => { - const message = `Rate limit exceeded. You can make ${max} requests every ${windowMin} minute(s).`; - logger.warn( - `Rate limit exceeded for IP ${req.ip} on path ${req.path}`, - ); - return next( - createHttpError(HttpCode.TOO_MANY_REQUESTS, message), - ); - }, - }); - } - return rateLimit({ - windowMs: windowMin * 60 * 1000, - max, - skip: skipCondition, - handler: (req: Request, res: Response, next: NextFunction) => { - const message = `Rate limit exceeded. You can make ${max} requests every ${windowMin} minute(s).`; - logger.warn(`Rate limit exceeded for IP ${req.ip}`); - return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); - }, - }); -} - -export default rateLimitMiddleware; diff --git a/server/routers/auth/verifyTotp.ts b/server/routers/auth/verifyTotp.ts index 4aca24ae..6b45a93e 100644 --- a/server/routers/auth/verifyTotp.ts +++ b/server/routers/auth/verifyTotp.ts @@ -75,6 +75,14 @@ export async function verifyTotp( ) ); user = res; + + const validPassword = await verifyPassword( + password, + user.passwordHash! + ); + if (!validPassword) { + return next(unauthorized()); + } } if (!user) { @@ -91,14 +99,6 @@ export async function verifyTotp( ); } - const validPassword = await verifyPassword( - password, - user.passwordHash! - ); - if (!validPassword) { - return next(unauthorized()); - } - if (user.type !== UserType.Internal) { return next( createHttpError( diff --git a/server/routers/external.ts b/server/routers/external.ts index 1ebf0cfe..17794974 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -16,7 +16,6 @@ import * as apiKeys from "./apiKeys"; import HttpCode from "@server/types/HttpCode"; import { verifyAccessTokenAccess, - rateLimitMiddleware, verifySessionMiddleware, verifySessionUserMiddleware, verifyOrgAccess, @@ -703,36 +702,139 @@ authenticated.get( export const authRouter = Router(); unauthenticated.use("/auth", authRouter); authRouter.use( - rateLimitMiddleware({ - windowMin: - config.getRawConfig().rate_limits.auth?.window_minutes || - config.getRawConfig().rate_limits.global.window_minutes, - max: - config.getRawConfig().rate_limits.auth?.max_requests || - config.getRawConfig().rate_limits.global.max_requests, - type: "IP_AND_PATH" + rateLimit({ + windowMs: config.getRawConfig().rate_limits.auth.window_minutes, + max: config.getRawConfig().rate_limits.auth.max_requests, + keyGenerator: (req) => `authRouterGlobal:${req.ip}:${req.path}`, + handler: (req, res, next) => { + const message = `Rate limit exceeded. You can make ${config.getRawConfig().rate_limits.auth.max_requests} requests every ${config.getRawConfig().rate_limits.auth.window_minutes} minute(s).`; + return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); + } }) ); -authRouter.put("/signup", auth.signup); -authRouter.post("/login", auth.login); +authRouter.put( + "/signup", + rateLimit({ + windowMs: 15 * 60 * 1000, + max: 15, + keyGenerator: (req) => `signup:${req.ip}:${req.body.email}`, + handler: (req, res, next) => { + const message = `You can only sign up ${15} times every ${15} minutes. Please try again later.`; + return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); + } + }), + auth.signup +); +authRouter.post( + "/login", + rateLimit({ + windowMs: 15 * 60 * 1000, + max: 15, + keyGenerator: (req) => `login:${req.body.email}`, + handler: (req, res, next) => { + const message = `You can only log in ${15} times every ${15} minutes. Please try again later.`; + return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); + } + }), + auth.login +); authRouter.post("/logout", auth.logout); -authRouter.post("/newt/get-token", getToken); +authRouter.post( + "/newt/get-token", + rateLimit({ + windowMs: 15 * 60 * 1000, + max: 900, + keyGenerator: (req) => `newtGetToken:${req.body.newtId}`, + handler: (req, res, next) => { + const message = `You can only request a Newt token ${900} times every ${15} minutes. Please try again later.`; + return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); + } + }), + getToken +); -authRouter.post("/2fa/enable", auth.verifyTotp); -authRouter.post("/2fa/request", auth.requestTotpSecret); -authRouter.post("/2fa/disable", verifySessionUserMiddleware, auth.disable2fa); -authRouter.post("/verify-email", verifySessionMiddleware, auth.verifyEmail); +authRouter.post( + "/2fa/enable", + rateLimit({ + windowMs: 15 * 60 * 1000, + max: 15, + keyGenerator: (req) => { + // user is authenticated, so we can use their userId; + // otherwise, they provide the email + if (req.body.email) { + return `signup:${req.body.email}`; + } else { + return `signup:${req.user!.userId}`; + } + }, + handler: (req, res, next) => { + const message = `You can only enable 2FA ${15} times every ${15} minutes. Please try again later.`; + return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); + } + }), + auth.verifyTotp +); +authRouter.post( + "/2fa/request", + rateLimit({ + windowMs: 15 * 60 * 1000, + max: 15, + keyGenerator: (req) => { + // user is authenticated, so we can use their userId; + // otherwise, they provide the email + if (req.body.email) { + return `signup:${req.body.email}`; + } else { + return `signup:${req.user!.userId}`; + } + }, + handler: (req, res, next) => { + const message = `You can only request a 2FA code ${15} times every ${15} minutes. Please try again later.`; + return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); + } + }), + + auth.requestTotpSecret +); +authRouter.post( + "/2fa/disable", + verifySessionUserMiddleware, + rateLimit({ + windowMs: 15 * 60 * 1000, + max: 15, + keyGenerator: (req) => `signup:${req.user!.userId}`, + handler: (req, res, next) => { + const message = `You can only disable 2FA ${15} times every ${15} minutes. Please try again later.`; + return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); + } + }), + auth.disable2fa +); +authRouter.post( + "/verify-email", + rateLimit({ + windowMs: 15 * 60 * 1000, + max: 15, + keyGenerator: (req) => `signup:${req.body.email}`, + handler: (req, res, next) => { + const message = `You can only sign up ${15} times every ${15} minutes. Please try again later.`; + return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); + } + }), + verifySessionMiddleware, + auth.verifyEmail +); authRouter.post( "/verify-email/request", verifySessionMiddleware, rateLimit({ windowMs: 15 * 60 * 1000, - max: 3, + max: 15, keyGenerator: (req) => `requestEmailVerificationCode:${req.body.email}`, handler: (req, res, next) => { - const message = `You can only request an email verification code ${3} times every ${15} minutes. Please try again later.`; + const message = `You can only request an email verification code ${15} times every ${15} minutes. Please try again later.`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); } }), @@ -749,29 +851,68 @@ authRouter.post( "/reset-password/request", rateLimit({ windowMs: 15 * 60 * 1000, - max: 3, + max: 15, keyGenerator: (req) => `requestPasswordReset:${req.body.email}`, handler: (req, res, next) => { - const message = `You can only request a password reset ${3} times every ${15} minutes. Please try again later.`; + const message = `You can only request a password reset ${15} times every ${15} minutes. Please try again later.`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); } }), auth.requestPasswordReset ); -authRouter.post("/reset-password/", auth.resetPassword); +authRouter.post( + "/reset-password/", + rateLimit({ + windowMs: 15 * 60 * 1000, + max: 15, + keyGenerator: (req) => `resetPassword:${req.body.email}`, + handler: (req, res, next) => { + const message = `You can only request a password reset ${15} times every ${15} minutes. Please try again later.`; + return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); + } + }), + auth.resetPassword +); -authRouter.post("/resource/:resourceId/password", resource.authWithPassword); -authRouter.post("/resource/:resourceId/pincode", resource.authWithPincode); +authRouter.post( + "/resource/:resourceId/password", + rateLimit({ + windowMs: 15 * 60 * 1000, + max: 15, + keyGenerator: (req) => + `authWithPassword:${req.ip}:${req.params.resourceId}`, + handler: (req, res, next) => { + const message = `You can only authenticate with password ${15} times every ${15} minutes. Please try again later.`; + return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); + } + }), + resource.authWithPassword +); +authRouter.post( + "/resource/:resourceId/pincode", + rateLimit({ + windowMs: 15 * 60 * 1000, + max: 15, + keyGenerator: (req) => + `authWithPincode:${req.ip}:${req.params.resourceId}`, + handler: (req, res, next) => { + const message = `You can only authenticate with pincode ${15} times every ${15} minutes. Please try again later.`; + return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); + } + }), + resource.authWithPincode +); authRouter.post( "/resource/:resourceId/whitelist", rateLimit({ windowMs: 15 * 60 * 1000, - max: 10, - keyGenerator: (req) => `authWithWhitelist:${req.body.email}`, + max: 15, + keyGenerator: (req) => + `authWithWhitelist:${req.ip}:${req.body.email}:${req.params.resourceId}`, handler: (req, res, next) => { - const message = `You can only request an email OTP ${10} times every ${15} minutes. Please try again later.`; + const message = `You can only request an email OTP ${15} times every ${15} minutes. Please try again later.`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); } }), @@ -799,21 +940,31 @@ authRouter.post( rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // Allow 5 security key registrations per 15 minutes - keyGenerator: (req) => `securityKeyRegister:${req.user?.userId}`, + keyGenerator: (req) => `securityKeyRegister:${req.user!.userId}`, handler: (req, res, next) => { - const message = `You can only register ${5} security keys every ${15} minutes. Please try again later.`; + const message = `You can only register a security key ${5} times every ${15} minutes. Please try again later.`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); } }), auth.startRegistration ); -authRouter.post("/security-key/register/verify", verifySessionUserMiddleware, auth.verifyRegistration); +authRouter.post( + "/security-key/register/verify", + verifySessionUserMiddleware, + auth.verifyRegistration +); authRouter.post( "/security-key/authenticate/start", rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 10, // Allow 10 authentication attempts per 15 minutes per IP - keyGenerator: (req) => `securityKeyAuth:${req.ip}`, + keyGenerator: (req) => { + if (req.body.email) { + return `securityKeyAuth:${req.body.email}`; + } else { + return `securityKeyAuth:${req.ip}`; + } + }, handler: (req, res, next) => { const message = `You can only attempt security key authentication ${10} times every ${15} minutes. Please try again later.`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); @@ -822,5 +973,22 @@ authRouter.post( auth.startAuthentication ); authRouter.post("/security-key/authenticate/verify", auth.verifyAuthentication); -authRouter.get("/security-key/list", verifySessionUserMiddleware, auth.listSecurityKeys); -authRouter.delete("/security-key/:credentialId", verifySessionUserMiddleware, auth.deleteSecurityKey); +authRouter.get( + "/security-key/list", + verifySessionUserMiddleware, + auth.listSecurityKeys +); +authRouter.delete( + "/security-key/:credentialId", + verifySessionUserMiddleware, + rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 20, // Allow 10 authentication attempts per 15 minutes per IP + keyGenerator: (req) => `securityKeyAuth:${req.user!.userId}`, + handler: (req, res, next) => { + const message = `You can only delete a security key ${10} times every ${15} minutes. Please try again later.`; + return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); + } + }), + auth.deleteSecurityKey +);