@@ -74,11 +74,30 @@ export class AuthController extends AbstractController {
7474 @Body ( ) body : LocalLoginBody ,
7575 ) : Promise < Response > {
7676 const payload = body ?. body && typeof body . body === 'object' ? body . body : body ;
77- const user = await this . service . authenticateWithLocal (
78- payload ?. username ,
79- payload ?. password ,
80- resolveClientIp ( req ) ?? undefined ,
77+ const ip = resolveClientIp ( req ) ?? null ;
78+ const username = `${ payload ?. username || '' } ` . trim ( ) ;
79+
80+ const bruteforce = await this . service . getLocalBruteforceBlock ( {
81+ username,
82+ ip,
83+ } ) ;
84+ this . logger . debug (
85+ `[anti-bf] local username=${ username || 'N/A' } ip=${ ip || 'n/a' } blocked=${ bruteforce . blocked } retryAfter=${ bruteforce . retryAfterSeconds } s` ,
8186 ) ;
87+ if ( bruteforce . blocked ) {
88+ await this . service . auditAuthAttempt ( {
89+ username : username || 'N/A' ,
90+ ip,
91+ result : 'failed' ,
92+ reason : 'bruteforce_blocked' ,
93+ } ) ;
94+ return res . status ( HttpStatus . TOO_MANY_REQUESTS ) . set ( 'Retry-After' , `${ bruteforce . retryAfterSeconds } ` ) . json ( {
95+ message : 'Too many authentication attempts. Please retry later.' ,
96+ retryAfterSeconds : bruteforce . retryAfterSeconds ,
97+ } ) ;
98+ }
99+
100+ const user = await this . service . authenticateWithLocal ( username , payload ?. password , ip ?? undefined ) ;
82101 if ( ! user ) throw new UnauthorizedException ( ) ;
83102
84103 if ( this . service . isTotpEnabledForUser ( user ) ) {
@@ -96,11 +115,45 @@ export class AuthController extends AbstractController {
96115 } ) ;
97116 }
98117
118+ const totpBruteforce = await this . service . getTotpBruteforceBlock ( {
119+ ip,
120+ challengeToken : payload . challengeToken . trim ( ) ,
121+ } ) ;
122+ this . logger . debug (
123+ `[anti-bf] totp username=${ user . username || 'N/A' } ip=${ ip || 'n/a' } blocked=${ totpBruteforce . blocked } retryAfter=${ totpBruteforce . retryAfterSeconds } s` ,
124+ ) ;
125+ if ( totpBruteforce . blocked ) {
126+ await this . service . auditAuthAttempt ( {
127+ username : user . username ,
128+ ip,
129+ result : 'failed' ,
130+ reason : 'totp_bruteforce_blocked' ,
131+ agentId : user . _id ,
132+ } ) ;
133+ return res . status ( HttpStatus . TOO_MANY_REQUESTS ) . set ( 'Retry-After' , `${ totpBruteforce . retryAfterSeconds } ` ) . json ( {
134+ message : 'Too many OTP attempts. Please retry later.' ,
135+ retryAfterSeconds : totpBruteforce . retryAfterSeconds ,
136+ } ) ;
137+ }
138+
99139 const verifiedUser = await this . service . verifyTotpChallenge (
100140 payload . challengeToken . trim ( ) ,
101141 payload . otpCode . trim ( ) ,
102142 ) ;
103- if ( ! verifiedUser || `${ verifiedUser . _id } ` !== `${ user . _id } ` ) throw new UnauthorizedException ( ) ;
143+ if ( ! verifiedUser || `${ verifiedUser . _id } ` !== `${ user . _id } ` ) {
144+ await this . service . registerTotpBruteforceFailure ( {
145+ ip,
146+ challengeToken : payload . challengeToken . trim ( ) ,
147+ username : user . username ,
148+ agentId : user . _id ,
149+ } ) ;
150+ throw new UnauthorizedException ( ) ;
151+ }
152+
153+ await this . service . clearTotpBruteforceState ( {
154+ ip,
155+ challengeToken : payload . challengeToken . trim ( ) ,
156+ } ) ;
104157
105158 const tokens = await this . service . createTokens ( verifiedUser , undefined , { mfaVerified : true } ) ;
106159 const uri =
@@ -125,12 +178,47 @@ export class AuthController extends AbstractController {
125178
126179 @Post ( 'local/2fa/verify' )
127180 @ApiOperation ( { summary : 'Validation du code TOTP pour finaliser la connexion' } )
128- public async verifyLocal2fa ( @Res ( ) res : Response , @Body ( ) body : VerifyTotpBody ) : Promise < Response > {
129- const user = await this . service . verifyTotpChallenge (
130- `${ body ?. challengeToken || '' } ` . trim ( ) ,
131- `${ body ?. otpCode || '' } ` . trim ( ) ,
181+ public async verifyLocal2fa (
182+ @Res ( ) res : Response ,
183+ @Req ( ) req : Request ,
184+ @Body ( ) body : VerifyTotpBody ,
185+ ) : Promise < Response > {
186+ const ip = resolveClientIp ( req ) ?? null ;
187+ const challengeToken = `${ body ?. challengeToken || '' } ` . trim ( ) ;
188+
189+ const totpBruteforce = await this . service . getTotpBruteforceBlock ( {
190+ ip,
191+ challengeToken,
192+ } ) ;
193+ this . logger . debug (
194+ `[anti-bf] totp-verify ip=${ ip || 'n/a' } tokenPrefix=${ challengeToken . slice ( 0 , 8 ) || 'n/a' } blocked=${ totpBruteforce . blocked } retryAfter=${ totpBruteforce . retryAfterSeconds } s` ,
132195 ) ;
133- if ( ! user ) throw new UnauthorizedException ( ) ;
196+ if ( totpBruteforce . blocked ) {
197+ await this . service . auditAuthAttempt ( {
198+ username : 'N/A' ,
199+ ip,
200+ result : 'failed' ,
201+ reason : 'totp_bruteforce_blocked' ,
202+ } ) ;
203+ return res . status ( HttpStatus . TOO_MANY_REQUESTS ) . set ( 'Retry-After' , `${ totpBruteforce . retryAfterSeconds } ` ) . json ( {
204+ message : 'Too many OTP attempts. Please retry later.' ,
205+ retryAfterSeconds : totpBruteforce . retryAfterSeconds ,
206+ } ) ;
207+ }
208+
209+ const user = await this . service . verifyTotpChallenge ( challengeToken , `${ body ?. otpCode || '' } ` . trim ( ) ) ;
210+ if ( ! user ) {
211+ await this . service . registerTotpBruteforceFailure ( {
212+ ip,
213+ challengeToken,
214+ } ) ;
215+ throw new UnauthorizedException ( ) ;
216+ }
217+
218+ await this . service . clearTotpBruteforceState ( {
219+ ip,
220+ challengeToken,
221+ } ) ;
134222 const tokens = await this . service . createTokens ( user , undefined , { mfaVerified : true } ) ;
135223 const uri = typeof user ?. baseURL === 'string' && user . baseURL . trim ( ) . length > 0 ? user . baseURL . trim ( ) : '/' ;
136224 return res . status ( HttpStatus . OK ) . json ( {
0 commit comments