use redis for hourly rate limiter

This commit is contained in:
Hongbo Wu
2024-04-01 13:01:02 +08:00
parent 788460c144
commit a97542d4ba
3 changed files with 55 additions and 58 deletions

View File

@ -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()

View File

@ -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())

View File

@ -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<Options> = {
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,
})