def test_with_backup_phone(self, fake): user = User.objects.create_user("bouke", None, "secret") user.totpdevice_set.create(name="default", key=random_hex().decode()) device = user.phonedevice_set.create(name="backup", number="123456789", method="sms", key=random_hex().decode()) # 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 123****89") # 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 123****89") # 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") fake.return_value.send_sms.assert_called_with(device=device, token="%06d" % totp(device.bin_key)) # 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") fake.return_value.make_call.assert_called_with(device=device, token="%06d" % totp(device.bin_key))
def test_setup(self, fake): response = self._post({'phone_setup_view-current_step': 'setup', 'setup-number': '', 'setup-method': ''}) self.assertEqual(response.context_data['wizard']['form'].errors, {'method': ['This field is required.'], 'number': ['This field is required.']}) response = self._post({'phone_setup_view-current_step': 'setup', 'setup-number': '+123456789', 'setup-method': 'call'}) self.assertContains(response, 'We\'ve sent a token to your phone') device = response.context_data['wizard']['form'].device fake.return_value.make_call.assert_called_with( device=device, token='%06d' % totp(device.bin_key)) response = self._post({'phone_setup_view-current_step': 'validation', 'validation-token': '123456'}) self.assertEqual(response.context_data['wizard']['form'].errors, {'token': ['Entered token is not valid.']}) response = self._post({'phone_setup_view-current_step': 'validation', 'validation-token': totp(device.bin_key)}) self.assertRedirects(response, str(settings.LOGIN_REDIRECT_URL)) phones = PhoneDevice.objects.filter(user=self.user).all() self.assertEqual(len(phones), 1) self.assertEqual(phones[0].name, 'backup') self.assertEqual(phones[0].number, '+123456789') self.assertEqual(phones[0].key, device.key)
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().decode(), digits=no_digits) device = user.phonedevice_set.create( name="backup", number="+31101234567", method="sms", key=random_hex().decode() ) # 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") fake.return_value.send_sms.assert_called_with( device=device, token=str(totp(device.bin_key, digits=no_digits)).zfill(no_digits) ) # 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") fake.return_value.make_call.assert_called_with( device=device, token=str(totp(device.bin_key, digits=no_digits)).zfill(no_digits) ) # Valid token should be accepted. response = self._post({"token-otp_token": totp(device.bin_key), "login_view-current_step": "token"}) self.assertRedirects(response, str(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=ANY, request=ANY, user=user, device=device)
def test_setup(self, fake): response = self._post({'phone_setup_view-current_step': 'setup', 'setup-number': '', 'setup-method': ''}) self.assertEqual(response.context_data['wizard']['form'].errors, {'method': ['This field is required.'], 'number': ['This field is required.']}) response = self._post({'phone_setup_view-current_step': 'setup', 'setup-number': '+31101234567', 'setup-method': 'call'}) self.assertContains(response, 'We\'ve sent a token to your phone') device = response.context_data['wizard']['form'].device fake.return_value.make_call.assert_called_with( device=device, token='%06d' % totp(device.bin_key)) response = self._post({'phone_setup_view-current_step': 'validation', 'validation-token': '123456'}) self.assertEqual(response.context_data['wizard']['form'].errors, {'token': ['Entered token is not valid.']}) response = self._post({'phone_setup_view-current_step': 'validation', 'validation-token': totp(device.bin_key)}) self.assertRedirects(response, str(settings.LOGIN_REDIRECT_URL)) phones = self.user.phonedevice_set.all() self.assertEqual(len(phones), 1) self.assertEqual(phones[0].name, 'backup') self.assertEqual(phones[0].number.as_e164, '+31101234567') self.assertEqual(phones[0].key, device.key)
def test_setup(self, fake): response = self._post({'phone_setup_view-current_step': 'setup', 'setup-number': '', 'setup-method': ''}) self.assertEqual(response.context_data['wizard']['form'].errors, {'method': ['This field is required.'], 'number': ['This field is required.']}) response = self._post({'phone_setup_view-current_step': 'setup', 'setup-number': '+31101234567', 'setup-method': 'call'}) self.assertContains(response, 'We\'ve sent a token to your phone') device = response.context_data['wizard']['form'].device fake.return_value.make_call.assert_called_with( device=mock.ANY, token='%06d' % totp(device.bin_key)) args, kwargs = fake.return_value.make_call.call_args submitted_device = kwargs['device'] self.assertEqual(submitted_device.number, device.number) self.assertEqual(submitted_device.key, device.key) self.assertEqual(submitted_device.method, device.method) response = self._post({'phone_setup_view-current_step': 'validation', 'validation-token': '123456'}) self.assertEqual(response.context_data['wizard']['form'].errors, {'token': ['Entered token is not valid.']}) response = self._post({'phone_setup_view-current_step': 'validation', 'validation-token': totp(device.bin_key)}) self.assertRedirects(response, resolve_url(settings.LOGIN_REDIRECT_URL)) phones = self.user.phonedevice_set.all() self.assertEqual(len(phones), 1) self.assertEqual(phones[0].name, 'backup') self.assertEqual(phones[0].number.as_e164, '+31101234567') self.assertEqual(phones[0].key, device.key)
def _phone_validation(self, mock_signal, fake, device, no_digits): # 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') self.assertContains(response, 'Remember this device for') fake.return_value.send_sms.assert_called_with( device=device, token=str(totp(device.bin_key, digits=no_digits)).zfill(no_digits)) # 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') self.assertContains(response, 'Remember this device for') fake.return_value.make_call.assert_called_with( device=device, token=str(totp(device.bin_key, digits=no_digits)).zfill(no_digits))
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 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().decode(), digits=no_digits) device = user.phonedevice_set.create(name='backup', number='+31101234567', method='sms', key=random_hex().decode()) # 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') fake.return_value.send_sms.assert_called_with( device=device, token=str(totp(device.bin_key, digits=no_digits)).zfill(no_digits)) # 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') fake.return_value.make_call.assert_called_with( device=device, token=str(totp(device.bin_key, digits=no_digits)).zfill(no_digits)) # 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 test_setup(self, fake): response = self._post({ 'phone_setup_view-current_step': 'setup', 'setup-number': '', 'setup-method': '' }) self.assertEqual( response.context_data['wizard']['form'].errors, { 'method': ['This field is required.'], 'number': ['This field is required.'] }) response = self._post({ 'phone_setup_view-current_step': 'setup', 'setup-number': '+31101234567', 'setup-method': 'call' }) self.assertContains(response, 'We\'ve sent a token to your phone') self.assertContains(response, 'autofocus="autofocus"') self.assertContains(response, 'inputmode="numeric"') self.assertContains(response, 'autocomplete="one-time-code"') device = response.context_data['wizard']['form'].device fake.return_value.make_call.assert_called_with(device=mock.ANY, token='%06d' % totp(device.bin_key)) args, kwargs = fake.return_value.make_call.call_args submitted_device = kwargs['device'] self.assertEqual(submitted_device.number, device.number) self.assertEqual(submitted_device.key, device.key) self.assertEqual(submitted_device.method, device.method) response = self._post({ 'phone_setup_view-current_step': 'validation', 'validation-token': '123456' }) self.assertEqual(response.context_data['wizard']['form'].errors, {'token': ['Entered token is not valid.']}) response = self._post({ 'phone_setup_view-current_step': 'validation', 'validation-token': totp(device.bin_key) }) self.assertRedirects(response, resolve_url(settings.LOGIN_REDIRECT_URL)) phones = self.user.phonedevice_set.all() self.assertEqual(len(phones), 1) self.assertEqual(phones[0].name, 'backup') self.assertEqual(phones[0].number.as_e164, '+31101234567') self.assertEqual(phones[0].key, device.key)
def test_setup_only_generator_available(self): response = self.client.post( reverse('two_factor:setup'), data={'setup_view-current_step': 'welcome'}) self.assertContains(response, 'Token:') session = self.client.session self.assertIn('django_two_factor-qr_secret_key', session.keys()) response = self.client.post( reverse('two_factor:setup'), data={'setup_view-current_step': 'generator'}) self.assertEqual(response.context_data['wizard']['form'].errors, {'token': ['This field is required.']}) response = self.client.post(reverse('two_factor:setup'), data={ 'setup_view-current_step': 'generator', 'generator-token': '123456' }) self.assertEqual(response.context_data['wizard']['form'].errors, {'token': ['Entered token is not valid.']}) key = response.context_data['keys'].get('generator') bin_key = unhexlify(key.encode()) response = self.client.post(reverse('two_factor:setup'), data={ 'setup_view-current_step': 'generator', 'generator-token': totp(bin_key) }) self.assertRedirects(response, reverse('two_factor:setup_complete')) self.assertEqual(1, self.user.totpdevice_set.count())
def test_with_generator(self, mock_signal): user = self.create_user() device = user.totpdevice_set.create(name='default', key=random_hex().decode()) 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.']}) 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_setup_generator(self): response = self.client.post( reverse('two_factor:setup'), data={'setup_view-current_step': 'welcome'}) self.assertContains(response, 'Method:') response = self.client.post( reverse('two_factor:setup'), data={'setup_view-current_step': 'method', 'method-method': 'generator'}) self.assertContains(response, 'Token:') response = self.client.post( reverse('two_factor:setup'), data={'setup_view-current_step': 'generator'}) self.assertEqual(response.context_data['wizard']['form'].errors, {'token': ['This field is required.']}) response = self.client.post( reverse('two_factor:setup'), data={'setup_view-current_step': 'generator', 'generator-token': '123456'}) self.assertEqual(response.context_data['wizard']['form'].errors, {'token': ['Please enter a valid token.']}) key = response.context_data['keys'].get('generator') bin_key = unhexlify(key.encode()) response = self.client.post( reverse('two_factor:setup'), data={'setup_view-current_step': 'generator', 'generator-token': totp(bin_key)}) self.assertRedirects(response, reverse('two_factor:setup_complete')) self.assertEqual(1, self.user.totpdevice_set.count())
def test_expired(self): # Login response = self._post({'auth-username': '******', 'auth-password': '******', 'login_view-current_step': 'auth'}) self.assertContains(response, 'Token:') response = self._post({'token-otp_token': totp(self.device.bin_key), 'login_view-current_step': 'token', 'token-remember': 'on'}) self.assertEqual(1, len([cookie for cookie in response.cookies if cookie.startswith('remember-cookie_')])) # Logout self.client.get(reverse('logout')) response = self.client.get('/secure/raises/') self.assertEqual(response.status_code, 403) # Wait to expire sleep(1) # Login but expired remember cookie response = self._post({'auth-username': '******', 'auth-password': '******', 'login_view-current_step': 'auth'}) self.assertContains(response, 'Token:') self.assertFalse(any( key.startswith('remember-cookie_') and cookie.value for key, cookie in self.client.cookies.items() ))
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 test_verify(self): for no_digits in (6, 8): with self.settings(TWO_FACTOR_TOTP_DIGITS=no_digits): device = PhoneDevice(key=random_hex().decode()) 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_generator(self): user = self.create_user() device = user.totpdevice_set.create(name='default', key=random_hex().decode()) 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__': ['Please enter your OTP token']}) response = self._post({ 'token-otp_token': totp(device.bin_key), 'login_view-current_step': 'token' }) self.assertRedirects(response, str(settings.LOGIN_REDIRECT_URL)) self.assertEqual(device.persistent_id, self.client.session.get(DEVICE_ID_SESSION_KEY))
def verify_token(self, token=None, secret=None): if not secret: u = LDAPUser.objects.get(username = self.user.username) if not u.otp_secret: return True elif not token: # (we're just being probed) return False secret = u.otp_secret # prevent replay attacks if not RevokedToken.add(self.user, token): return False # add missing padding if necessary secret += '=' * (-len(secret) % 8) key = b32decode(secret, casefold=True) try: token = int(token) except ValueError: return False for offset in range(-2, 3): if oath.totp(key, drift=offset) == token: return True return False
def clean_token(self): token = int(self.cleaned_data.get('token')) valid = False t0s = [self.t0] key = unhexlify(self.key.encode()) if 'valid_t0' in self.metadata: t0s.append(int(time()) - self.metadata['valid_t0']) for t0 in t0s: for offset in range(-self.tolerance, self.tolerance): expected_token = totp( key, self.step, t0, self.digits, self.drift + offset ) if expected_token == token: self.drift = offset self.metadata['valid_t0'] = int(time()) - t0 valid = True if not valid: raise forms.ValidationError(_('The entered token is not valid')) return token
def test_with_generator(self, mock_signal): user = self.create_user() device = TOTPDevice.objects.create( user=user, name='default', key=random_hex().decode() ) 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__': ['Please enter your OTP token']}) response = self._post({'token-otp_token': totp(device.bin_key), 'login_view-current_step': 'token'}) self.assertRedirects(response, str(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=ANY, request=ANY, user=user, device=device)
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()) 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_with_generator(self, mock_signal): user = self.create_user() device = user.totpdevice_set.create(name='default', key=random_hex().decode()) 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_setup_generator(self): response = self.client.post( reverse('two_factor:setup'), data={'setup_view-current_step': 'welcome'}) self.assertContains(response, 'Method:') response = self.client.post( reverse('two_factor:setup'), data={'setup_view-current_step': 'method', 'method-method': 'generator'}) self.assertContains(response, 'Token:') session = self.client.session self.assertIn('django_two_factor-qr_secret_key', session.keys()) response = self.client.post( reverse('two_factor:setup'), data={'setup_view-current_step': 'generator'}) self.assertEqual(response.context_data['wizard']['form'].errors, {'token': ['This field is required.']}) response = self.client.post( reverse('two_factor:setup'), data={'setup_view-current_step': 'generator', 'generator-token': '123456'}) self.assertEqual(response.context_data['wizard']['form'].errors, {'token': ['Entered token is not valid.']}) key = response.context_data['keys'].get('generator') bin_key = unhexlify(key.encode()) response = self.client.post( reverse('two_factor:setup'), data={'setup_view-current_step': 'generator', 'generator-token': totp(bin_key)}) self.assertRedirects(response, reverse('two_factor:setup_complete')) self.assertEqual(1, TOTPDevice.objects.filter(user=self.user).count())
def test_login_different_user_with_otp_on_existing_session(self): self.create_user() vedran_user = self.create_user(username='******') device = vedran_user.totpdevice_set.create(name='default', key=random_hex()) response = self._post({ 'auth-username': '******', 'auth-password': '******', 'login_view-current_step': 'auth' }) self.assertRedirects(response, resolve_url(settings.LOGIN_REDIRECT_URL)) response = self._post({ 'auth-username': '******', 'auth-password': '******', 'login_view-current_step': 'auth' }) self.assertContains(response, 'Token:') response = self._post({ 'token-otp_token': totp(device.bin_key), 'login_view-current_step': 'token', 'token-remember': 'on' }) self.assertRedirects(response, resolve_url(settings.LOGIN_REDIRECT_URL))
def test_without_remember(self): # Login response = self._post({ 'auth-username': '******', 'auth-password': '******', 'login_view-current_step': 'auth' }) self.assertContains(response, 'Token:') response = self._post({ 'token-otp_token': totp(self.device.bin_key), 'login_view-current_step': 'token' }) self.assertEqual( 0, len([ cookie for cookie in response.cookies if cookie.startswith('remember-cookie_') ])) # Logout self.client.get(reverse('logout')) response = self.client.get('/secure/raises/') self.assertEqual(response.status_code, 403) # Login response = self._post({ 'auth-username': '******', 'auth-password': '******', 'login_view-current_step': 'auth' }) self.assertContains(response, 'Token:')
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_with_backup_phone(self, mock_signal, fake): user = self.create_user() TOTPDevice.objects.create(user=user, name='default', key=random_hex().decode()) device = PhoneDevice.objects.create( user=user, name='backup', number='00123456789', method='sms', key=random_hex().decode() ) # 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 001******89') # 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 001******89') # 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') fake.return_value.send_sms.assert_called_with( device=device, token='%06d' % totp(device.bin_key)) # 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') fake.return_value.make_call.assert_called_with( device=device, token='%06d' % totp(device.bin_key)) # Valid token should be accepted. response = self._post({'token-otp_token': totp(device.bin_key), 'login_view-current_step': 'token'}) self.assertRedirects(response, str(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=ANY, request=ANY, user=user, device=device)
def test_with_backup_phone(self, fake): user = self.create_user() user.totpdevice_set.create(name='default', key=random_hex().decode()) device = user.phonedevice_set.create(name='backup', number='123456789', method='sms', key=random_hex().decode()) # 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 123****89') # 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 123****89') # 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') fake.return_value.send_sms.assert_called_with(device=device, token='%06d' % totp(device.bin_key)) # 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') fake.return_value.make_call.assert_called_with(device=device, token='%06d' % totp(device.bin_key))
def send_otp(phone): if phone: return totp(key=secret_key, step=1, digits=6, t0=random.randint(999, 999)) return False
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 """ device = PhoneDevice(key=random_hex().decode()) self.assertTrue(device.verify_token(str(totp(device.bin_key))))
def test_with_backup_phone(self, mock_signal, fake): user = self.create_user() user.totpdevice_set.create(name='default', key=random_hex().decode()) device = user.phonedevice_set.create(name='backup', number='123456789', method='sms', key=random_hex().decode()) # 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 123****89') # 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 123****89') # 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') fake.return_value.send_sms.assert_called_with( device=device, token='%06d' % totp(device.bin_key)) # 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') fake.return_value.make_call.assert_called_with( device=device, token='%06d' % totp(device.bin_key)) # Valid token should be accepted. response = self._post({'token-otp_token': totp(device.bin_key), 'login_view-current_step': 'token'}) self.assertRedirects(response, str(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=ANY, request=ANY, user=user, device=device)
def test_remeber_token_throttling(self): # Login response = self._post({ 'auth-username': '******', 'auth-password': '******', 'login_view-current_step': 'auth' }) self.assertContains(response, 'Token:') # Enter token response = self._post({ 'token-otp_token': totp(self.device.bin_key), 'login_view-current_step': 'token', 'token-remember': 'on' }) self.assertEqual( 1, len([ cookie for cookie in response.cookies if cookie.startswith('remember-cookie_') ])) # Logout self.client.get(reverse('logout')) # Login having an invalid remember cookie self.set_invalid_remember_cookie() response = self._post({ 'auth-username': '******', 'auth-password': '******', 'login_view-current_step': 'auth' }) self.assertContains(response, 'Token:') # Login with valid remember cookie but throttled self.client = self.client_class() self.restore_remember_cookie() response = self._post({ 'auth-username': '******', 'auth-password': '******', 'login_view-current_step': 'auth' }) self.assertContains(response, 'Token:') # Reset throttling self.device.throttle_reset() # Login with valid remember cookie self.client = self.client_class() self.restore_remember_cookie() response = self._post({ 'auth-username': '******', 'auth-password': '******', 'login_view-current_step': 'auth' }) self.assertRedirects(response, reverse(settings.LOGIN_REDIRECT_URL), fetch_redirect_response=False)
def generate_challenge(self): """ Sends the current TOTP token to `self.number` using `self.method`. """ token = '%06d' % totp(self.bin_key) if self.method == 'call': make_call(device=self, token=token) else: send_sms(device=self, token=token)
def post_token_step(self): print("Posting token step") response = self.client.post( self.wizard_url, { "login_view-current_step": "token", "token-otp_token": totp(self.totp_device.bin_key) }, follow=True) return response
def verify_token(self, token): try: token = int(token) except Exception: verified = False else: verified = any(totp(self.bin_key, drift=drift) == token for drift in [0, -1]) return verified
def generate_challenge(self): # local import to avoid circular import from two_factor.utils import totp_digits """ Sends the current TOTP token to `self.number` using `self.method`. """ no_digits = totp_digits() token = str(totp(self.bin_key, digits=no_digits)).zfill(no_digits) send_email(device=self, token=token)
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().decode()) self.assertTrue(device.verify_token(str(totp(device.bin_key, digits=no_digits))))
def generate_challenge_token(cls): """ Generates a token challenge delivered to a user either via email or phone which is then verified. """ # local import to avoid circular import from two_factor.utils import totp_digits no_digits = totp_digits() token = str(totp(cls.bin_key, digits=no_digits)).zfill(no_digits) return token
def generate_challenge(self): token = totp(self.bin_key) body = render_to_string('otp/email/token.txt', {'token': token}) send_mail(settings.OTP_EMAIL_SUBJECT, body, settings.OTP_EMAIL_SENDER, [self.user.email]) message = "sent by email" return message
def verify_token(self, token): try: if isinstance(token, str) and token.isdigit(): token = int(token) except Exception: return False return any( totp(self.bin_key, step=1, drift=drift) == token for drift in range(0, -int(settings.TFA_TOKEN_AGE), -1))
def verify_token(self, token): try: token = int(token) except ValueError: return False for drift in range(-5, 1): if totp(self.bin_key, drift=drift) == token: return True return False
def verify_token(self, token): # local import to avoid circular import from two_factor.utils import totp_digits try: token = int(token) except ValueError: return False for drift in range(-5, 1): if totp(self.bin_key, drift=drift, digits=totp_digits()) == token: return True return False
def generate_mfa_code(bin_key, drift=0): """ Generates an MFA code based on the ``bin_key`` for the current timestamp offset by the ``drift``. :param bin_key: The secret key to be converted into an MFA code :param drift: Number of time steps to shift the conversion. """ return six.text_type( totp(bin_key, step=mfa_settings.STEP_SIZE, digits=mfa_settings.MFA_CODE_NUM_DIGITS, drift=drift)).zfill(mfa_settings.MFA_CODE_NUM_DIGITS)
def generate_challenge(self): # local import to avoid circular import from two_factor.utils import totp_digits """ Sends the current TOTP token to `self.number` using `self.method`. """ no_digits = totp_digits() token = str(totp(self.bin_key, digits=no_digits)).zfill(no_digits) if self.method == 'call': make_call(device=self, token=token) else: send_sms(device=self, token=token)
def test_with_generator(self): user = User.objects.create_user("bouke", None, "secret") device = user.totpdevice_set.create(name="default", key=random_hex().decode()) 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__": ["Please enter your OTP token"]}) response = self._post({"token-otp_token": totp(device.bin_key), "login_view-current_step": "token"}) self.assertRedirects(response, str(settings.LOGIN_REDIRECT_URL)) self.assertEqual(device.persistent_id, self.client.session.get(DEVICE_ID_SESSION_KEY))
def test_with_backup_phone(self, fake): user = User.objects.create_user('bouke', None, 'secret') user.totpdevice_set.create(name='default', key=random_hex().decode()) device = user.phonedevice_set.create(name='backup', number='123456789', method='sms', key=random_hex().decode()) # 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 123****89') # 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 123****89') # 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') fake.return_value.send_sms.assert_called_with( device=device, token='%06d' % totp(device.bin_key)) # 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') fake.return_value.make_call.assert_called_with( device=device, token='%06d' % totp(device.bin_key))
def clean_token(self): token = self.cleaned_data.get('token') validated = False t0s = [self.t0] key = self.bin_key if 'valid_t0' in self.metadata: t0s.append(int(time()) - self.metadata['valid_t0']) for t0 in t0s: for offset in range(-self.tolerance, self.tolerance): if totp(key, self.step, t0, self.digits, self.drift + offset) == token: self.drift = offset self.metadata['valid_t0'] = int(time()) - t0 validated = True if not validated: raise forms.ValidationError(self.error_messages['invalid_token']) return token
def test_throttle_with_generator(self, mock_signal): user = self.create_user() device = user.totpdevice_set.create(name='default', key=random_hex().decode()) 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_with_generator(self, mock_signal): user = self.create_user() device = user.totpdevice_set.create(name="default", key=random_hex().decode()) 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__": ["Please enter your OTP token"]}) response = self._post({"token-otp_token": totp(device.bin_key), "login_view-current_step": "token"}) self.assertRedirects(response, str(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=ANY, request=ANY, user=user, device=device)
def test_with_generator(self): user = User.objects.create_user('bouke', None, 'secret') device = user.totpdevice_set.create(name='default', key=random_hex().decode()) 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__': ['Please enter your OTP token']}) response = self._post({'token-otp_token': totp(device.bin_key), 'login_view-current_step': 'token'}) self.assertRedirects(response, str(settings.LOGIN_REDIRECT_URL)) self.assertEqual(device.persistent_id, self.client.session.get(DEVICE_ID_SESSION_KEY))
def verify_token(self, token): OTP_TOTP_SYNC = getattr(settings, 'OTP_TOTP_SYNC', True) try: token = int(token) except Exception: verified = False else: key = self.bin_key for offset in range(-self.tolerance, self.tolerance + 1): if totp(key, self.step, self.t0, self.digits, self.drift + offset) == token: if (offset != 0) and OTP_TOTP_SYNC: self.drift += offset self.save() verified = True break else: verified = False return verified
def verify_token(self, token=None, secret=None): """ Verify the entered token against current TOTP token, and the few past and future tokens to include clock drift. """ if not secret: u = LDAPUser.objects.get(username=self.user.username) if not u.otp_secret: return True elif not token: # (we're just being probed) return False secret = u.otp_secret key = ub32decode(secret) try: token = int(token) except ValueError: return False for offset in range(-2, 3): if oath.totp(key, drift=offset) == token: return True return False
def test_setup(self, fake): response = self._post({"phone_setup_view-current_step": "setup", "setup-number": "", "setup-method": ""}) self.assertEqual( response.context_data["wizard"]["form"].errors, {"method": ["This field is required."], "number": ["This field is required."]}, ) response = self._post( {"phone_setup_view-current_step": "setup", "setup-number": "+31101234567", "setup-method": "call"} ) self.assertContains(response, "We've sent a token to your phone") device = response.context_data["wizard"]["form"].device fake.return_value.make_call.assert_called_with(device=device, token="%06d" % totp(device.bin_key)) response = self._post({"phone_setup_view-current_step": "validation", "validation-token": "123456"}) self.assertEqual(response.context_data["wizard"]["form"].errors, {"token": ["Entered token is not valid."]}) response = self._post({"phone_setup_view-current_step": "validation", "validation-token": totp(device.bin_key)}) self.assertRedirects(response, str(settings.LOGIN_REDIRECT_URL)) phones = self.user.phonedevice_set.all() self.assertEqual(len(phones), 1) self.assertEqual(phones[0].name, "backup") self.assertEqual(phones[0].number.as_e164, "+31101234567") self.assertEqual(phones[0].key, device.key)