def test_with_backup_phone(self, mock_signal, fake): user = self.create_user() for no_digits in (6, 8): with self.settings(TWO_FACTOR_TOTP_DIGITS=no_digits): user.totpdevice_set.create(name='default', key=random_hex_str(), digits=no_digits) device = user.phonedevice_set.create(name='backup', number='+31101234567', method='sms', key=random_hex_str()) # Backup phones should be listed on the login form response = self._post({'auth-username': '******', 'auth-password': '******', 'login_view-current_step': 'auth'}) self.assertContains(response, 'Send text message to +31 ** *** **67') # Ask for challenge on invalid device response = self._post({'auth-username': '******', 'auth-password': '******', 'challenge_device': 'MALICIOUS/INPUT/666'}) self.assertContains(response, 'Send text message to +31 ** *** **67') # Ask for SMS challenge response = self._post({'auth-username': '******', 'auth-password': '******', 'challenge_device': device.persistent_id}) self.assertContains(response, 'We sent you a text message') test_call_kwargs = fake.return_value.send_sms.call_args[1] self.assertEqual(test_call_kwargs['device'], device) self.assertIn(test_call_kwargs['token'], [str(totp(device.bin_key, digits=no_digits, drift=i)).zfill(no_digits) for i in [-1, 0]]) # Ask for phone challenge device.method = 'call' device.save() response = self._post({'auth-username': '******', 'auth-password': '******', 'challenge_device': device.persistent_id}) self.assertContains(response, 'We are calling your phone right now') test_call_kwargs = fake.return_value.make_call.call_args[1] self.assertEqual(test_call_kwargs['device'], device) self.assertIn(test_call_kwargs['token'], [str(totp(device.bin_key, digits=no_digits, drift=i)).zfill(no_digits) for i in [-1, 0]]) # Valid token should be accepted. response = self._post({'token-otp_token': totp(device.bin_key), 'login_view-current_step': 'token'}) self.assertRedirects(response, resolve_url(settings.LOGIN_REDIRECT_URL)) self.assertEqual(device.persistent_id, self.client.session.get(DEVICE_ID_SESSION_KEY)) # Check that the signal was fired. mock_signal.assert_called_with(sender=mock.ANY, request=mock.ANY, user=user, device=device)
def get_key(self, step): self.storage.extra_data.setdefault('keys', {}) if step in self.storage.extra_data['keys']: return self.storage.extra_data['keys'].get(step) key = random_hex_str(20) self.storage.extra_data['keys'][step] = key return key
def test_throttle_with_generator(self, mock_signal): user = self.create_user() device = user.totpdevice_set.create(name='default', key=random_hex_str()) self._post({ 'auth-username': '******', 'auth-password': '******', 'login_view-current_step': 'auth' }) # throttle device device.throttle_increment() response = self._post({ 'token-otp_token': totp(device.bin_key), 'login_view-current_step': 'token' }) self.assertEqual( response.context_data['wizard']['form'].errors, { '__all__': [ 'Invalid token. Please make sure you ' 'have entered it correctly.' ] })
def test_valid_login_no_timeout(self, mock_time): mock_time.time.return_value = 12345.12 user = self.create_user() device = user.totpdevice_set.create(name='default', key=random_hex_str()) response = self._post({ 'auth-username': '******', 'auth-password': '******', 'login_view-current_step': 'auth' }) self.assertContains(response, 'Token:') self.assertEqual(self.client.session['wizard_login_view']['user_pk'], str(user.pk)) self.assertEqual( self.client.session['wizard_login_view']['user_backend'], 'django.contrib.auth.backends.ModelBackend') self.assertEqual( self.client.session['wizard_login_view']['authentication_time'], 12345) mock_time.time.return_value = 20345.12 response = self._post({ 'token-otp_token': totp(device.bin_key), 'login_view-current_step': 'token' }) self.assertRedirects(response, resolve_url(settings.LOGIN_REDIRECT_URL)) self.assertEqual(self.client.session['_auth_user_id'], str(user.pk))
def test_verify(self): for no_digits in (6, 8): with self.settings(TWO_FACTOR_TOTP_DIGITS=no_digits): device = PhoneDevice(key=random_hex_str()) self.assertFalse(device.verify_token(-1)) self.assertFalse(device.verify_token('foobar')) self.assertTrue(device.verify_token(totp(device.bin_key, digits=no_digits)))
def test_with_backup_token(self, mock_signal): user = self.create_user() user.totpdevice_set.create(name='default', key=random_hex_str()) device = user.staticdevice_set.create(name='backup') device.token_set.create(token='abcdef123') # Backup phones should be listed on the login form response = self._post({'auth-username': '******', 'auth-password': '******', 'login_view-current_step': 'auth'}) self.assertContains(response, 'Backup Token') # Should be able to go to backup tokens step in wizard response = self._post({'wizard_goto_step': 'backup'}) self.assertContains(response, 'backup tokens') # Wrong codes should not be accepted response = self._post({'backup-otp_token': 'WRONG', 'login_view-current_step': 'backup'}) self.assertEqual(response.context_data['wizard']['form'].errors, {'__all__': ['Invalid token. Please make sure you ' 'have entered it correctly.']}) # static devices are throttled device.throttle_reset() # Valid token should be accepted. response = self._post({'backup-otp_token': 'abcdef123', 'login_view-current_step': 'backup'}) self.assertRedirects(response, resolve_url(settings.LOGIN_REDIRECT_URL)) # Check that the signal was fired. mock_signal.assert_called_with(sender=mock.ANY, request=mock.ANY, user=user, device=device)
def test_with_generator(self, mock_signal): user = self.create_user() device = user.totpdevice_set.create(name='default', key=random_hex_str()) response = self._post({'auth-username': '******', 'auth-password': '******', 'login_view-current_step': 'auth'}) self.assertContains(response, 'Token:') response = self._post({'token-otp_token': '123456', 'login_view-current_step': 'token'}) self.assertEqual(response.context_data['wizard']['form'].errors, {'__all__': ['Invalid token. Please make sure you ' 'have entered it correctly.']}) # reset throttle because we're not testing that device.throttle_reset() response = self._post({'token-otp_token': totp(device.bin_key), 'login_view-current_step': 'token'}) self.assertRedirects(response, resolve_url(settings.LOGIN_REDIRECT_URL)) self.assertEqual(device.persistent_id, self.client.session.get(DEVICE_ID_SESSION_KEY)) # Check that the signal was fired. mock_signal.assert_called_with(sender=mock.ANY, request=mock.ANY, user=user, device=device)
def test_valid_login_expired(self, mock_time, mock_logger): mock_time.time.return_value = 12345.12 user = self.create_user() device = user.totpdevice_set.create(name='default', key=random_hex_str()) response = self._post({'auth-username': '******', 'auth-password': '******', 'login_view-current_step': 'auth'}) self.assertContains(response, 'Token:') self.assertEqual(self.client.session['wizard_login_view']['user_pk'], str(user.pk)) self.assertEqual( self.client.session['wizard_login_view']['user_backend'], 'django.contrib.auth.backends.ModelBackend') self.assertEqual(self.client.session['wizard_login_view']['authentication_time'], 12345) mock_time.time.return_value = 20345.12 response = self._post({'token-otp_token': totp(device.bin_key), 'login_view-current_step': 'token'}) self.assertEqual(response.status_code, 200) self.assertNotContains(response, 'Token:') self.assertContains(response, 'Password:'******'Your session has timed out. Please login again.') # Check that a message was logged. mock_logger.info.assert_called_with( "User's authentication flow has timed out. The user " "has been redirected to the initial auth form.")
def get_key(self): """ The key is preserved between steps and stored as ascii in the session. """ if self.key_name not in self.storage.extra_data: key = random_hex_str(20) self.storage.extra_data[self.key_name] = key return self.storage.extra_data[self.key_name]
def test_verify_token_as_string(self): """ The field used to read the token may be a CharField, so the PhoneDevice must be able to validate tokens read as strings """ for no_digits in (6, 8): with self.settings(TWO_FACTOR_TOTP_DIGITS=no_digits): device = PhoneDevice(key=random_hex_str()) self.assertTrue(device.verify_token(str(totp(device.bin_key, digits=no_digits))))
def test_random_hex(self): # test that returned random_hex_str is string h = random_hex_str() self.assertIsInstance(h, six.string_types) # hex string must be 40 characters long. If cannot be longer, because CharField max_length=40 self.assertEqual(len(h), 40) # Added tests to verify that we can safely remove IF statement from random_hex_str function hh = random_hex().decode('utf-8') self.assertIsInstance(hh, six.string_types) self.assertEqual(len(hh), 40)
def test_valid_login_primary_key_stored(self, mock_time): mock_time.time.return_value = 12345.12 user = self.create_user() user.totpdevice_set.create(name='default', key=random_hex_str()) response = self._post({'auth-username': '******', 'auth-password': '******', 'login_view-current_step': 'auth'}) self.assertContains(response, 'Token:') self.assertEqual(self.client.session['wizard_login_view']['user_pk'], str(user.pk)) self.assertEqual( self.client.session['wizard_login_view']['user_backend'], 'django.contrib.auth.backends.ModelBackend') self.assertEqual(self.client.session['wizard_login_view']['authentication_time'], 12345)
def test_valid_login_post_auth_session_clear_of_form_data(self, mock_time): mock_time.time.return_value = 12345.12 user = self.create_user() user.totpdevice_set.create(name='default', key=random_hex_str()) response = self._post({'auth-username': '******', 'auth-password': '******', 'login_view-current_step': 'auth'}) self.assertContains(response, 'Token:') self.assertEqual(self.client.session['wizard_login_view']['user_pk'], str(user.pk)) self.assertEqual(self.client.session['wizard_login_view']['step'], 'token') self.assertEqual(self.client.session['wizard_login_view']['step_data'], {'auth': None}) self.assertEqual(self.client.session['wizard_login_view']['step_files'], {'auth': {}}) self.assertEqual(self.client.session['wizard_login_view']['validated_step_data'], {})
def setUp(self): super().setUp() self.user = self.create_user() self.device = self.user.totpdevice_set.create(name='default', key=random_hex_str())
def test_random_hex(self): # test that returned random_hex_str is string h = random_hex_str() self.assertIsInstance(h, str) # hex string must be 40 characters long. If cannot be longer, because CharField max_length=40 self.assertEqual(len(h), 40)