mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(core): Allow customizing rate limits on a per-route basis, and add rate limiting to more endpoints (#9522)
Co-authored-by: Omar Ajoue <krynble@gmail.com>
This commit is contained in:
parent
cda1e3f6e5
commit
7be616e583
|
@ -39,7 +39,7 @@ export class AuthController {
|
|||
) {}
|
||||
|
||||
/** Log in a user */
|
||||
@Post('/login', { skipAuth: true, rateLimit: true })
|
||||
@Post('/login', { skipAuth: true, rateLimit: {} })
|
||||
async login(req: LoginRequest, res: Response): Promise<PublicUser | undefined> {
|
||||
const { email, password, mfaToken, mfaRecoveryCode } = req.body;
|
||||
if (!email) throw new ApplicationError('Email is required to log in');
|
||||
|
|
|
@ -37,7 +37,7 @@ export class InvitationController {
|
|||
* Send email invite(s) to one or multiple users and create user shell(s).
|
||||
*/
|
||||
|
||||
@Post('/')
|
||||
@Post('/', { rateLimit: { limit: 10 } })
|
||||
@GlobalScope('user:create')
|
||||
async inviteUser(req: UserRequest.Invite) {
|
||||
const isWithinUsersLimit = this.license.isWithinUsersLimit();
|
||||
|
|
|
@ -41,7 +41,7 @@ export class PasswordResetController {
|
|||
/**
|
||||
* Send a password reset email.
|
||||
*/
|
||||
@Post('/forgot-password', { skipAuth: true, rateLimit: true })
|
||||
@Post('/forgot-password', { skipAuth: true, rateLimit: { limit: 3 } })
|
||||
async forgotPassword(req: PasswordResetRequest.Email) {
|
||||
if (!this.mailer.isEmailSetUp) {
|
||||
this.logger.debug(
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import type { RequestHandler } from 'express';
|
||||
import { CONTROLLER_ROUTES } from './constants';
|
||||
import type { Method, RouteMetadata } from './types';
|
||||
import type { Method, RateLimit, RouteMetadata } from './types';
|
||||
|
||||
interface RouteOptions {
|
||||
middlewares?: RequestHandler[];
|
||||
usesTemplates?: boolean;
|
||||
/** When this flag is set to true, auth cookie isn't validated, and req.user will not be set */
|
||||
skipAuth?: boolean;
|
||||
/** When this flag is set to true, calls to this endpoint is rate limited to a max of 5 over a window of 5 minutes **/
|
||||
rateLimit?: boolean;
|
||||
/** When these options are set, calls to this endpoint are rate limited using the options */
|
||||
rateLimit?: RateLimit;
|
||||
}
|
||||
|
||||
const RouteFactory =
|
||||
|
@ -25,7 +25,7 @@ const RouteFactory =
|
|||
handlerName: String(handlerName),
|
||||
usesTemplates: options.usesTemplates ?? false,
|
||||
skipAuth: options.skipAuth ?? false,
|
||||
rateLimit: options.rateLimit ?? false,
|
||||
rateLimit: options.rateLimit,
|
||||
});
|
||||
Reflect.defineMetadata(CONTROLLER_ROUTES, routes, controllerClass);
|
||||
};
|
||||
|
|
|
@ -8,7 +8,7 @@ import type { Class } from 'n8n-core';
|
|||
import { AuthService } from '@/auth/auth.service';
|
||||
import config from '@/config';
|
||||
import { UnauthenticatedError } from '@/errors/response-errors/unauthenticated.error';
|
||||
import { inE2ETests, inTest, RESPONSE_ERROR_MESSAGES } from '@/constants';
|
||||
import { inProduction, RESPONSE_ERROR_MESSAGES } from '@/constants';
|
||||
import type { BooleanLicenseFeature } from '@/Interfaces';
|
||||
import { License } from '@/License';
|
||||
import type { AuthenticatedRequest } from '@/requests';
|
||||
|
@ -24,16 +24,18 @@ import type {
|
|||
Controller,
|
||||
LicenseMetadata,
|
||||
MiddlewareMetadata,
|
||||
RateLimit,
|
||||
RouteMetadata,
|
||||
RouteScopeMetadata,
|
||||
} from './types';
|
||||
import { userHasScope } from '@/permissions/checkAccess';
|
||||
|
||||
const throttle = expressRateLimit({
|
||||
windowMs: 5 * 60 * 1000, // 5 minutes
|
||||
limit: 5, // Limit each IP to 5 requests per `window` (here, per 5 minutes).
|
||||
message: { message: 'Too many requests' },
|
||||
});
|
||||
const createRateLimitMiddleware = (rateLimit: RateLimit): RequestHandler =>
|
||||
expressRateLimit({
|
||||
windowMs: rateLimit.windowMs,
|
||||
limit: rateLimit.limit,
|
||||
message: { message: 'Too many requests' },
|
||||
});
|
||||
|
||||
export const createLicenseMiddleware =
|
||||
(features: BooleanLicenseFeature[]): RequestHandler =>
|
||||
|
@ -124,7 +126,7 @@ export const registerController = (app: Application, controllerClass: Class<obje
|
|||
await controller[handlerName](req, res);
|
||||
router[method](
|
||||
path,
|
||||
...(!inTest && !inE2ETests && rateLimit ? [throttle] : []),
|
||||
...(inProduction && rateLimit ? [createRateLimitMiddleware(rateLimit)] : []),
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
...(skipAuth ? [] : [authService.authMiddleware]),
|
||||
...(features ? [createLicenseMiddleware(features)] : []),
|
||||
|
|
|
@ -17,6 +17,19 @@ export interface MiddlewareMetadata {
|
|||
handlerName: string;
|
||||
}
|
||||
|
||||
export interface RateLimit {
|
||||
/**
|
||||
* The maximum number of requests to allow during the `window` before rate limiting the client.
|
||||
* @default 5
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* How long we should remember the requests.
|
||||
* @default 300_000 (5 minutes)
|
||||
*/
|
||||
windowMs?: number;
|
||||
}
|
||||
|
||||
export interface RouteMetadata {
|
||||
method: Method;
|
||||
path: string;
|
||||
|
@ -24,7 +37,7 @@ export interface RouteMetadata {
|
|||
middlewares: RequestHandler[];
|
||||
usesTemplates: boolean;
|
||||
skipAuth: boolean;
|
||||
rateLimit: boolean;
|
||||
rateLimit?: RateLimit;
|
||||
}
|
||||
|
||||
export type Controller = Record<
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
jest.mock('@/constants', () => ({
|
||||
inE2ETests: false,
|
||||
inTest: false,
|
||||
inProduction: true,
|
||||
}));
|
||||
|
||||
import express from 'express';
|
||||
|
@ -14,7 +13,7 @@ describe('registerController', () => {
|
|||
@RestController('/test')
|
||||
class TestController {
|
||||
@Get('/unlimited', { skipAuth: true })
|
||||
@Get('/rate-limited', { skipAuth: true, rateLimit: true })
|
||||
@Get('/rate-limited', { skipAuth: true, rateLimit: {} })
|
||||
endpoint() {
|
||||
return { ok: true };
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue