@@ -47,8 +47,11 @@ import { AuthUserPayload } from './types/auth-user.type';
4747import { GoogleProfile } from './strategies/google.strategy' ;
4848
4949import { LoginRateLimitService } from './login-rate-limit.service' ;
50+ import { RateLimitService } from './rate-limit.service' ;
5051import { UserRole } from '../types/prisma.types' ;
5152import { FraudService } from '../fraud/fraud.service' ;
53+ import { ENDPOINT_RATE_LIMITS } from './rate-limit.config' ;
54+ import { CacheService } from '../cache/cache.service' ;
5255import { ApiKeyAnalyticsService } from './api-key-analytics.service' ;
5356
5457type JwtPayload = {
@@ -88,8 +91,10 @@ export class AuthService {
8891 private readonly sessionsService : SessionsService ,
8992 private readonly configService : ConfigService ,
9093 private readonly emailService : EmailService ,
91- private readonly rateLimitService : LoginRateLimitService ,
94+ private readonly loginRateLimitService : LoginRateLimitService ,
95+ private readonly rateLimitService : RateLimitService ,
9296 private readonly fraudService : FraudService ,
97+ private readonly cacheService : CacheService ,
9398 @Optional ( ) private readonly apiKeyAnalyticsService ?: ApiKeyAnalyticsService ,
9499 ) {
95100 this . jwtSecret = this . configService . get < string > ( 'JWT_SECRET' ) ?? 'propchain-access-secret' ;
@@ -122,7 +127,7 @@ export class AuthService {
122127 async register ( data : RegisterDto , ipAddress ?: string ) {
123128 // Block re-registration from same IP until prior email is verified
124129 if ( ipAddress ) {
125- const allowed = this . canRegisterFromIp ( ipAddress ) ;
130+ const allowed = await this . canRegisterFromIp ( ipAddress ) ;
126131 if ( ! allowed ) {
127132 throw new BadRequestException (
128133 'A registration from this IP is already pending email verification. Please verify your email before registering a new account.' ,
@@ -189,15 +194,23 @@ export class AuthService {
189194
190195 // Track IP for re-registration prevention
191196 if ( ipAddress ) {
192- const expiryMs =
197+ const expirySeconds =
193198 parseDuration (
194199 this . configService . get < string > ( 'EMAIL_VERIFICATION_EXPIRES_IN' ) ?? '24h' ,
195200 24 * 60 * 60 ,
196- ) * 1000 ;
197- this . registrationIpMap . set ( ipAddress , {
201+ ) ;
202+ const expiryMs = expirySeconds * 1000 ;
203+ const cacheKey = `registration:ip:${ ipAddress } ` ;
204+ const entry = {
198205 email : user . email ,
199206 expiresAt : new Date ( Date . now ( ) + expiryMs ) ,
200- } ) ;
207+ } ;
208+
209+ // Store in Redis with TTL
210+ await this . cacheService . set ( cacheKey , entry , expirySeconds ) ;
211+
212+ // Also keep in in-memory map for backward compatibility/fallback
213+ this . registrationIpMap . set ( ipAddress , entry ) ;
201214 }
202215
203216 return {
@@ -207,23 +220,45 @@ export class AuthService {
207220 } ;
208221 }
209222
210- private canRegisterFromIp ( ipAddress : string ) : boolean {
211- const entry = this . registrationIpMap . get ( ipAddress ) ;
212- if ( ! entry ) return true ;
213- if ( Date . now ( ) > entry . expiresAt . getTime ( ) ) {
223+ private async canRegisterFromIp ( ipAddress : string ) : Promise < boolean > {
224+ const cacheKey = `registration:ip:${ ipAddress } ` ;
225+ const entry = await this . cacheService . get < { email : string ; expiresAt : Date } > ( cacheKey ) ;
226+
227+ // Check cache first
228+ if ( entry ) {
229+ if ( Date . now ( ) > entry . expiresAt . getTime ( ) ) {
230+ await this . cacheService . del ( cacheKey ) ;
231+ return true ;
232+ }
233+ return false ;
234+ }
235+
236+ // Fallback to in-memory map for backward compatibility
237+ const inMemoryEntry = this . registrationIpMap . get ( ipAddress ) ;
238+ if ( ! inMemoryEntry ) return true ;
239+ if ( Date . now ( ) > inMemoryEntry . expiresAt . getTime ( ) ) {
214240 this . registrationIpMap . delete ( ipAddress ) ;
215241 return true ;
216242 }
217243 return false ;
218244 }
219245
220- private cleanupIpForEmail ( email : string ) : void {
246+ private async cleanupIpForEmail ( email : string ) : Promise < void > {
247+ // First check in-memory map to find the IP for this email
248+ let ipToCleanup : string | null = null ;
221249 for ( const [ ip , entry ] of this . registrationIpMap . entries ( ) ) {
222250 if ( entry . email === email ) {
223251 this . registrationIpMap . delete ( ip ) ;
224- return ;
252+ ipToCleanup = ip ;
253+ break ;
225254 }
226255 }
256+
257+ // Also delete from Redis if we found the IP, or scan for it
258+ if ( ipToCleanup ) {
259+ const cacheKey = `registration:ip:${ ipToCleanup } ` ;
260+ await this . cacheService . del ( cacheKey ) ;
261+ }
227262 }
228263
229264 /**
@@ -1374,7 +1409,23 @@ export class AuthService {
13741409 return Array . from ( new Set ( permissions . map ( ( permission ) => permission . trim ( ) ) . filter ( Boolean ) ) ) ;
13751410 }
13761411
1377- async requestPasswordReset ( data : RequestPasswordResetDto ) : Promise < void > {
1412+ async requestPasswordReset ( data : RequestPasswordResetDto , ipAddress ?: string ) : Promise < void > {
1413+ // Apply rate limiting: max 3 requests per email per hour
1414+ const emailRateLimit = await this . rateLimitService . checkEmailRateLimit (
1415+ 'POST /auth/password-reset/request' ,
1416+ data . email ,
1417+ 3 ,
1418+ 60 * 60 * 1000 , // 1 hour
1419+ ) ;
1420+
1421+ if ( emailRateLimit . isExceeded ) {
1422+ this . logger . warn (
1423+ `Password reset request rate limit exceeded for email: ${ redactEmail ( data . email ) } (IP: ${ ipAddress || 'unknown' } )` ,
1424+ ) ;
1425+ // Don't reveal rate limit was exceeded to prevent user enumeration
1426+ return ;
1427+ }
1428+
13781429 const user = await this . usersService . findByEmail ( data . email ) ;
13791430 if ( ! user ) {
13801431 // Don't reveal if email exists or not for security
@@ -1415,7 +1466,22 @@ export class AuthService {
14151466 await this . emailService . sendPasswordResetEmail ( user . email , resetToken ) ;
14161467 }
14171468
1418- async resetPassword ( data : ResetPasswordDto ) : Promise < void > {
1469+ async resetPassword ( data : ResetPasswordDto , ipAddress ?: string ) : Promise < void > {
1470+ // Apply rate limiting: max 5 attempts per token per hour
1471+ const tokenRateLimit = await this . rateLimitService . checkTokenRateLimit (
1472+ 'POST /auth/password-reset/reset' ,
1473+ data . token ,
1474+ 5 ,
1475+ 60 * 60 * 1000 , // 1 hour
1476+ ) ;
1477+
1478+ if ( tokenRateLimit . isExceeded ) {
1479+ this . logger . warn (
1480+ `Password reset token rate limit exceeded. Token: ${ data . token . substring ( 0 , 8 ) } ... (IP: ${ ipAddress || 'unknown' } )` ,
1481+ ) ;
1482+ throw new BadRequestException ( 'Too many attempts. Please try again later.' ) ;
1483+ }
1484+
14191485 const tokenHash = createSha256 ( data . token ) ;
14201486 const resetToken = await this . prisma . passwordResetToken . findUnique ( {
14211487 where : { token : tokenHash } ,
@@ -1585,6 +1651,21 @@ export class AuthService {
15851651 }
15861652
15871653 async verifyInitialEmail ( token : string , ipAddress ?: string , userAgent ?: string ) {
1654+ // Apply rate limiting: max 5 attempts per token per hour
1655+ const tokenRateLimit = await this . rateLimitService . checkTokenRateLimit (
1656+ 'POST /auth/verify-email' ,
1657+ token ,
1658+ 5 ,
1659+ 60 * 60 * 1000 , // 1 hour
1660+ ) ;
1661+
1662+ if ( tokenRateLimit . isExceeded ) {
1663+ this . logger . warn (
1664+ `Email verification token rate limit exceeded. Token: ${ token . substring ( 0 , 8 ) } ... (IP: ${ ipAddress || 'unknown' } )` ,
1665+ ) ;
1666+ throw new BadRequestException ( 'Too many attempts. Please try again later.' ) ;
1667+ }
1668+
15881669 // Find user by verification token
15891670 const user = await this . prisma . user . findFirst ( {
15901671 where : {
@@ -1624,6 +1705,9 @@ export class AuthService {
16241705 } ,
16251706 } ) ;
16261707
1708+ // Clean up IP tracking since email is now verified
1709+ await this . cleanupIpForEmail ( user . email ) ;
1710+
16271711 // Issue token pair
16281712 const tokens = await this . issueTokenPair ( updatedUser , undefined , ipAddress , userAgent ) ;
16291713
@@ -1635,6 +1719,22 @@ export class AuthService {
16351719 }
16361720
16371721 async resendEmailVerification ( email : string , ipAddress ?: string , userAgent ?: string ) {
1722+ // Apply rate limiting: max 3 requests per email per hour
1723+ const emailRateLimit = await this . rateLimitService . checkEmailRateLimit (
1724+ 'POST /auth/email/resend' ,
1725+ email ,
1726+ 3 ,
1727+ 60 * 60 * 1000 , // 1 hour
1728+ ) ;
1729+
1730+ if ( emailRateLimit . isExceeded ) {
1731+ this . logger . warn (
1732+ `Email resend rate limit exceeded for email: ${ redactEmail ( email ) } (IP: ${ ipAddress || 'unknown' } )` ,
1733+ ) ;
1734+ // Don't reveal rate limit was exceeded to prevent user enumeration
1735+ return ;
1736+ }
1737+
16381738 const user = await this . usersService . findByEmail ( email ) ;
16391739 if ( ! user ) {
16401740 return ;
@@ -1681,4 +1781,4 @@ export class AuthService {
16811781
16821782 this . logger . log ( `Verification email resent for user ${ user . id } ` ) ;
16831783 }
1684- }
1784+ }
0 commit comments