1- import type { FastifyInstance , FastifyRequest } from 'fastify' ;
1+ import type { FastifyInstance , FastifyRequest , FastifyReply } from 'fastify' ;
22import fastifyPlugin from 'fastify-plugin' ;
33import rateLimit from '@fastify/rate-limit' ;
44
@@ -9,8 +9,18 @@ import rateLimit from '@fastify/rate-limit';
99 * - OAuth start endpoints: 10 requests per minute per IP
1010 * - Uses Redis for distributed rate limiting across multiple instances
1111 */
12+
13+ // Extend Fastify instance with OAuth rate limit middleware
14+ declare module 'fastify' {
15+ interface FastifyInstance {
16+ oauthCallbackRateLimit : ( request : FastifyRequest , reply : FastifyReply ) => Promise < void > ;
17+ oauthStartRateLimit : ( request : FastifyRequest , reply : FastifyReply ) => Promise < void > ;
18+ }
19+ }
20+
1221export const oauthRateLimitPlugin = fastifyPlugin ( async ( app : FastifyInstance ) => {
1322 // Rate limit for OAuth callback endpoints (stricter)
23+ // cache: 10000 = in-memory LRU capacity; sufficient for per-IP tracking on typical apps
1424 const callbackLimiter = rateLimit . createStore ( {
1525 max : 5 ,
1626 timeWindow : '1 minute' ,
@@ -19,6 +29,7 @@ export const oauthRateLimitPlugin = fastifyPlugin(async (app: FastifyInstance) =
1929 } ) ;
2030
2131 // Rate limit for OAuth start endpoints (moderate)
32+ // cache: 10000 = in-memory LRU capacity; sufficient for per-IP tracking on typical apps
2233 const startLimiter = rateLimit . createStore ( {
2334 max : 10 ,
2435 timeWindow : '1 minute' ,
@@ -29,37 +40,42 @@ export const oauthRateLimitPlugin = fastifyPlugin(async (app: FastifyInstance) =
2940 // Middleware for OAuth callback rate limiting (per IP, with user-aware fallback)
3041 const callbackRateLimitMiddleware = async (
3142 request : FastifyRequest ,
32- reply : any
43+ reply : FastifyReply
3344 ) => {
3445 // Use user ID if authenticated, otherwise use IP
3546 const key = ( request . user as any ) ?. id || request . ip ;
36- const limited = await callbackLimiter . incr ( key ) ;
47+ const count = await callbackLimiter . incr ( key ) ;
3748
38- if ( limited > 5 ) {
49+ // incr() returns count AFTER incrementing, so >= 5 means limit exceeded
50+ if ( count >= 5 ) {
51+ reply . header ( 'Retry-After' , '60' ) ;
3952 return reply . status ( 429 ) . send ( {
4053 error : 'Too many authentication attempts. Please try again later.' ,
41- retryAfter : 60 ,
4254 } ) ;
4355 }
4456 } ;
4557
4658 // Middleware for OAuth start rate limiting (per IP)
4759 const startRateLimitMiddleware = async (
4860 request : FastifyRequest ,
49- reply : any
61+ reply : FastifyReply
5062 ) => {
5163 const key = `oauth_start:${ request . ip } ` ;
52- const limited = await startLimiter . incr ( key ) ;
64+ const count = await startLimiter . incr ( key ) ;
5365
54- if ( limited > 10 ) {
66+ // incr() returns count AFTER incrementing, so >= 10 means limit exceeded
67+ if ( count >= 10 ) {
68+ reply . header ( 'Retry-After' , '60' ) ;
5569 return reply . status ( 429 ) . send ( {
5670 error : 'Too many OAuth requests. Please try again later.' ,
57- retryAfter : 60 ,
5871 } ) ;
5972 }
6073 } ;
6174
6275 // Export middleware for use in auth routes
63- app . decorate ( 'oauthCallbackRateLimit' , callbackRateLimitMiddleware ) ;
64- app . decorate ( 'oauthStartRateLimit' , startRateLimitMiddleware ) ;
76+ app . decorate (
77+ 'oauthCallbackRateLimit' ,
78+ callbackRateLimitMiddleware as any
79+ ) ;
80+ app . decorate ( 'oauthStartRateLimit' , startRateLimitMiddleware as any ) ;
6581} ) ;
0 commit comments