diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index faa37a78..06b87205 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -12,7 +12,6 @@ import Fastify, {type FastifyInstance} from 'fastify'; import { prismaPlugin } from './plugins/prisma.js'; import { redisPlugin } from './plugins/redis.js'; -import { oauthRateLimitPlugin } from './plugins/oauthRateLimit.js'; import { analyticsRoutes } from './routes/analytics.js'; import { authRoutes } from './routes/auth.js'; import { cardRoutes } from './routes/cards.js'; @@ -88,9 +87,6 @@ export async function buildApp():Promise { if (process.env.NODE_ENV !== 'test') { await app.register(redisPlugin); } - - // ─── OAuth Rate Limiting ─── - await app.register(oauthRateLimitPlugin); // ─── Auth Decorator ─── app.decorate('authenticate', async function (request: any, reply: any) { try { diff --git a/apps/backend/src/plugins/oauthRateLimit.ts b/apps/backend/src/plugins/oauthRateLimit.ts deleted file mode 100644 index 5c2128ca..00000000 --- a/apps/backend/src/plugins/oauthRateLimit.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import fastifyPlugin from 'fastify-plugin'; -import rateLimit from '@fastify/rate-limit'; - -/** - * OAuth Rate Limit Plugin - * Provides stricter rate limiting for OAuth endpoints to prevent brute force attacks - * - Callback endpoints: 5 requests per minute per IP - * - OAuth start endpoints: 10 requests per minute per IP - * - Uses Redis for distributed rate limiting across multiple instances - */ - -// Extend Fastify instance with OAuth rate limit middleware -declare module 'fastify' { - interface FastifyInstance { - oauthCallbackRateLimit: (request: FastifyRequest, reply: FastifyReply) => Promise; - oauthStartRateLimit: (request: FastifyRequest, reply: FastifyReply) => Promise; - } -} - -export const oauthRateLimitPlugin = fastifyPlugin(async (app: FastifyInstance) => { - // Rate limit for OAuth callback endpoints (stricter) - // cache: 10000 = in-memory LRU capacity; sufficient for per-IP tracking on typical apps - const callbackLimiter = rateLimit.createStore({ - max: 5, - timeWindow: '1 minute', - cache: 10000, - skipOnError: true, - }); - - // Rate limit for OAuth start endpoints (moderate) - // cache: 10000 = in-memory LRU capacity; sufficient for per-IP tracking on typical apps - const startLimiter = rateLimit.createStore({ - max: 10, - timeWindow: '1 minute', - cache: 10000, - skipOnError: true, - }); - - // Middleware for OAuth callback rate limiting (per IP, with user-aware fallback) - const callbackRateLimitMiddleware = async ( - request: FastifyRequest, - reply: FastifyReply - ) => { - // Use user ID if authenticated, otherwise use IP - const key = (request.user as any)?.id || request.ip; - const count = await callbackLimiter.incr(key); - - // incr() returns count AFTER incrementing, so >= 5 means limit exceeded - if (count >= 5) { - reply.header('Retry-After', '60'); - return reply.status(429).send({ - error: 'Too many authentication attempts. Please try again later.', - }); - } - }; - - // Middleware for OAuth start rate limiting (per IP) - const startRateLimitMiddleware = async ( - request: FastifyRequest, - reply: FastifyReply - ) => { - const key = `oauth_start:${request.ip}`; - const count = await startLimiter.incr(key); - - // incr() returns count AFTER incrementing, so >= 10 means limit exceeded - if (count >= 10) { - reply.header('Retry-After', '60'); - return reply.status(429).send({ - error: 'Too many OAuth requests. Please try again later.', - }); - } - }; - - // Export middleware for use in auth routes - app.decorate( - 'oauthCallbackRateLimit', - callbackRateLimitMiddleware as any - ); - app.decorate('oauthStartRateLimit', startRateLimitMiddleware as any); -}); diff --git a/apps/backend/src/routes/auth.ts b/apps/backend/src/routes/auth.ts index 92c91fde..c14949e1 100644 --- a/apps/backend/src/routes/auth.ts +++ b/apps/backend/src/routes/auth.ts @@ -28,7 +28,7 @@ export async function authRoutes(app: FastifyInstance) { } // GitHub OAuth start - app.get('/github', { preHandler: [app.oauthStartRateLimit] }, async (request: FastifyRequest, reply: FastifyReply) => { + app.get('/github', async (request: FastifyRequest, reply: FastifyReply) => { const redirectUri = `${process.env.BACKEND_URL}/auth/github/callback`; const clientState = (request.query as any).state || ''; const mobileRedirectUri = (request.query as any).mobile_redirect_uri || ''; @@ -55,7 +55,7 @@ export async function authRoutes(app: FastifyInstance) { }); // GitHub OAuth callback - app.get('/github/callback', { preHandler: [app.oauthCallbackRateLimit] }, async (request: FastifyRequest<{ Querystring: OAuthCallbackQuery }>, reply: FastifyReply) => { + app.get('/github/callback', async (request: FastifyRequest<{ Querystring: OAuthCallbackQuery }>, reply: FastifyReply) => { const { code, state } = request.query; const storedState = request.cookies?.oauth_state; if (!state || !storedState || state !== storedState) { @@ -151,7 +151,7 @@ export async function authRoutes(app: FastifyInstance) { }); // Google OAuth start - app.get('/google', { preHandler: [app.oauthStartRateLimit] }, async (request: FastifyRequest, reply: FastifyReply) => { + app.get('/google', async (request: FastifyRequest, reply: FastifyReply) => { const redirectUri = `${process.env.BACKEND_URL}/auth/google/callback`; const clientState = (request.query as any).state || ''; const mobileRedirectUri = (request.query as any).mobile_redirect_uri || ''; @@ -180,7 +180,7 @@ export async function authRoutes(app: FastifyInstance) { }); // Google callback - app.get('/google/callback', { preHandler: [app.oauthCallbackRateLimit] }, async (request: FastifyRequest<{ Querystring: OAuthCallbackQuery }>, reply: FastifyReply) => { + app.get('/google/callback', async (request: FastifyRequest<{ Querystring: OAuthCallbackQuery }>, reply: FastifyReply) => { const { code, state } = request.query; const storedState = request.cookies?.oauth_state;