diff --git a/packages/api/src/routers/auth/auth_router.ts b/packages/api/src/routers/auth/auth_router.ts index 3af801ae4..fa1cf763e 100644 --- a/packages/api/src/routers/auth/auth_router.ts +++ b/packages/api/src/routers/auth/auth_router.ts @@ -11,7 +11,6 @@ import axios from 'axios' import cors from 'cors' import type { Request, Response } from 'express' import express from 'express' -import rateLimit from 'express-rate-limit' import * as jwt from 'jsonwebtoken' import url from 'url' import { promisify } from 'util' @@ -36,6 +35,8 @@ import { } from '../../utils/auth' import { corsConfig } from '../../utils/corsConfig' import { logger } from '../../utils/logger' +import { hourlyLimiter } from '../../utils/rate_limit' +import { verifyChallengeRecaptcha } from '../../utils/recaptcha' import { createSsoToken, ssoRedirectURL } from '../../utils/sso' import { handleAppleWebAuth } from './apple_auth' import type { AuthProvider } from './auth_types' @@ -47,7 +48,6 @@ import { } from './google_auth' import { createWebAuthToken } from './jwt_helpers' import { createMobileAccountCreationResponse } from './mobile/account_creation' -import { verifyChallengeRecaptcha } from '../../utils/recaptcha' export interface SignupRequest { email: string @@ -91,15 +91,6 @@ export const isValidSignupRequest = (obj: any): obj is SignupRequest => { ) } -// The hourly limiter is used on the create account, -// and reset password endpoints -// this limits users to five operations per an hour -const hourlyLimiter = rateLimit({ - windowMs: 60 * 60 * 1000, - max: 5, - skip: (req) => env.dev.isLocal, -}) - export function authRouter() { const router = express.Router() diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts index e0ac429d2..473df7a79 100755 --- a/packages/api/src/server.ts +++ b/packages/api/src/server.ts @@ -10,10 +10,8 @@ import cookieParser from 'cookie-parser' import express, { Express } from 'express' import * as httpContext from 'express-http-context2' import promBundle from 'express-prom-bundle' -import rateLimit, { MemoryStore } from 'express-rate-limit' import { createServer, Server } from 'http' import * as prom from 'prom-client' -import { RedisStore } from 'rate-limit-redis' import { config, loggers } from 'winston' import { makeApolloServer } from './apollo' import { appDataSource } from './data_source' @@ -43,13 +41,9 @@ import { textToSpeechRouter } from './routers/text_to_speech' import { userRouter } from './routers/user_router' import { sentryConfig } from './sentry' import { analytics } from './utils/analytics' -import { - getClaimsByToken, - getTokenByRequest, - isSystemRequest, -} from './utils/auth' import { corsConfig } from './utils/corsConfig' import { buildLogger, buildLoggerTransport, logger } from './utils/logger' +import { apiLimiter, authLimiter } from './utils/rate_limit' const PORT = process.env.PORT || 4000 @@ -73,37 +67,6 @@ export const createApp = (): { // set to true if behind a reverse proxy/load balancer app.set('trust proxy', env.server.trustProxy) - // use the redis store if we have a redis connection - const redisClient = redisDataSource.redisClient - const store = redisClient - ? new RedisStore({ - // @ts-expect-error - Known issue: the `call` function is not present in @types/ioredis - sendCommand: (...args: string[]) => redisClient.call(...args), - }) - : new MemoryStore() - - const apiLimiter = rateLimit({ - windowMs: 60 * 1000, // 1 minute - max: async (req) => { - // 100 RPM for an authenticated request, 15 for a non-authenticated request - const token = getTokenByRequest(req) - try { - const claims = await getClaimsByToken(token) - return claims ? 60 : 15 - } catch (e) { - console.log('non-authenticated request') - return 15 - } - }, - keyGenerator: (req) => { - return getTokenByRequest(req) || req.ip - }, - // skip preflight requests and test requests and system requests - skip: (req) => - req.method === 'OPTIONS' || env.dev.isLocal || isSystemRequest(req), - store, - }) - // Apply the rate limiting middleware to API calls only app.use('/api/', apiLimiter) @@ -120,15 +83,6 @@ export const createApp = (): { // respond healthy to auto-scaler. app.get('/_ah/health', (req, res) => res.sendStatus(200)) - // 5 RPM for auth requests - const authLimiter = rateLimit({ - windowMs: 60 * 1000, // 1 minute - max: 5, - // skip preflight requests and test requests - skip: (req) => req.method === 'OPTIONS' || env.dev.isLocal, - store, - }) - app.use('/api/auth', authLimiter, authRouter()) app.use('/api/mobile-auth', authLimiter, mobileAuthRouter()) app.use('/api/page', pageRouter()) diff --git a/packages/api/src/utils/rate_limit.ts b/packages/api/src/utils/rate_limit.ts new file mode 100644 index 000000000..340e60530 --- /dev/null +++ b/packages/api/src/utils/rate_limit.ts @@ -0,0 +1,52 @@ +import rateLimit, { MemoryStore, Options } from 'express-rate-limit' +import { RedisStore } from 'rate-limit-redis' +import { env } from '../env' +import { redisDataSource } from '../redis_data_source' +import { getClaimsByToken, getTokenByRequest, isSystemRequest } from './auth' + +// use the redis store if we have a redis connection +const redisClient = redisDataSource.redisClient +const store = redisClient + ? new RedisStore({ + // @ts-expect-error - Known issue: the `call` function is not present in @types/ioredis + sendCommand: (...args: string[]) => redisClient.call(...args), + }) + : new MemoryStore() + +const configs: Partial = { + windowMs: 60 * 1000, // 1 minute + max: 5, + // skip preflight requests and test requests and system requests + skip: (req) => + req.method === 'OPTIONS' || env.dev.isLocal || isSystemRequest(req), + store, +} + +export const apiLimiter = rateLimit({ + ...configs, + max: async (req) => { + // 100 RPM for an authenticated request, 15 for a non-authenticated request + const token = getTokenByRequest(req) + try { + const claims = await getClaimsByToken(token) + return claims ? 60 : 15 + } catch (e) { + console.log('non-authenticated request') + return 15 + } + }, + keyGenerator: (req) => { + return getTokenByRequest(req) || req.ip + }, +}) + +// 5 RPM for auth requests +export const authLimiter = rateLimit(configs) + +// The hourly limiter is used on the create account, +// and reset password endpoints +// this limits users to five operations per an hour +export const hourlyLimiter = rateLimit({ + ...configs, + windowMs: 60 * 60 * 1000, +})