diff --git a/packages/backend-app-api/config.d.ts b/packages/backend-app-api/config.d.ts index 2371623b82..3625e9323f 100644 --- a/packages/backend-app-api/config.d.ts +++ b/packages/backend-app-api/config.d.ts @@ -34,16 +34,20 @@ export interface Config { * unless you configure credentials for service calls. */ dangerouslyDisableDefaultAuthPolicy?: boolean; - rateLimit?: { - /** - * Limit each IP to max requests per window - */ - max?: number; - /** - * The duration for which the rate limit is enforced - */ - window?: HumanDuration; - }; + rateLimit?: + | false + | { + /** + * Limit each IP to max requests per window + */ + max?: number; + /** + * The duration for which the rate limit is enforced + */ + window?: HumanDuration; + + disable?: true; + }; }; }; diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.test.ts b/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.test.ts index e28179e1ad..94109ccfc0 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.test.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.test.ts @@ -219,6 +219,8 @@ it('do not limit authenticated requests', async () => { .send() .expect(200); } + + jest.useRealTimers(); }); it('limit the number of unauthenticated requests', async () => { @@ -302,3 +304,62 @@ it('limit the number of unauthenticated requests', async () => { jest.useRealTimers(); }); + +it('skip limiting requests when the rate limit is disabled', async () => { + const now = Date.now(); + jest.useFakeTimers({ now }); + const max = 2; + const cacheMock = mockServices.cache.mock(); + const twoMinutesInMilliseconds = 2 * 60 * 1000; + const resetTime = now + twoMinutesInMilliseconds; + + // Simulate that the totalHits exceeds the configured max value + cacheMock.get.mockResolvedValue({ totalHits: max + 1, resetTime }); + + const { app: app1, barrier: barrier1 } = setup({ + cache: cacheMock, + config: { + backend: { + auth: { + rateLimit: { + max, + window: { minutes: 2 }, + disabled: true, + }, + }, + }, + }, + }); + + barrier1.addAuthPolicy({ allow: 'unauthenticated', path: '/public' }); + + // exceed the rate limit for authenticated requests + for (let i = 0; i < max + 1; i += 1) { + await request(app1) + .get('/public') + .set('authorization', mockCredentials.user.header()) + .send() + .expect(200); + } + + // Simulate that the totalHits exceeds the default max value + cacheMock.get.mockResolvedValue({ totalHits: 61, resetTime }); + + const { app: app2, barrier: barrier2 } = setup({ + cache: cacheMock, + config: { + backend: { + auth: { + rateLimit: false, + }, + }, + }, + }); + + barrier2.addAuthPolicy({ allow: 'unauthenticated', path: '/public' }); + + // exceed the rate limit for authenticated requests + await request(app2).get('/public').send().expect(200); + + jest.useRealTimers(); +}); diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.ts b/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.ts index 16e0203155..aca7838804 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.ts @@ -65,24 +65,39 @@ export function createCredentialsBarrier(options: { const unauthenticatedPredicates = new Array<(path: string) => boolean>(); const cookiePredicates = new Array<(path: string) => boolean>(); - // Default rate limit is 60 requests per 1 minute - const max = config?.has('backend.auth.rateLimit.max') - ? config.getNumber('backend.auth.rateLimit.max') - : 60; + const rateLimitConfig = config.getOptional('backend.auth.rateLimit'); - const duration = config?.has('backend.auth.rateLimit.window') - ? readDurationFromConfig(config.getConfig('backend.auth.rateLimit.window')) - : undefined; + const disabled = + rateLimitConfig === false || + (typeof rateLimitConfig === 'object' && + config?.getOptionalBoolean('backend.auth.rateLimit.disabled') === true); + + const duration = + typeof rateLimitConfig === 'object' && + config?.has('backend.auth.rateLimit.window') + ? readDurationFromConfig( + config.getConfig('backend.auth.rateLimit.window'), + ) + : undefined; - // Default rate limit window is 1 minute const windowMs = duration ? durationToMilliseconds(duration) : 1 * 60 * 1000; + const max = + typeof rateLimitConfig === 'object' && + config?.has('backend.auth.rateLimit.max') + ? config.getNumber('backend.auth.rateLimit.max') + : 60; + + // Default rate limit is 60 requests per 1 minute const limiter = rateLimit({ windowMs, - max, + limit: max, standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers, store: RateLimitStore.fromOptions({ cache }), + skip() { + return disabled; + }, }); const middleware: RequestHandler = (req, res, next) => {