@@ -364,7 +364,8 @@ def test_with_backup_phone(self, mock_signal, fake):
364
364
365
365
# Valid token should be accepted.
366
366
response = self ._post ({'token-otp_token' : totp_str (device .bin_key ),
367
- 'login_view-current_step' : 'token' })
367
+ 'login_view-current_step' : 'token' ,
368
+ 'device_id' : device .persistent_id })
368
369
self .assertRedirects (response , resolve_url (settings .LOGIN_REDIRECT_URL ))
369
370
self .assertEqual (device .persistent_id ,
370
371
self .client .session .get (DEVICE_ID_SESSION_KEY ))
@@ -406,6 +407,94 @@ def test_with_backup_token(self, mock_signal):
406
407
# Check that the signal was fired.
407
408
mock_signal .assert_called_with (sender = mock .ANY , request = mock .ANY , user = user , device = device )
408
409
410
+ def test_totp_token_does_not_impact_backup_token (self ):
411
+ """
412
+ Ensures that successfully authenticating with a TOTP token does not
413
+ inadvertently increase the throttling count of the backup token device.
414
+
415
+ Addresses issue #473, where correct TOTP token usage was unintentionally
416
+ affecting the throttling count of backup tokens, potentially leading to
417
+ their invalidation.
418
+ """
419
+ user = self .create_user ()
420
+ backup_device = user .staticdevice_set .create (name = 'backup' )
421
+ backup_device .token_set .create (token = 'abcdef123' )
422
+ totp_device = user .totpdevice_set .create (name = 'default' , key = random_hex ())
423
+
424
+ response = self .
_post ({
'auth-username' :
'[email protected] ' ,
425
+ 'auth-password' : 'secret' ,
426
+ 'login_view-current_step' : 'auth' })
427
+ self .assertContains (response , 'Token:' )
428
+
429
+ backup_device .refresh_from_db ()
430
+ self .assertEqual (backup_device .throttling_failure_count , 0 )
431
+ response = self ._post ({'token-otp_token' : totp_str (totp_device .bin_key ),
432
+ 'login_view-current_step' : 'token' })
433
+ self .assertRedirects (response , resolve_url (settings .LOGIN_REDIRECT_URL ))
434
+ self .assertEqual (self .client .session ['_auth_user_id' ], str (user .pk ))
435
+
436
+ backup_device .refresh_from_db ()
437
+ self .assertEqual (backup_device .throttling_failure_count , 0 )
438
+
439
+ def test_wrong_token_does_not_affect_other_device_throttling (self ):
440
+ """
441
+ Tests that entering an incorrect backup token increases the throttling count
442
+ of the backup device, but does not affect the TOTP device's throttling count (and other way around).
443
+
444
+ This addresses issue #473 where TOTP token submissions were incorrectly
445
+ impacting the backup device's throttling count.
446
+ """
447
+ # Setup: Create a user, backup device, and TOTP device.
448
+ user = self .create_user ()
449
+ backup_device = user .staticdevice_set .create (user = user , name = 'backup' )
450
+ backup_token = 'abcdef123'
451
+ backup_device .token_set .create (token = backup_token )
452
+ totp_device = user .totpdevice_set .create (
453
+ user = user , name = 'default' , confirmed = True ,
454
+ key = random_hex ()
455
+ )
456
+
457
+ # Simulate login process: username and password step.
458
+ response = self ._post ({
459
+ 'auth-username' : user .get_username (),
460
+ 'auth-password' : 'secret' ,
461
+ 'login_view-current_step' : 'auth' ,
462
+ })
463
+ self .assertContains (response , 'Token:' )
464
+
465
+ # Attempt login with incorrect backup token and check response.
466
+ response = self ._post ({
467
+ 'backup-otp_token' : 'WRONG' ,
468
+ 'login_view-current_step' : 'backup' ,
469
+ })
470
+ expected_error = 'Invalid token. Please make sure you have entered it correctly.'
471
+ self .assertEqual (response .context_data ['wizard' ]['form' ].errors ,
472
+ {'__all__' : [expected_error ]})
473
+
474
+ # Verify that incorrect backup token submission throttles backup device.
475
+ backup_device .refresh_from_db ()
476
+ self .assertEqual (backup_device .throttling_failure_count , 1 )
477
+
478
+ # Ensure TOTP device is not affected by the incorrect backup token submission.
479
+ totp_device .refresh_from_db ()
480
+ self .assertEqual (totp_device .throttling_failure_count , 0 )
481
+
482
+ # Attempt login with incorrect TOTP token and check response.
483
+ response = self ._post ({
484
+ 'token-otp_token' : "123456" ,
485
+ 'login_view-current_step' : 'token'
486
+ })
487
+ self .assertEqual (response .context_data ['wizard' ]['form' ].errors ,
488
+ {'__all__' : [expected_error ]})
489
+
490
+ # Backup device's throttling count should remain unchanged after TOTP token submission.
491
+ backup_device .refresh_from_db ()
492
+ self .assertEqual (backup_device .throttling_failure_count , 1 )
493
+
494
+ # Incorrect TOTP token submission should throttle TOTP device.
495
+ totp_device .refresh_from_db ()
496
+ self .assertEqual (totp_device .throttling_failure_count , 1 )
497
+
409
498
@mock .patch ('two_factor.views.utils.logger' )
410
499
def test_reset_wizard_state (self , mock_logger ):
411
500
self .create_user ()
0 commit comments