99use App \Http \Middleware \GlobalRateLimiter ;
1010use Illuminate \Http \Request ;
1111use Illuminate \Support \Facades \Auth ;
12- use Illuminate \Support \Facades \RateLimiter ;
1312
1413class TwoFactorController extends Controller
1514{
@@ -25,7 +24,7 @@ public function __construct(
2524 $ this ->twoFactorService = $ twoFactorService ;
2625 $ this ->otpService = $ otpService ;
2726 $ this ->globalRateLimiter = $ globalRateLimiter ;
28- }
27+ }
2928
3029 public function activate ()
3130 {
@@ -46,20 +45,28 @@ public function enable(TwoFactorEnableRequest $request)
4645 {
4746 $ userId = Auth::id ();
4847
49- // Rate limiting untuk setup dengan enhanced key (IP + User-Agent + User ID)
48+ // Rate limiting key for 2FA setup
5049 $ key = $ this ->get2faSetupRateLimitKey ($ request , $ userId );
5150 $ maxAttempts = config ('app.2fa_setup_max_attempts ' , 3 );
52- $ decaySeconds = config ('app.2fa_setup_decay_seconds ' , 300 );
51+ $ decayMinutes = config ('app.2fa_setup_decay_seconds ' , 300 ) / 60 ;
5352
54- if (RateLimiter::tooManyAttempts ($ key , $ maxAttempts )) {
53+ // Check if account is locked due to too many failed attempts
54+ $ lockoutCheck = $ this ->globalRateLimiter ->isLocked ($ key , $ maxAttempts );
55+ if ($ lockoutCheck ['locked ' ]) {
56+ $ minutes = ceil ($ lockoutCheck ['availableIn ' ] / 60 );
5557 return response ()->json ([
5658 'success ' => false ,
57- 'message ' => 'Terlalu banyak percobaan. Coba lagi dalam ' . RateLimiter::availableIn ($ key ) . ' detik. '
58- ], 429 );
59+ 'message ' => "AKUN TERKUNCI. Terlalu banyak percobaan aktivasi 2FA. Coba lagi dalam {$ minutes } menit. " ,
60+ 'locked ' => true ,
61+ 'retry_after ' => $ lockoutCheck ['availableIn ' ],
62+ ], 403 );
5963 }
6064
61- RateLimiter::hit ($ key , $ decaySeconds );
6265 $ identifier = $ request ->channel === 'email ' ? Auth::user ()->email : Auth::user ()->telegram_chat_id ;
66+
67+ // Record this attempt (will apply progressive delay)
68+ $ result = $ this ->globalRateLimiter ->recordFailedAttempt ($ key , $ maxAttempts , $ decayMinutes );
69+
6370 // Simpan konfigurasi sementara di session
6471 $ request ->session ()->put ('temp_2fa_config ' , [
6572 'channel ' => $ request ->channel ,
@@ -92,20 +99,23 @@ public function verifyEnable(TwoFactorVerifyRequest $request)
9299 {
93100 $ userId = Auth::id ();
94101
95- // Rate limiting untuk verifikasi dengan enhanced key
102+ // Rate limiting key for 2FA verification
96103 $ key = $ this ->get2faVerifyRateLimitKey ($ request , $ userId );
97104 $ maxAttempts = config ('app.2fa_verify_max_attempts ' , 5 );
98- $ decaySeconds = config ('app.2fa_verify_decay_seconds ' , 300 );
105+ $ decayMinutes = config ('app.2fa_verify_decay_seconds ' , 300 ) / 60 ;
99106
100- if (RateLimiter::tooManyAttempts ($ key , $ maxAttempts )) {
107+ // Check if account is locked due to too many failed attempts
108+ $ lockoutCheck = $ this ->globalRateLimiter ->isLocked ($ key , $ maxAttempts );
109+ if ($ lockoutCheck ['locked ' ]) {
110+ $ minutes = ceil ($ lockoutCheck ['availableIn ' ] / 60 );
101111 return response ()->json ([
102112 'success ' => false ,
103- 'message ' => 'Terlalu banyak percobaan verifikasi. Coba lagi dalam ' . RateLimiter::availableIn ($ key ) . ' detik. '
104- ], 429 );
113+ 'message ' => "AKUN TERKUNCI. Terlalu banyak percobaan verifikasi 2FA. Coba lagi dalam {$ minutes } menit. " ,
114+ 'locked ' => true ,
115+ 'retry_after ' => $ lockoutCheck ['availableIn ' ],
116+ ], 403 );
105117 }
106118
107- RateLimiter::hit ($ key , $ decaySeconds );
108-
109119 $ tempConfig = $ request ->session ()->get ('temp_2fa_config ' );
110120
111121 if (!$ tempConfig ) {
@@ -123,7 +133,8 @@ public function verifyEnable(TwoFactorVerifyRequest $request)
123133 session (['2fa_verified ' => true ]);
124134 // Hapus konfigurasi sementara
125135 $ request ->session ()->forget ('temp_2fa_config ' );
126- RateLimiter::clear ($ key );
136+ // Clear rate limiter on successful verification
137+ $ this ->globalRateLimiter ->clearFailedAttempts ($ key );
127138
128139 return response ()->json ([
129140 'success ' => true ,
@@ -132,10 +143,30 @@ public function verifyEnable(TwoFactorVerifyRequest $request)
132143 ]);
133144 }
134145
135- return response ()->json ([
146+ // Record failed attempt with progressive delay
147+ $ failResult = $ this ->globalRateLimiter ->recordFailedAttempt ($ key , $ maxAttempts , $ decayMinutes );
148+
149+ $ response = [
136150 'success ' => false ,
137151 'message ' => $ result ['message ' ]
138- ], 400 );
152+ ];
153+
154+ // Add progressive delay information
155+ if ($ failResult ['delay ' ] > 0 ) {
156+ $ response ['progressive_delay ' ] = $ failResult ['delay ' ];
157+ $ response ['message ' ] = "Kode tidak valid. Percobaan gagal ke- {$ failResult ['attempts ' ]}. Delay: {$ failResult ['delay ' ]} detik. " ;
158+ }
159+
160+ // Add lockout warning
161+ if ($ failResult ['locked ' ]) {
162+ $ response ['message ' ] = "AKUN TERKUNCI. Terlalu banyak gagal verifikasi ( {$ failResult ['attempts ' ]} kali). " ;
163+ $ response ['locked ' ] = true ;
164+ $ response ['lockout_expires_in ' ] = $ failResult ['lockout_expires_in ' ] ?? 900 ;
165+ } elseif ($ failResult ['remaining ' ] === 0 ) {
166+ $ response ['message ' ] = "PERINGATAN: Akun akan terkunci setelah {$ failResult ['attempts ' ]} kali gagal verifikasi. " ;
167+ }
168+
169+ return response ()->json ($ response , 400 );
139170 }
140171
141172 /**
@@ -167,19 +198,25 @@ public function resend(Request $request)
167198
168199 $ userId = Auth::id ();
169200
170- // Rate limiting untuk resend dengan enhanced key
201+ // Rate limiting key for 2FA resend
171202 $ key = $ this ->get2faResendRateLimitKey ($ request , $ userId );
172203 $ maxAttempts = config ('app.2fa_resend_max_attempts ' , 2 );
173- $ decaySeconds = config ('app.2fa_resend_decay_seconds ' , 30 );
204+ $ decayMinutes = config ('app.2fa_resend_decay_seconds ' , 30 ) / 60 ;
174205
175- if (RateLimiter::tooManyAttempts ($ key , $ maxAttempts )) {
206+ // Check if rate limited
207+ $ lockoutCheck = $ this ->globalRateLimiter ->isLocked ($ key , $ maxAttempts );
208+ if ($ lockoutCheck ['locked ' ]) {
209+ $ minutes = ceil ($ lockoutCheck ['availableIn ' ] / 60 );
176210 return response ()->json ([
177211 'success ' => false ,
178- 'message ' => 'Tunggu ' . RateLimiter::availableIn ($ key ) . ' detik sebelum mengirim ulang. '
212+ 'message ' => "Terlalu banyak permintaan. Tunggu {$ minutes } menit sebelum mengirim ulang. " ,
213+ 'locked ' => true ,
214+ 'retry_after ' => $ lockoutCheck ['availableIn ' ],
179215 ], 429 );
180216 }
181217
182- RateLimiter::hit ($ key , $ decaySeconds );
218+ // Record this attempt
219+ $ this ->globalRateLimiter ->recordFailedAttempt ($ key , $ maxAttempts , $ decayMinutes );
183220
184221 $ result = $ this ->otpService ->generateAndSend (
185222 Auth::id (),
@@ -218,26 +255,30 @@ public function verifyChallenge(TwoFactorVerifyRequest $request)
218255 {
219256 $ userId = Auth::id ();
220257
221- // Rate limiting untuk verifikasi challenge dengan enhanced key
258+ // Rate limiting key for 2FA challenge
222259 $ key = $ this ->get2faChallengeRateLimitKey ($ request , $ userId );
223260 $ maxAttempts = config ('app.2fa_challenge_max_attempts ' , 5 );
224- $ decaySeconds = config ('app.2fa_challenge_decay_seconds ' , 300 );
261+ $ decayMinutes = config ('app.2fa_challenge_decay_seconds ' , 300 ) / 60 ;
225262
226- if (RateLimiter::tooManyAttempts ($ key , $ maxAttempts )) {
263+ // Check if account is locked due to too many failed attempts
264+ $ lockoutCheck = $ this ->globalRateLimiter ->isLocked ($ key , $ maxAttempts );
265+ if ($ lockoutCheck ['locked ' ]) {
266+ $ minutes = ceil ($ lockoutCheck ['availableIn ' ] / 60 );
227267 return response ()->json ([
228268 'success ' => false ,
229- 'message ' => 'Terlalu banyak percobaan. Coba lagi dalam ' . RateLimiter::availableIn ($ key ) . ' detik. '
230- ], 429 );
269+ 'message ' => "AKUN TERKUNCI. Terlalu banyak percobaan verifikasi 2FA. Coba lagi dalam {$ minutes } menit. " ,
270+ 'locked ' => true ,
271+ 'retry_after ' => $ lockoutCheck ['availableIn ' ],
272+ ], 403 );
231273 }
232274
233- RateLimiter::hit ($ key , $ decaySeconds );
234-
235275 $ result = $ this ->otpService ->verify (Auth::id (), $ request ->code );
236276
237277 if ($ result ['success ' ]) {
238278 // Tandai session bahwa 2FA sudah terverifikasi
239279 session (['2fa_verified ' => true ]);
240- RateLimiter::clear ($ key );
280+ // Clear rate limiter on successful verification
281+ $ this ->globalRateLimiter ->clearFailedAttempts ($ key );
241282
242283 return response ()->json ([
243284 'success ' => true ,
@@ -246,10 +287,30 @@ public function verifyChallenge(TwoFactorVerifyRequest $request)
246287 ]);
247288 }
248289
249- return response ()->json ([
290+ // Record failed attempt with progressive delay
291+ $ failResult = $ this ->globalRateLimiter ->recordFailedAttempt ($ key , $ maxAttempts , $ decayMinutes );
292+
293+ $ response = [
250294 'success ' => false ,
251295 'message ' => $ result ['message ' ]
252- ], 400 );
296+ ];
297+
298+ // Add progressive delay information
299+ if ($ failResult ['delay ' ] > 0 ) {
300+ $ response ['progressive_delay ' ] = $ failResult ['delay ' ];
301+ $ response ['message ' ] = "Kode tidak valid. Percobaan gagal ke- {$ failResult ['attempts ' ]}. Delay: {$ failResult ['delay ' ]} detik. " ;
302+ }
303+
304+ // Add lockout warning
305+ if ($ failResult ['locked ' ]) {
306+ $ response ['message ' ] = "AKUN TERKUNCI. Terlalu banyak gagal verifikasi ( {$ failResult ['attempts ' ]} kali). " ;
307+ $ response ['locked ' ] = true ;
308+ $ response ['lockout_expires_in ' ] = $ failResult ['lockout_expires_in ' ] ?? 900 ;
309+ } elseif ($ failResult ['remaining ' ] === 0 ) {
310+ $ response ['message ' ] = "PERINGATAN: Akun akan terkunci setelah {$ failResult ['attempts ' ]} kali gagal verifikasi. " ;
311+ }
312+
313+ return response ()->json ($ response , 400 );
253314 }
254315
255316 /**
0 commit comments