def post(self, request: HttpRequest): """ Check the Code """ form = TestYourAppOtpForm(request.POST) if not form.is_valid(): return self._send_template(request, form) chosen_otp = form.cleaned_data['otp_to_be_tested'] success = chosen_otp in get_possible_otps(self.secret) list_of_otps = get_possible_otps(self.secret, -10, 10) return self._send_template(request, form, True, chosen_otp, list_of_otps, success)
def post(self, request: HttpRequest): """ Set the new secret """ form = NewOtpSecretForm(request.POST) if not form.is_valid(): return self._send_otp_form(request, form) secret = request.session.get('encrypted_new_totp_secret', None) if secret is None: form.add_error( None, _('No Secret set. Please create a new secret first.')) return self._send_otp_form(request, form) secret = SymmetricCrypt().decrypt(b64decode(secret.encode('us-ascii'))) if form.cleaned_data['otp_confirm'] not in get_possible_otps(secret): form.add_error('otp_confirm', _('One-time password invalid')) return self._send_otp_form(request, form) del request.session['encrypted_new_totp_secret'] try: user = HubUser.objects.get(id=request.user.id) user.set_totp_secret(secret) user.save() except DatabaseError: # pragma: no cover # Database Safeguard form.add_error( None, _('We are currently unable to update your OTP Secret. Please try again later.' )) return self._send_otp_form(request, form) return self._send_otp_form(request, success=True)
def test_inactive_user(self): """ User is not allowed to login """ self.assertIsNone( TotpAuthenticationBackend().authenticate( # nosec self.request_factory.get('/'), username='******', password='******', one_time_pw=get_possible_otps(b'SUPERSECRETSUPER-SUPERSECRETSUPER', 0, 0)[0] ) )
def test_all_good(self): """ Everything is correct """ self.assertEqual( self.user, TotpAuthenticationBackend().authenticate( # nosec self.request_factory.get('/'), username='******', password='******', one_time_pw=get_possible_otps(b'SUPERSECRETSUPER-SUPERSECRETSUPER', 0, 0)[0] ) )
def post(self, request: HttpRequest, recovery: UUID): """ Set the new secret """ encrypted_secret = request.session.get( 'encrypted_temporary_otp_secret', None) if encrypted_secret is None: # pragma: no cover # Safeguard for tinkered session return deny_step(request, 'EA10') auth_form = ForgottenCredentialsStep3BaseForm(request.POST) if not auth_form.is_valid( ): # pragma: no cover # Safeguard Client Manipulation return deny_step(request, 'EA11') new_secret = SymmetricCrypt().decrypt(b64decode(encrypted_secret)) recovery_str = str(recovery) try: auth = Signer(salt=recovery_str).unsign( auth_form.cleaned_data['auth']) recovery_data = PendingCredentialRecovery.objects.filter( user__is_active=True, valid_until__gte=now(), recovery_type='otp-secret').get(uuid=recovery, key=auth) user = recovery_data.user except PendingCredentialRecovery.DoesNotExist: # pragma: no cover # Database Safeguard return deny_step(request, 'EA12') except (ValueError, BadSignature): # pragma: no cover # Manipulation Safeguard return deny_step(request, 'EA13') form = ForgottenCredentialsStep3ConfirmOtpForm(request.POST) if not form.is_valid(): return self.show_form(request, new_secret, user.username, recovery_str, form) if form.cleaned_data['otp'] not in get_possible_otps(new_secret): form.add_error('otp', _('This one time password is not valid.')) return self.show_form(request, new_secret, user.username, recovery_str, form) with transaction.atomic(): tx_id = transaction.savepoint() user.set_totp_secret(new_secret) user.save() recovery_data.delete() transaction.savepoint_commit(tx_id) logout(request) messages.add_message( request, messages.SUCCESS, _('Your new OTP secret has been set. You can login now.')) return redirect(reverse_lazy('ha:auth:login'))
def authenticate(self, request: HttpRequest, username=None, password=None, one_time_pw=None) -> Optional[HubUser]: # pylint: disable=arguments-differ random = SystemRandom() for i in range(random.randrange(1, 5)): # nosec HubUser().set_password('against-timing-attack' + str(i)) # Mitigation against timing attack if username is None or password is None or one_time_pw is None: return None if any([ len(username) < 1, len(username) > 150, len(password) < 1, len(password) > 1000, len(one_time_pw) != 6 ]): return None c_user = TotpAuthenticationBackend.clean_username(username) user_object = super(TotpAuthenticationBackend, self).authenticate(request, c_user, password) # type: HubUser if user_object is None: return None if user_object.totp_secret is None: return None possible_tokens = get_possible_otps(SymmetricCrypt().decrypt( user_object.totp_secret)) if one_time_pw not in possible_tokens: return None current_time = now() starting_at = current_time - datetime.timedelta(hours=1) try: BurnedOtp.objects.filter( user=user_object, burned_timestamp__gte=starting_at).get(token=one_time_pw) return None except BurnedOtp.DoesNotExist: BurnedOtp.objects.create(user=user_object, token=one_time_pw) return user_object
def test_token_generator(self): """ Test token generation """ test_items = ( (b'12345678123456781234567812345678', -3, 3), (b'12345678123456781234567812345678', -1, 1), (b'12345678123456781234567812345678', -0, 0), (b'1234567812345678123456781234567812345678123456781234567812345678', -3, 3), (b'1234567812345678123456781234567812345678123456781234567812345678', -1, 1), (b'1234567812345678123456781234567812345678123456781234567812345678', -0, 0), (b'123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678', -3, 3), (b'123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678', -1, 1), (b'123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678', -0, 0), (bytes(SystemRandom().getrandbits(8) for _ in range(32)), -3, 3), (bytes(SystemRandom().getrandbits(8) for _ in range(32)), -1, 1), (bytes(SystemRandom().getrandbits(8) for _ in range(32)), -0, 0), (bytes(SystemRandom().getrandbits(8) for _ in range(64)), -3, 3), (bytes(SystemRandom().getrandbits(8) for _ in range(64)), -1, 1), (bytes(SystemRandom().getrandbits(8) for _ in range(64)), -0, 0), (bytes(SystemRandom().getrandbits(8) for _ in range(96)), -3, 3), (bytes(SystemRandom().getrandbits(8) for _ in range(96)), -1, 1), (bytes(SystemRandom().getrandbits(8) for _ in range(96)), -0, 0), ) for secret, offset_lower, offset_upper in test_items: with self.subTest( 'Testing with a secret of "{}", a lower offset of {} and an upper offset of {}' .format(secret, offset_lower, offset_upper)): tokens = get_possible_otps(secret, offset_lower, offset_upper) self.assertIsNotNone(tokens) expected_count = abs(offset_upper) + abs(offset_lower) + 1 self.assertEqual(expected_count, len(tokens)) for token in tokens: self.assertRegex(token, r'^\d{6}$')
def test_only_one_part_wrong(self): """ Testing with correct OTP, but one wrong other part """ test_items = ( ('mr_right', 'wrong_pass'), ('mr_wrong', 'right_pass'), ('', 'right_pass'), ('mr_right', ''), (None, 'right_pass'), ('mr_right', None), ) for username, password in test_items: with self.subTest(msg='Testing with user "{}" and password "{}"'.format(username, password)): self.assertIsNone( TotpAuthenticationBackend().authenticate( self.request_factory.get('/'), username=username, password=password, one_time_pw=get_possible_otps(b'SUPERSECRETSUPER-SUPERSECRETSUPER', 0, 0)[0] ) )
def test_separate_use_of_otp(self): """ Test that logging in with independent OTPs works """ tokens = get_possible_otps(b'SUPERSECRETSUPER-SUPERSECRETSUPER', 0, 1) self.assertEqual( self.user, TotpAuthenticationBackend().authenticate( # nosec self.request_factory.get('/'), username='******', password='******', one_time_pw=tokens[0] ) ) self.assertEqual( self.user, TotpAuthenticationBackend().authenticate( # nosec self.request_factory.get('/'), username='******', password='******', one_time_pw=tokens[1] ) )
def test_double_use_of_otp(self): """ What happens, when a otp is used twice? """ otp = get_possible_otps(b'SUPERSECRETSUPER-SUPERSECRETSUPER', 0, 0)[0] self.assertEqual( self.user, TotpAuthenticationBackend().authenticate( # nosec self.request_factory.get('/'), username='******', password='******', one_time_pw=otp ) ) self.assertEqual( None, TotpAuthenticationBackend().authenticate( # nosec self.request_factory.get('/'), username='******', password='******', one_time_pw=otp ) )
def post(self, request): """ Handle the POST request """ form = RegistrationStep2Form(request.POST) if not form.is_valid(): return self._send_form(request, form) additional_errors = False if form.cleaned_data['password1'] != form.cleaned_data['password2']: additional_errors = True form.add_error('password2', _('The passwords do not match')) if any([ 'registration_step2_user_id' not in request.session, 'registration_step2_username' not in request.session, 'registration_step2_totp' not in request.session, ]): return render(request, self.bad_link_template_name, {}, content_type=self.content_type) user = None try: user = HubUser.objects.filter( is_active=True, totp_secret__isnull=False).get( id=request.session['registration_step2_user_id']) validate_password(password=form.cleaned_data['password1'], user=user) except HubUser.DoesNotExist: # pragma: no cover # Simple safeguard for a User deletion in-between return render(request, self.bad_link_template_name, {}, content_type=self.content_type) except ValidationError as error: additional_errors = True form.add_error('password1', error) if additional_errors: form.add_error( 'otp', _('Your one time password may be expired, be sure to provide a current one.' )) return self._send_form(request, form) # Check the OTP otp = form.cleaned_data['otp'] if otp not in get_possible_otps( b64decode(request.session['registration_step2_totp'].encode( 'ascii'))): form.add_error('otp', _('Your one time password has expired')) form.add_error('password1', _('Please re-enter your password')) return self._send_form(request, form) # Put it into the database with transaction.atomic(): tx_id = transaction.savepoint() try: pending = PendingRegistration.objects.filter( valid_until__gte=now(), user__is_active=True, user__totp_secret__isnull=False, user=user).select_related('user').get( uuid=form.cleaned_data['reg'] ) # type: PendingRegistration except PendingRegistration.DoesNotExist: # pragma: no cover # Safeguard for deletion in-between transaction.savepoint_rollback(tx_id) return render(request, self.bad_link_template_name, {}, content_type=self.content_type) signer = Signer(salt=str(form.cleaned_data['reg'])) try: key = signer.unsign(form.cleaned_data['key']) except (BadSignature, ValueError ): # pragma: no cover # Just a signature safeguard transaction.savepoint_rollback(tx_id) return render(request, self.bad_link_template_name, {}, content_type=self.content_type) if key != pending.key: # pragma: no cover # Double-safeguard, unreachable without compromised secret key transaction.savepoint_rollback(tx_id) return render(request, self.bad_link_template_name, {}, content_type=self.content_type) pending.delete() user.set_password(form.cleaned_data['password1']) user.save() transaction.savepoint_commit(tx_id) logout(request) return render(request, self.success_template_name, {}, content_type=self.content_type)
def verify_otp(user: HubUser, otp: str): """ Verify that the OTP matches the user """ return otp in get_possible_otps(user.get_totp_secret())