Merge pull request #3763 from omnivore-app/fix/rate-limit

use redis for hourly rate limiter
This commit is contained in:
Hongbo Wu 2024-04-01 13:57:47 +08:00 committed by GitHub
commit 093aca0fca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 62 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,59 @@
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 getStore = (prefix?: string) =>
redisClient
? new RedisStore({
sendCommand: (command: string, ...args: string[]) =>
redisClient.call(command, ...args) as never,
prefix,
})
: 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: getStore('rate-limit'),
}
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
},
store: getStore('api-rate-limit'),
})
// 5 RPM for auth requests
export const authLimiter = rateLimit({
...configs,
store: getStore('auth-rate-limit'),
})
// 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,
store: getStore('hourly-rate-limit'),
})