Skip to content

Commit

Permalink
reduce rate limit env variable and set them by level in a config file
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianboros committed Oct 3, 2024
1 parent aec4c9f commit 4addec6
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 59 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ The `RAPYD_ACCESS_KEY` and `RAPYD_SECRET_KEY` variables values can be found in y

To create a new Interledger Test Wallet account, a verification email will be sent to the provided email address. If you want to send emails within the development environment, you will need to have a personal Sendgrid account and update the following environment variables: `SEND_EMAIL` to `true`, `SENDGRID_API_KEY` and `FROM_EMAIL`. If you prefer not to send emails in the development environment, simply set `SEND_EMAIL` to `false` and use the verification link found in the Docker `wallet-backend` container logs to finalize the registration process for a new user.

To enable rate limiter on the wallet for security purposes you can set these environment variables: `RATE_LIMIT` to `true` and `RATE_LIMIT_LEVEL`. `RATE_LIMIT_LEVEL` has three possible values: `LAX|NORMAL|STRICT`, default is `LAX`.

Cross-currency transactions are supported. To enable this functionality, you will need to register at [freecurrencyapi.com/](https://freecurrencyapi.com/) and update the `RATE_API_KEY` environment variable with your own API key.
Currencies can be added in the `admin` environment. For example `assetCode` is `EUR`, `assetScale` is `2`, and you will need to add an amount to `liquidity`.

Expand Down
14 changes: 2 additions & 12 deletions docker/dev/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,8 @@ GATEHUB_GATEWAY_UUID=
GATEHUB_VAULT_UUID_EUR=
GATEHUB_VAULT_UUID_USD=
GATEHUB_SETTLEMENT_WALLET_ADDRESS=
SEND_EMAIL_RATE_LIMIT=1
SEND_EMAIL_RATE_LIMIT_PAUSE_IN_SECONDS=3600
SEND_EMAIL_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS=3600
LOGIN_RATE_LIMIT=3
LOGIN_RATE_LIMIT_PAUSE_IN_SECONDS=600
LOGIN_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS=3600
LOGIN_IP_RATE_LIMIT=30
LOGIN_IP_RATE_LIMIT_PAUSE_IN_SECONDS=3600
LOGIN_IP_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS=3600
LOGIN_IP_BLOCK_RATE_LIMIT=500
LOGIN_IP_BLOCK_RATE_LIMIT_PAUSE_IN_SECONDS=86400
LOGIN_IP_BLOCK_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS=86400
RATE_LIMIT=
RATE_LIMIT_LEVEL=

# commerce env variables
# encoded base 64 private key
Expand Down
14 changes: 2 additions & 12 deletions docker/dev/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,8 @@ services:
GATEHUB_VAULT_UUID_EUR: ${GATEHUB_VAULT_UUID_EUR}
GATEHUB_VAULT_UUID_USD: ${GATEHUB_VAULT_UUID_USD}
GATEHUB_SETTLEMENT_WALLET_ADDRESS: ${GATEHUB_SETTLEMENT_WALLET_ADDRESS}
SEND_EMAIL_RATE_LIMIT: ${SEND_EMAIL_RATE_LIMIT}
SEND_EMAIL_RATE_LIMIT_PAUSE_IN_SECONDS: ${SEND_EMAIL_RATE_LIMIT_PAUSE_IN_SECONDS}
SEND_EMAIL_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS: ${SEND_EMAIL_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS}
LOGIN_RATE_LIMIT: ${LOGIN_RATE_LIMIT}
LOGIN_RATE_LIMIT_PAUSE_IN_SECONDS: ${LOGIN_RATE_LIMIT_PAUSE_IN_SECONDS}
LOGIN_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS: ${LOGIN_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS}
LOGIN_IP_RATE_LIMIT: ${LOGIN_IP_RATE_LIMIT}
LOGIN_IP_RATE_LIMIT_PAUSE_IN_SECONDS: ${LOGIN_IP_RATE_LIMIT_PAUSE_IN_SECONDS}
LOGIN_IP_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS: ${LOGIN_IP_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS}
LOGIN_IP_BLOCK_RATE_LIMIT: ${LOGIN_IP_BLOCK_RATE_LIMIT}
LOGIN_IP_BLOCK_RATE_LIMIT_PAUSE_IN_SECONDS: ${LOGIN_IP_BLOCK_RATE_LIMIT_PAUSE_IN_SECONDS}
LOGIN_IP_BLOCK_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS: ${LOGIN_IP_BLOCK_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS}
RATE_LIMIT: ${RATE_LIMIT}
RATE_LIMIT_LEVEL: ${RATE_LIMIT_LEVEL}
restart: always
networks:
- testnet
Expand Down
20 changes: 1 addition & 19 deletions packages/wallet/backend/src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,25 +34,7 @@ const envSchema = z.object({
SEND_EMAIL: z
.enum(['true', 'false'])
.default('false')
.transform((value) => value === 'true'),
SEND_EMAIL_RATE_LIMIT: z.coerce.number().default(1),
SEND_EMAIL_RATE_LIMIT_PAUSE_IN_SECONDS: z.coerce.number().default(3600),
SEND_EMAIL_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS: z.coerce
.number()
.default(3600),
LOGIN_RATE_LIMIT: z.coerce.number().default(3),
LOGIN_RATE_LIMIT_PAUSE_IN_SECONDS: z.coerce.number().default(600),
LOGIN_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS: z.coerce.number().default(3600),
LOGIN_IP_RATE_LIMIT: z.coerce.number().default(30),
LOGIN_IP_RATE_LIMIT_PAUSE_IN_SECONDS: z.coerce.number().default(3600),
LOGIN_IP_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS: z.coerce
.number()
.default(3600),
LOGIN_IP_BLOCK_RATE_LIMIT: z.coerce.number().default(500),
LOGIN_IP_BLOCK_RATE_LIMIT_PAUSE_IN_SECONDS: z.coerce.number().default(86400),
LOGIN_IP_BLOCK_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS: z.coerce
.number()
.default(86400)
.transform((value) => value === 'true')
})

export type Env = z.infer<typeof envSchema>
Expand Down
81 changes: 81 additions & 0 deletions packages/wallet/backend/src/config/rateLimit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { z } from 'zod'

export const envRateLimit = () => {
const rateLimitSchema = z
.object({
RATE_LIMIT: z
.enum(['true', 'false', ''])
.default('false')
.transform((value) => value === 'true'),
RATE_LIMIT_LEVEL: z.enum(['STRICT', 'NORMAL', 'LAX', '']).default('LAX'),
SEND_EMAIL_RATE_LIMIT: z.coerce.number().default(1),
SEND_EMAIL_RATE_LIMIT_PAUSE_IN_SECONDS: z.coerce.number().default(1800),
SEND_EMAIL_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS: z.coerce
.number()
.default(1800),
LOGIN_RATE_LIMIT: z.coerce.number().default(6),
LOGIN_RATE_LIMIT_PAUSE_IN_SECONDS: z.coerce.number().default(300),
LOGIN_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS: z.coerce
.number()
.default(1800),
LOGIN_IP_RATE_LIMIT: z.coerce.number().default(30),
LOGIN_IP_RATE_LIMIT_PAUSE_IN_SECONDS: z.coerce.number().default(1800),
LOGIN_IP_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS: z.coerce
.number()
.default(1800),
LOGIN_IP_BLOCK_RATE_LIMIT: z.coerce.number().default(1500),
LOGIN_IP_BLOCK_RATE_LIMIT_PAUSE_IN_SECONDS: z.coerce
.number()
.default(86400),
LOGIN_IP_BLOCK_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS: z.coerce
.number()
.default(86400)
})
.transform((data) => {
switch (data.RATE_LIMIT_LEVEL) {
case 'NORMAL':
return {
...data,
SEND_EMAIL_RATE_LIMIT: 1,
SEND_EMAIL_RATE_LIMIT_PAUSE_IN_SECONDS: 3600,
SEND_EMAIL_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS: 3600,
LOGIN_RATE_LIMIT: 3,
LOGIN_RATE_LIMIT_PAUSE_IN_SECONDS: 600,
LOGIN_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS: 3600,
LOGIN_IP_RATE_LIMIT: 30,
LOGIN_IP_RATE_LIMIT_PAUSE_IN_SECONDS: 3600,
LOGIN_IP_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS: 3600,
LOGIN_IP_BLOCK_RATE_LIMIT: 500,
LOGIN_IP_BLOCK_RATE_LIMIT_PAUSE_IN_SECONDS: 86400,
LOGIN_IP_BLOCK_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS: 86400
}
case 'STRICT':
return {
...data,
SEND_EMAIL_RATE_LIMIT: 1,
SEND_EMAIL_RATE_LIMIT_PAUSE_IN_SECONDS: 7200,
SEND_EMAIL_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS: 3600,
LOGIN_RATE_LIMIT: 3,
LOGIN_RATE_LIMIT_PAUSE_IN_SECONDS: 1800,
LOGIN_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS: 3600,
LOGIN_IP_RATE_LIMIT: 20,
LOGIN_IP_RATE_LIMIT_PAUSE_IN_SECONDS: 7200,
LOGIN_IP_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS: 3600,
LOGIN_IP_BLOCK_RATE_LIMIT: 250,
LOGIN_IP_BLOCK_RATE_LIMIT_PAUSE_IN_SECONDS: 86400,
LOGIN_IP_BLOCK_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS: 86400
}
}
return data
})

const result = rateLimitSchema.safeParse(process.env)
if (!result.success) {
console.error(
'Error parsing rate limit environment variables:',
result.error.flatten()
)
process.exit(1)
}
return result.data
}
37 changes: 24 additions & 13 deletions packages/wallet/backend/src/middleware/rateLimit.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
import { env } from '@/config/env'
import { envRateLimit } from '@/config/rateLimit'
import { RateLimiterRedisHelper } from '@/rateLimit/service'
import { NextFunction, Request, Response } from 'express'
import { getRedisClient } from '@/config/redis'

const rateLimit = envRateLimit()

export const rateLimiterEmail = async (
req: Request,
_res: Response,
next: NextFunction
): Promise<void> => {
if (!rateLimit.RATE_LIMIT) {
next()
return
}
try {
const sendEmailLimiter = new RateLimiterRedisHelper({
storeClient: getRedisClient(env),
keyPrefix: 'send_email',
points: env.SEND_EMAIL_RATE_LIMIT,
duration: env.SEND_EMAIL_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS,
blockDuration: env.SEND_EMAIL_RATE_LIMIT_PAUSE_IN_SECONDS
points: rateLimit.SEND_EMAIL_RATE_LIMIT,
duration: rateLimit.SEND_EMAIL_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS,
blockDuration: rateLimit.SEND_EMAIL_RATE_LIMIT_PAUSE_IN_SECONDS
})
await sendEmailLimiter.checkAttempts(req.body.email)
await sendEmailLimiter.useAttempt(req.body.email)
} catch (e) {
next(e)
}

next()
}
export const rateLimiterLogin = async (
Expand All @@ -30,27 +36,32 @@ export const rateLimiterLogin = async (
next: NextFunction
): Promise<void> => {
try {
if (!rateLimit.RATE_LIMIT) {
next()
return
}

const userIp = `${req.ip}`
const loginAttemptLimiter = new RateLimiterRedisHelper({
storeClient: getRedisClient(env),
keyPrefix: 'login_email',
points: env.LOGIN_RATE_LIMIT,
duration: env.LOGIN_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS,
blockDuration: env.LOGIN_RATE_LIMIT_PAUSE_IN_SECONDS
points: rateLimit.LOGIN_RATE_LIMIT,
duration: rateLimit.LOGIN_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS,
blockDuration: rateLimit.LOGIN_RATE_LIMIT_PAUSE_IN_SECONDS
})
const loginIPLimiter = new RateLimiterRedisHelper({
storeClient: getRedisClient(env),
keyPrefix: 'login_ip',
points: env.LOGIN_IP_RATE_LIMIT,
duration: env.LOGIN_IP_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS,
blockDuration: env.LOGIN_IP_RATE_LIMIT_PAUSE_IN_SECONDS
points: rateLimit.LOGIN_IP_RATE_LIMIT,
duration: rateLimit.LOGIN_IP_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS,
blockDuration: rateLimit.LOGIN_IP_RATE_LIMIT_PAUSE_IN_SECONDS
})
const loginBlockIPLimiter = new RateLimiterRedisHelper({
storeClient: getRedisClient(env),
keyPrefix: 'login_block_ip',
points: env.LOGIN_IP_BLOCK_RATE_LIMIT,
duration: env.LOGIN_IP_BLOCK_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS,
blockDuration: env.LOGIN_IP_BLOCK_RATE_LIMIT_PAUSE_IN_SECONDS
points: rateLimit.LOGIN_IP_BLOCK_RATE_LIMIT,
duration: rateLimit.LOGIN_IP_BLOCK_RATE_LIMIT_RESET_INTERVAL_IN_SECONDS,
blockDuration: rateLimit.LOGIN_IP_BLOCK_RATE_LIMIT_PAUSE_IN_SECONDS
})

await loginBlockIPLimiter.checkAttempts(userIp)
Expand Down
14 changes: 11 additions & 3 deletions packages/wallet/backend/tests/auth/controller.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Cradle, createContainer } from '@/createContainer'
import { env } from '@/config/env'
import { envRateLimit } from '@/config/rateLimit'
import { createApp, TestApp } from '@/tests/app'
import { Knex } from 'knex'
import { truncateTables } from '@shared/backend/tests'
Expand Down Expand Up @@ -39,6 +40,7 @@ describe('Authentication Controller', (): void => {
let req: MockRequest<Request>
let res: MockResponse<Response>

const rateLimit = envRateLimit()
const next = jest.fn()

beforeAll(async (): Promise<void> => {
Expand Down Expand Up @@ -217,6 +219,9 @@ describe('Authentication Controller', (): void => {
})
})
it('should return status 429 (rate limit) if the user login rate limit is reached', async (): Promise<void> => {
if (!rateLimit.RATE_LIMIT) {
return
}
const fakeLogin = fakeLoginData()
const newUserData = {
...fakeLogin,
Expand All @@ -226,7 +231,7 @@ describe('Authentication Controller', (): void => {
req.body = { ...fakeLogin, password: 'invalid' }

let failedLoginResp: MockResponse<Response>
for (let i = 0; i < env.LOGIN_RATE_LIMIT; i++) {
for (let i = 0; i < rateLimit.LOGIN_RATE_LIMIT; i++) {
failedLoginResp = createResponse()
await applyMiddleware(rateLimiterLogin, req, failedLoginResp)
await authController.logIn(req, failedLoginResp, (err) => {
Expand All @@ -243,7 +248,7 @@ describe('Authentication Controller', (): void => {
} catch (err) {
expect((err as BaseError).statusCode).toBe(429)
expect((err as BaseError).message).toMatch(
`Too many attempts. Retry after ${env.LOGIN_RATE_LIMIT_PAUSE_IN_SECONDS / 60} minutes`
`Too many attempts. Retry after ${rateLimit.LOGIN_RATE_LIMIT_PAUSE_IN_SECONDS / 60} minutes`
)
}
})
Expand Down Expand Up @@ -379,6 +384,9 @@ describe('Authentication Controller', (): void => {
})

it('should return 429 (rate limit) if the endpoint to resend verify email is called twice ', async () => {
if (!rateLimit.RATE_LIMIT) {
return
}
const fakeLogin = fakeLoginData()
const newUserData = {
...fakeLogin,
Expand All @@ -404,7 +412,7 @@ describe('Authentication Controller', (): void => {
} catch (err) {
expect((err as BaseError).statusCode).toBe(429)
expect((err as BaseError).message).toMatch(
`Too many attempts. Retry after ${env.SEND_EMAIL_RATE_LIMIT_PAUSE_IN_SECONDS / 60} minutes`
`Too many attempts. Retry after ${rateLimit.SEND_EMAIL_RATE_LIMIT_PAUSE_IN_SECONDS / 60} minutes`
)
}
})
Expand Down

0 comments on commit 4addec6

Please sign in to comment.