def test_plugin(): plugin = registry.get_plugin('u2f') assert plugin.get_create_form_class() == U2fDeviceCreateForm assert plugin.get_verify_form_class() == U2fVerifyForm user = UserFactory() # When a user logs in the OTP device is added as a property. user.otp_device = U2fDeviceFactory(user=user) assert registry.user_authentication_method(user) == 'u2f'
def test_recovery(self): user = UserFactory() response = self.login(user) self.assertContains(response, 'These are your 2 step authentication methods.') # Create recovery codes to secure the account. response = self.client.post('/recovery-code/create/', follow=True) self.assertContains(response, 'Your recovery codes have been generated') # Login with a recovery code. self.client.logout() device = user.staticdevice_set.get() self.assertEqual(device.token_set.count(), 10) device_url = '/recovery-code/verify/{}/'.format(device.pk) response = self.login(user, '{}?next=/list/'.format(device_url)) context_user = response.context['user'] self.assertTrue(context_user.is_anonymous) # Try a faulty code with a unicode BOM. response = self.client.post(device_url, {'otp_token': '\ufeffxxx'}, follow=True) self.assertEqual(response.status_code, 200) self.assertContains(response, 'The token is not valid for this device.') # Continue authentication using recovery code. token = device.token_set.first() response = self.client.post(device_url, {'otp_token': token.token}, follow=True) self.assertRedirects(response, '/list/') self.assertEqual(device.token_set.count(), 9) context_user = response.context['user'] self.assertTrue(context_user.is_authenticated) self.assertTrue(context_user.is_verified) # Verify that the authentication method matches. self.assertEqual(registry.user_authentication_method(context_user), 'recovery-code') # The create and update form for recovery codes replace existing codes. self.client.post('/recovery-code/update/{}/'.format(device.pk)) self.assertEqual(device.token_set.count(), 10) # Secure access is revoked when all authentication methods are removed. response = self.client.post('/recovery-code/delete/{}/'.format( device.pk), follow=True) self.assertRedirects(response, '/list/') context_user = response.context['user'] self.assertTrue(context_user.is_single_factor_authenticated) self.assertFalse(context_user.is_authenticated) self.assertFalse(context_user.is_verified)
def test_totp(self): user = UserFactory() self.login(user) response = self.client.post('/totp/create/', { 'otp_token': 'XXX', 'name': 'My Phone' }) self.assertContains(response, 'Unable to validate the token with the device.') response = self.client.post('/totp/create/', { 'otp_token': '123456', 'name': 'My Phone' }) self.assertContains(response, 'Unable to validate the token with the device.') # User takes the TOTP config and applies it to his device. response = self.client.get('/totp/create/') totp = self.totp_from_device(response.context['form'].instance) # Confirm the user can generate tokens and the device is configured. registration_token = totp.token() with handle_signal(mfa_added) as handler: response = self.client.post('/totp/create/', { 'otp_token': registration_token, 'name': 'My Phone' }) handler.assert_called_once_with(instance=mock.ANY, sender=mock.ANY, request=mock.ANY, signal=mfa_added) self.assertRedirects(response, '/list/') device = user.totpdevice_set.get() self.assertTrue(device.confirmed) self.assertEqual(device.name, 'My Phone') response = self.client.post('/totp/update/{}/'.format(device.pk), {'name': 'Acme ID'}) self.assertRedirects(response, '/list/') device = user.totpdevice_set.get() self.assertEqual(device.name, 'Acme ID') self.client.logout() # Login without TOTP. verify_url = '/totp/verify/{}/'.format(device.pk) response = self.login(user, '{}?next=/list/'.format(verify_url)) context_user = response.context['user'] self.assertTrue(context_user.is_anonymous) # User is forced to use 2 step authentication. response = self.client.get('/list/') self.assertRedirects(response, '/login/?next=/list/') response = self.client.post('/totp/delete/{}/'.format(device.pk)) self.assertRedirects(response, '/login/?next=/totp/delete/{}/'.format(device.pk)) # Token reuse is not allowed so the registration is no longer valid. with handle_signal(user_login_failed) as handler: response = self.client.post(verify_url, {'otp_token': registration_token}) handler.assert_called_once_with(credentials={}, user=user, device=device, sender=mock.ANY, request=mock.ANY, signal=user_login_failed) self.assertContains(response, 'The token is not valid for this device.') # Default tolerance is 1, increase drift to force next token. totp.drift = 1 with handle_signal(user_logged_in) as handler: response = self.client.post(verify_url, {'otp_token': totp.token()}, follow=True) handler.assert_called_once_with(user=user, sender=mock.ANY, request=mock.ANY, signal=user_logged_in) self.assertRedirects(response, '/list/') context_user = response.context['user'] self.assertTrue(context_user.is_authenticated) self.assertTrue(context_user.is_verified) # Verify that the authentication method matches. self.assertEqual(registry.user_authentication_method(context_user), 'totp') with handle_signal(mfa_removed) as handler: response = self.client.post('/totp/delete/{}/'.format(device.pk), follow=True) handler.assert_called_once_with(instance=device, sender=mock.ANY, request=mock.ANY, signal=mfa_removed) self.assertContains( response, 'The TOTP "Acme ID" was deleted successfully.')
def test_yubikey(self, mock_verify_token): user = UserFactory() self.login(user) # Test bad/missing input. mock_verify_token.return_value = False service = ValidationService.objects.latest('pk') response = self.client.post( '/yubikey/create/', {'name': 'Keychain'}) self.assertContains( response, 'This field is required.') self.assertTrue( response.context['form'].fields['service'].widget.is_hidden) # Test field visibility with multiple services. ValidationService.objects.create( name='YubiCustom', param_sl='', param_timeout='') response = self.client.post( '/yubikey/create/', { 'service': service.pk, 'otp_token': 'XXX', 'name': 'Keychain'}) self.assertContains( response, 'Unable to validate the token with the device.') self.assertFalse( response.context['form'].fields['service'].widget.is_hidden) # Test success. mock_verify_token.return_value = True yubikey = YubiKey(unhexlify(default_id()), 6, 0) registration_token = yubikey.generate() response = self.client.post( '/yubikey/create/', { 'service': service.pk, 'otp_token': registration_token, 'name': 'Keychain'}) self.assertRedirects(response, '/list/') device = user.remoteyubikeydevice_set.get() self.assertTrue(device.confirmed) self.assertEqual(device.name, 'Keychain') # Test update. response = self.client.post( '/yubikey/update/{}/'.format(device.pk), {'name': 'Acme Inc.'}) self.assertRedirects(response, '/list/') device = user.remoteyubikeydevice_set.get() self.assertEqual(device.name, 'Acme Inc.') self.client.logout() # Login without Yubikey. verify_url = '/yubikey/verify/{}/'.format(device.pk) response = self.login(user, '{}?next=/list/'.format(verify_url)) context_user = response.context['user'] self.assertTrue(context_user.is_anonymous) # User is forced to use 2 step authentication. mock_verify_token.return_value = False response = self.client.get('/list/') self.assertRedirects(response, '/login/?next=/list/') response = self.client.post('/yubikey/delete/{}/'.format(device.pk)) self.assertRedirects( response, '/login/?next=/yubikey/delete/{}/'.format(device.pk)) response = self.client.post(verify_url, {'otp_token': ''}) self.assertContains( response, 'This field is required.') # Token reuse is not allowed so now the registration token is invalid. response = self.client.post( verify_url, {'otp_token': registration_token}) self.assertContains( response, 'The token is not valid for this device.') # Complete authentication with a new token. mock_verify_token.return_value = True response = self.client.post( verify_url, {'otp_token': yubikey.generate()}, follow=True) self.assertRedirects(response, '/list/') context_user = response.context['user'] self.assertTrue(context_user.is_authenticated) self.assertTrue(context_user.is_verified) # Verify that the authentication method matches. self.assertEqual( registry.user_authentication_method(context_user), 'yubikey') response = self.client.post( '/yubikey/delete/{}/'.format(device.pk), follow=True) self.assertContains( response, 'The Yubikey "Acme Inc." was deleted successfully.')
def test_initial_account_setup(self): # With single factor authentication a user can see the empty # list of available authentication methods as well as add a device. # The user cannot do anything else without 2 step authentication. user = UserFactory() with handle_signal(user_logged_in) as handler: response = self.login(user) handler.assert_called_once_with(user=user, sender=mock.ANY, request=mock.ANY, signal=user_logged_in) authenticated_user = handler.call_args[1]['user'] self.assertIsNone( registry.user_authentication_method(authenticated_user)) self.assertContains(response, 'These are your 2 step authentication methods.') self.assertFalse(registry.user_has_device(user)) context_user = response.context['user'] self.assertTrue(context_user.is_single_factor_authenticated) self.assertFalse(context_user.is_authenticated) self.assertFalse(context_user.is_verified) # User is allowed to add a authentication method. response = self.client.get('/totp/create/') self.assertEqual(response.status_code, 200) # Force 2 step login setup. with handle_signal(user_logged_in) as handler: self.login_with_mfa(user) handler.assert_called_once_with(user=user, sender=mock.ANY, request=mock.ANY, signal=user_logged_in) authenticated_user = handler.call_args[1]['user'] self.assertEqual( registry.user_authentication_method(authenticated_user), 'totp') response = self.client.get('/list/') context_user = response.context['user'] self.assertTrue(context_user.is_single_factor_authenticated) self.assertTrue(context_user.is_authenticated) self.assertTrue(context_user.is_verified) # Single factor authentication no longer authenticates the user. self.client.logout() device = user.totpdevice_set.get(name='test') response = self.login(user, '/totp/verify/{}/?next=/list/'.format(device.pk)) context_user = response.context['user'] self.assertTrue(context_user.is_anonymous) # No access to the device listing with single factor auth. response = self.client.get('/list/') self.assertRedirects(response, '/login/?next=/list/') response = self.client.get('/totp/create/') self.assertRedirects(response, '/login/?next=/totp/create/') # Start new MFA session. self.login_with_mfa(user) update_url = '/totp/update/{}/'.format(device.pk) response = self.client.get(update_url) self.assertEqual(response.status_code, 200) context_user = response.context['user'] self.assertTrue(context_user.is_single_factor_authenticated) self.assertTrue(context_user.is_authenticated) self.assertTrue(context_user.is_verified) # Update TOTP name. response = self.client.post('/totp/update/{}/'.format(device.pk), {'name': 'Secure Phone'}, follow=True) self.assertRedirects(response, '/list/') self.assertContains( response, 'The TOTP "Secure Phone" was changed successfully.') # Secure access is revoked when all authentication methods are removed. response = self.client.post('/totp/delete/{}/'.format(device.pk), follow=True) self.assertRedirects(response, '/list/') self.assertContains( response, 'The TOTP "Secure Phone" was deleted successfully.') context_user = response.context['user'] self.assertTrue(context_user.is_single_factor_authenticated) self.assertFalse(context_user.is_authenticated) self.assertFalse(context_user.is_verified)