introduce strict rate limitso on auth router endpoints

This commit is contained in:
miloschwartz 2025-07-14 18:00:41 -07:00
parent d6fdb38c22
commit b7df0b122d
No known key found for this signature in database
6 changed files with 236 additions and 99 deletions

View file

@ -6,13 +6,15 @@ import logger from "@server/logger";
import { import {
errorHandlerMiddleware, errorHandlerMiddleware,
notFoundMiddleware, notFoundMiddleware,
rateLimitMiddleware
} from "@server/middlewares"; } from "@server/middlewares";
import { authenticated, unauthenticated } from "@server/routers/external"; import { authenticated, unauthenticated } from "@server/routers/external";
import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws"; import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws";
import { logIncomingMiddleware } from "./middlewares/logIncoming"; import { logIncomingMiddleware } from "./middlewares/logIncoming";
import { csrfProtectionMiddleware } from "./middlewares/csrfProtection"; import { csrfProtectionMiddleware } from "./middlewares/csrfProtection";
import helmet from "helmet"; 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 dev = config.isDev;
const externalPort = config.getRawConfig().server.external_port; const externalPort = config.getRawConfig().server.external_port;
@ -56,11 +58,19 @@ export function createApiServer() {
if (!dev) { if (!dev) {
apiServer.use( apiServer.use(
rateLimitMiddleware({ rateLimit({
windowMin: windowMs:
config.getRawConfig().rate_limits.global.window_minutes, config.getRawConfig().rate_limits.global.window_minutes *
60 *
1000,
max: config.getRawConfig().rate_limits.global.max_requests, 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)
);
}
}) })
); );
} }

View file

@ -3,8 +3,6 @@ import yaml from "js-yaml";
import { configFilePath1, configFilePath2 } from "./consts"; import { configFilePath1, configFilePath2 } from "./consts";
import { z } from "zod"; import { z } from "zod";
import stoi from "./stoi"; 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); const portSchema = z.number().positive().gt(0).lte(65535);
@ -179,10 +177,21 @@ export const configSchema = z.object({
.default({}), .default({}),
auth: z auth: z
.object({ .object({
window_minutes: z.number().positive().gt(0), window_minutes: z
max_requests: z.number().positive().gt(0) .number()
.positive()
.gt(0)
.optional()
.default(1),
max_requests: z
.number()
.positive()
.gt(0)
.optional()
.default(500)
}) })
.optional() .optional()
.default({}),
}) })
.optional() .optional()
.default({}), .default({}),

View file

@ -1,5 +1,4 @@
export * from "./notFound"; export * from "./notFound";
export * from "./rateLimit";
export * from "./formatError"; export * from "./formatError";
export * from "./verifySession"; export * from "./verifySession";
export * from "./verifyUser"; export * from "./verifyUser";

View file

@ -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;

View file

