mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-27 22:25:58 +02:00
introduce strict rate limitso on auth router endpoints
This commit is contained in:
parent
d6fdb38c22
commit
b7df0b122d
6 changed files with 236 additions and 99 deletions
|
@ -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)
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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({}),
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
export * from "./notFound";
|
||||
export * from "./rateLimit";
|
||||
export * from "./formatError";
|
||||
export * from "./verifySession";
|
||||
export * from "./verifyUser";
|
||||
|
|
|
@ -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;
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue