def test_lock_delete_alread_deleted(tmp_lock_dir): lock = PasswordResetLock("dummy") try: lock.delete() except FileNotFoundError: assert False, "delete() crashes on absent files" assert lock.valid_until() is None
def forgot_password_ask(): form = ForgottenPasswordForm() if form.validate_on_submit(): username = form.username.data lock = PasswordResetLock(username) valid_until = lock.valid_until() now = datetime.datetime.now() with handle_form_errors(form): if valid_until is not None and now < valid_until: wait_min = int((valid_until - now).total_seconds() / 60) wait_sec = int((valid_until - now).total_seconds() % 60) raise FormError( "non_field_errors", _( 'You have already requested a password reset, you need to wait ' '%(wait_min)s minute(s) and %(wait_sec)s seconds before you can request ' 'another.', wait_min=wait_min, wait_sec=wait_sec, ), ) try: user = User(ipa_admin.user_show(username)) except python_freeipa.exceptions.NotFound: raise FormError( "username", _("User %(username)s does not exist", username=username)) token = PasswordResetToken.from_user(user).as_string() # Send the email email_context = {"token": token, "username": username} email = Message( body=render_template("forgot-password-email.txt", **email_context), html=render_template("forgot-password-email.html", **email_context), recipients=[user.mail], subject="Password reset procedure", ) try: mailer.send(email) except ConnectionRefusedError as e: app.logger.error( f"Impossible to send a password reset email: {e}") flash(_("We could not send you an email, please retry later"), "danger") return redirect(url_for('root')) if app.config["DEBUG"]: # pragma: no cover app.logger.debug(email) lock.store() app.logger.info( f'{username} forgot their password and requested a token') flash( _('An email has been sent to your address with instructions on how to reset ' 'your password'), "success", ) return redirect(url_for('root')) return render_template('forgot-password-ask.html', form=form)
def forgot_password_change(): token = request.args.get('token') if not token: flash('No token provided, please request one.', 'warning') return redirect(url_for('forgot_password_ask')) try: token_obj = PasswordResetToken.from_string(token) except jwt.exceptions.DecodeError: flash(_("The token is invalid, please request a new one."), "warning") return redirect(url_for('forgot_password_ask')) username = token_obj.username lock = PasswordResetLock(username) valid_until = lock.valid_until() now = datetime.datetime.now() if valid_until is None or now > valid_until: lock.delete() flash(_("The token has expired, please request a new one."), "warning") return redirect(url_for('forgot_password_ask')) user = User(ipa_admin.user_show(username)) if not token_obj.validate_last_change(user): lock.delete() flash( _("Your password has been changed since you requested this token, please request " "a new one."), "warning", ) return redirect(url_for('forgot_password_ask')) form = NewPasswordForm() if form.validate_on_submit(): password = form.password.data # Generate a random temporary number. temp_password = ''.join( random.choices(string.ascii_letters + string.digits, k=24)) try: # Force change password to the random password, so that the password is not actually # changed to the given one in case the next step fails (because the OTP is wrong for # example) ipa_admin.user_mod(username, userpassword=temp_password) # Change the password as the user, so it's not expired. ipa = untouched_ipa_client(app) ipa.change_password( username, new_password=password, old_password=temp_password, otp=form.otp.data, ) except python_freeipa.exceptions.PWChangePolicyError as e: lock.delete() flash( _( 'Your password has been changed, but it does not comply with the policy ' '(%(policy_error)s) and has thus been set as expired. You will be asked to ' 'change it after logging in.', policy_error=e.policy_error, ), 'warning', ) app.logger.info( f"Password for {username} was changed to a non-compliant password after " f"completing the forgotten password process.") # Send them to the login page, they will have to change their password # after login. return redirect(url_for('root')) except python_freeipa.exceptions.PWChangeInvalidPassword: # The provided OTP was wrong app.logger.info( f"Password for {username} was changed to a random string because " f"the OTP token they provided was wrong.") # Oh noes, the token is now invalid since the user's password was changed! Let's # re-generate a token so they can keep going. user = User(ipa_admin.user_show(username)) token = PasswordResetToken.from_user(user).as_string() form.otp.errors.append(_("Incorrect value.")) except python_freeipa.exceptions.FreeIPAError as e: # If we made it here, we hit something weird not caught above. app.logger.error( f'An unhandled error {e.__class__.__name__} happened while reseting ' f'the password for user {username}: {e.message}') form.errors['non_field_errors'] = [ _('Could not change password, please try again.') ] else: lock.delete() flash(_('Your password has been changed.'), 'success') app.logger.info( f"Password for {username} was changed after completing the forgotten " f"password process.") return redirect(url_for('root')) return render_template('forgot-password-change.html', username=username, form=form, token=token)
def test_lock_delete(tmp_lock_dir): lock = PasswordResetLock("dummy") lock.store() lock.delete() assert lock.valid_until() is None
def test_lock_valid_until(tmp_lock_dir): lock = PasswordResetLock("dummy") lock.store() assert lock.valid_until() is not None now = datetime.datetime.now() assert lock.valid_until() > now
def test_lock_store(tmp_lock_dir): lock = PasswordResetLock("dummy") lock.store() assert tmp_lock_dir.joinpath("dummy").exists()