@ -75,6 +75,14 @@ export async function verifyTotp(
) )
); );
user = res; user = res;
const validPassword = await verifyPassword(
password,
user.passwordHash!
);
if (!validPassword) {
return next(unauthorized());
}
} }
if (!user) { 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) { if (user.type !== UserType.Internal) {
return next( return next(
createHttpError( createHttpError(

View file

@ -16,7 +16,6 @@ import * as apiKeys from "./apiKeys";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { import {
verifyAccessTokenAccess, verifyAccessTokenAccess,
rateLimitMiddleware,
verifySessionMiddleware, verifySessionMiddleware,
verifySessionUserMiddleware, verifySessionUserMiddleware,
verifyOrgAccess, verifyOrgAccess,
@ -703,36 +702,139 @@ authenticated.get(
export const authRouter = Router(); export const authRouter = Router();
unauthenticated.use("/auth", authRouter); unauthenticated.use("/auth", authRouter);
authRouter.use( authRouter.use(
rateLimitMiddleware({ rateLimit({
windowMin: windowMs: config.getRawConfig().rate_limits.auth.window_minutes,
config.getRawConfig().rate_limits.auth?.window_minutes || max: config.getRawConfig().rate_limits.auth.max_requests,
config.getRawConfig().rate_limits.global.window_minutes, keyGenerator: (req) => `authRouterGlobal:${req.ip}:${req.path}`,
max: handler: (req, res, next) => {
config.getRawConfig().rate_limits.auth?.max_requests || 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).`;
config.getRawConfig().rate_limits.global.max_requests, return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
type: "IP_AND_PATH" }
}) })
); );
authRouter.put("/signup", auth.signup); authRouter.put(
authRouter.post("/login", auth.login); "/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("/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(
authRouter.post("/2fa/request", auth.requestTotpSecret); "/2fa/enable",
authRouter.post("/2fa/disable", verifySessionUserMiddleware, auth.disable2fa); rateLimit({
authRouter.post("/verify-email", verifySessionMiddleware, auth.verifyEmail); 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( authRouter.post(
"/verify-email/request", "/verify-email/request",
verifySessionMiddleware, verifySessionMiddleware,
rateLimit({ rateLimit({
windowMs: 15 * 60 * 1000, windowMs: 15 * 60 * 1000,
max: 3, max: 15,
keyGenerator: (req) => `requestEmailVerificationCode:${req.body.email}`, keyGenerator: (req) => `requestEmailVerificationCode:${req.body.email}`,
handler: (req, res, next) => { 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)); return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
} }
}), }),
@ -749,29 +851,68 @@ authRouter.post(
"/reset-password/request", "/reset-password/request",
rateLimit({ rateLimit({
windowMs: 15 * 60 * 1000, windowMs: 15 * 60 * 1000,
max: 3, max: 15,
keyGenerator: (req) => `requestPasswordReset:${req.body.email}`, keyGenerator: (req) => `requestPasswordReset:${req.body.email}`,
handler: (req, res, next) => { 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)); return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
} }
}), }),
auth.requestPasswordReset 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(
authRouter.post("/resource/:resourceId/pincode", resource.authWithPincode); "/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( authRouter.post(
"/resource/:resourceId/whitelist", "/resource/:resourceId/whitelist",
rateLimit({ rateLimit({
windowMs: 15 * 60 * 1000, windowMs: 15 * 60 * 1000,
max: 10, max: 15,
keyGenerator: (req) => `authWithWhitelist:${req.body.email}`, keyGenerator: (req) =>
`authWithWhitelist:${req.ip}:${req.body.email}:${req.params.resourceId}`,
handler: (req, res, next) => { 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)); return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
} }
}), }),
@ -799,21 +940,31 @@ authRouter.post(
rateLimit({ rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // Allow 5 security key registrations per 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) => { 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)); return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
} }
}), }),
auth.startRegistration auth.startRegistration
); );
authRouter.post("/security-key/register/verify", verifySessionUserMiddleware, auth.verifyRegistration); authRouter.post(
"/security-key/register/verify",
verifySessionUserMiddleware,
auth.verifyRegistration
);
authRouter.post( authRouter.post(
"/security-key/authenticate/start", "/security-key/authenticate/start",
rateLimit({ rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // Allow 10 authentication attempts per 15 minutes per IP 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) => { handler: (req, res, next) => {
const message = `You can only attempt security key authentication ${10} times every ${15} minutes. Please try again later.`; 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)); return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
@ -822,5 +973,22 @@ authRouter.post(
auth.startAuthentication auth.startAuthentication
); );
authRouter.post("/security-key/authenticate/verify", auth.verifyAuthentication); authRouter.post("/security-key/authenticate/verify", auth.verifyAuthentication);
authRouter.get("/security-key/list", verifySessionUserMiddleware, auth.listSecurityKeys); authRouter.get(
authRouter.delete("/security-key/:credentialId", verifySessionUserMiddleware, auth.deleteSecurityKey); "/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
);