diff --git a/packages/backend-app-api/config.d.ts b/packages/backend-app-api/config.d.ts index 137615a152..2371623b82 100644 --- a/packages/backend-app-api/config.d.ts +++ b/packages/backend-app-api/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { backend?: { auth?: { @@ -32,6 +34,16 @@ 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; + }; }; }; diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 6f69c6551f..406ce80e4b 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -64,6 +64,7 @@ "cors": "^2.8.5", "express": "^4.17.1", "express-promise-router": "^4.1.0", + "express-rate-limit": "^7.2.0", "fs-extra": "^11.2.0", "helmet": "^6.0.0", "jose": "^5.0.0", 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 a69fa5a804..6e5e06c59e 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.ts @@ -19,8 +19,11 @@ import { HttpRouterServiceAuthPolicy, RootConfigService, } from '@backstage/backend-plugin-api'; +import { durationToMilliseconds } from '@backstage/types'; import { RequestHandler } from 'express'; import { pathToRegexp } from 'path-to-regexp'; +import { rateLimit } from 'express-rate-limit'; +import { readDurationFromConfig } from '@backstage/config'; export function createPathPolicyPredicate(policyPath: string) { if (policyPath === '/' || policyPath === '*') { @@ -59,13 +62,32 @@ export function createCredentialsBarrier(options: { const unauthenticatedPredicates = new Array<(path: string) => boolean>(); const cookiePredicates = new Array<(path: string) => boolean>(); - const middleware: RequestHandler = (req, _, next) => { + // Default rate limit is 100 requests per 15 minutes + const max = config?.has('backend.auth.rateLimit.max') + ? config.getNumber('backend.auth.rateLimit.max') + : 100; + + const duration = config?.has('backend.auth.rateLimit.window') + ? readDurationFromConfig(config.getConfig('backend.auth.rateLimit.window')) + : undefined; + + // Default rate limit window is 15 minutes + const windowMs = duration ? durationToMilliseconds(duration) : 15 * 60 * 1000; + + const limiter = rateLimit({ + windowMs, + max, + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers + }); + + const middleware: RequestHandler = (req, res, next) => { const allowsUnauthenticated = unauthenticatedPredicates.some(predicate => predicate(req.path), ); if (allowsUnauthenticated) { - next(); + limiter(req, res, next); return; } diff --git a/yarn.lock b/yarn.lock index a0d94a3027..081bbeb40c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3248,6 +3248,7 @@ __metadata: cors: ^2.8.5 express: ^4.17.1 express-promise-router: ^4.1.0 + express-rate-limit: ^7.2.0 fs-extra: ^11.2.0 helmet: ^6.0.0 http-errors: ^2.0.0 @@ -28044,6 +28045,15 @@ __metadata: languageName: node linkType: hard +"express-rate-limit@npm:^7.2.0": + version: 7.2.0 + resolution: "express-rate-limit@npm:7.2.0" + peerDependencies: + express: 4 || 5 || ^5.0.0-beta.1 + checksum: 6adc3e06d430e91cf8ef8da23107466ffd9193b5680b03bc12702894cab0adcbb980634602b839b3a1444620f2379707098af3a4dcc38c909e89214abaf6c1bf + languageName: node + linkType: hard + "express-session@npm:^1.17.1, express-session@npm:^1.17.3": version: 1.18.0 resolution: "express-session@npm:1.18.0"