Skip to content

Commit

Permalink
server: rate limiter v1
Browse files Browse the repository at this point in the history
  • Loading branch information
TBonnin committed Feb 21, 2024
1 parent d73f02c commit 733f8fc
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 15 deletions.
13 changes: 9 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

70 changes: 70 additions & 0 deletions packages/server/lib/controllers/ratelimit.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { Request, Response, NextFunction } from 'express';
import { createClient } from 'redis';
import { RateLimiterRedis, RateLimiterMemory, RateLimiterRes } from 'rate-limiter-flexible';
import { getAccount, getRedisUrl } from '@nangohq/shared';
import { logger } from '@nangohq/shared';

const rateLimiter = await (async () => {
const opts = {
keyPrefix: 'middleware',
points: 1200,
duration: 60,
blockDuration: 0
};
const url = getRedisUrl();
if (url) {
const redisClient = await createClient({ url: url, disableOfflineQueue: true }).connect();
redisClient.on('error', (err) => {
logger.error(`Redis (rate-limiter) error: ${err}`);
});
return new RateLimiterRedis({
storeClient: redisClient,
...opts
});
}
return new RateLimiterMemory(opts);
})();

export const rateLimiterMiddleware = (req: Request, res: Response, next: NextFunction) => {
const setXRateLimitHeaders = (rateLimiterRes: RateLimiterRes) => {
const resetEpoch = Math.floor(new Date(Date.now() + rateLimiterRes.msBeforeNext).getTime() / 1000);
res.setHeader('X-RateLimit-Limit', rateLimiter.points);
res.setHeader('X-RateLimit-Remaining', rateLimiterRes.remainingPoints);
res.setHeader('X-RateLimit-Reset', resetEpoch);
};
const key = getKey(req, res);
const pointsToConsume = getPointsToConsume(req);
rateLimiter
.consume(key, pointsToConsume)
.then((rateLimiterRes) => {
setXRateLimitHeaders(rateLimiterRes);
next();
})
.catch((rateLimiterRes) => {
res.setHeader('Retry-After', Math.floor(rateLimiterRes.msBeforeNext / 1000));
setXRateLimitHeaders(rateLimiterRes);
logger.info(`Rate limit exceeded for ${key}. Request: ${req.method} ${req.path})`);
next();
// TODO:
// res.status(429).send('Too Many Requests');
});
};

function getKey(req: Request, res: Response): string {
try {
return `account-${getAccount(res)}`;
} catch (e) {
if (req.user) {
return `user-${req.user.id}`;
}
return `ip-${req.ip}`;
}
}

function getPointsToConsume(req: Request): number {
if (['/api/v1/signin', '/api/v1/signup', '/api/v1/forgot-password', '/api/v1/reset-password'].includes(req.path)) {
// limiting to 6 requests per period to avoid brute force attacks
return rateLimiter.points / 6;
}
return 1;
}
24 changes: 13 additions & 11 deletions packages/server/lib/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import apiAuthController from './controllers/apiAuth.controller.js';
import appAuthController from './controllers/appAuth.controller.js';
import onboardingController from './controllers/onboarding.controller.js';
import webhookController from './controllers/webhook.controller.js';
import { rateLimiterMiddleware } from './controllers/ratelimit.middleware.js';
import path from 'path';
import { dirname } from './utils/utils.js';
import { WebSocketServer, WebSocket } from 'ws';
Expand Down Expand Up @@ -52,14 +53,15 @@ const app = express();

// Auth
AuthClient.setup(app);
const apiAuth = authMiddleware.secretKeyAuth.bind(authMiddleware);
const apiPublicAuth = authMiddleware.publicKeyAuth.bind(authMiddleware);

const apiAuth = [authMiddleware.secretKeyAuth, rateLimiterMiddleware];
const apiPublicAuth = [authMiddleware.publicKeyAuth, rateLimiterMiddleware];
const webAuth =
isCloud() || isEnterprise()
? [passport.authenticate('session'), authMiddleware.sessionAuth.bind(authMiddleware)]
? [passport.authenticate('session'), authMiddleware.sessionAuth, rateLimiterMiddleware]
: isBasicAuthEnabled()
? [passport.authenticate('basic', { session: false }), authMiddleware.basicAuth.bind(authMiddleware)]
: [authMiddleware.noAuth.bind(authMiddleware)];
? [passport.authenticate('basic', { session: false }), authMiddleware.basicAuth, rateLimiterMiddleware]
: [authMiddleware.noAuth, rateLimiterMiddleware];

app.use(
express.json({
Expand Down Expand Up @@ -139,12 +141,12 @@ app.route('/proxy/*').all(apiAuth, upload.any(), proxyController.routeCall.bind(

// Webapp routes (no auth).
if (isCloud() || isEnterprise()) {
app.route('/api/v1/signup').post(authController.signup.bind(authController));
app.route('/api/v1/signup/invite').get(authController.invitation.bind(authController));
app.route('/api/v1/logout').post(authController.logout.bind(authController));
app.route('/api/v1/signin').post(passport.authenticate('local'), authController.signin.bind(authController));
app.route('/api/v1/forgot-password').put(authController.forgotPassword.bind(authController));
app.route('/api/v1/reset-password').put(authController.resetPassword.bind(authController));
app.route('/api/v1/signup').post(rateLimiterMiddleware, authController.signup.bind(authController));
app.route('/api/v1/signup/invite').get(rateLimiterMiddleware, authController.invitation.bind(authController));
app.route('/api/v1/logout').post(rateLimiterMiddleware, authController.logout.bind(authController));
app.route('/api/v1/signin').post(rateLimiterMiddleware, passport.authenticate('local'), authController.signin.bind(authController));
app.route('/api/v1/forgot-password').put(rateLimiterMiddleware, authController.forgotPassword.bind(authController));
app.route('/api/v1/reset-password').put(rateLimiterMiddleware, authController.resetPassword.bind(authController));
}

// Webapp routes (session auth).
Expand Down
2 changes: 2 additions & 0 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
"passport-local": "^1.0.0",
"pg": "^8.8.0",
"posthog-node": "^3.1.3",
"rate-limiter-flexible": "^5.0.0",
"redis": "^4.6.11",
"simple-oauth2": "^5.0.0",
"uuid": "^9.0.0",
"winston": "^3.8.2",
Expand Down

0 comments on commit 733f8fc

Please sign in to comment.