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)
Example #3
0
    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)
Example #9
0
    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)
Example #10
0
    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())
Example #11
0
    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)
Example #12
0
    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))
Example #17
0
    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
Example #18
0
    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_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, 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))
Example #29
0
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)
Example #32
0
 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_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
Example #36
0
    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
Example #37
0
 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)
Example #38
0
    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
Example #39
0
 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))))
Example #41
0
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
Example #42
0
    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
Example #43
0
    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 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))))
Example #46
0
    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
Example #47
0
    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
Example #48
0
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)
Example #49
0
    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)
Example #50
0
    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 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))
Example #52
0
    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))
Example #53
0
 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.']})
Example #55
0
    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)
Example #56
0
    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))
Example #57
0
    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
Example #58
0
    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
Example #59
0
    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)