def _send_validation_email(user): token = make_token( { "sub": user.username, "mail": user.mail }, audience=Audience.email_validation, ttl=current_app.config["ACTIVATION_TOKEN_EXPIRATION"], ) email_context = {"token": token, "user": user} email = Message( body=render_template("email-validation.txt", **email_context), html=render_template("email-validation.html", **email_context), recipients=[user.mail], subject=_("Verify your email address"), ) if current_app.config["DEBUG"]: # pragma: no cover current_app.logger.debug(email) try: mailer.send(email) except ConnectionRefusedError as e: current_app.logger.error( f"Impossible to send an address validation email: {e}") flash( _("We could not send you the address validation email, please retry later" ), "danger", )
def test_spamcheck(client, dummy_stageuser, mocker, spamcheck_status, spamcheck_on): user = User(ipa_admin.stageuser_show("dummy")["result"]) assert user.status_note != spamcheck_status token = make_token({"sub": "dummy"}, audience=Audience.spam_check) with mailer.record_messages() as outbox: response = client.post( "/register/spamcheck-hook", json={ "token": token, "status": spamcheck_status }, ) assert response.status_code == 200 assert response.json == {"status": "success"} # Check that the status was changed user = User(ipa_admin.stageuser_show("dummy")["result"]) assert user.status_note == spamcheck_status # Sent email if spamcheck_status == "active": assert len(outbox) == 1 message = outbox[0] assert message.subject == "Verify your email address" assert message.recipients == ["*****@*****.**"] else: assert len(outbox) == 0
def request_basset_check(sender, **kwargs): user = sender basset_url = current_app.config.get("BASSET_URL") if not basset_url: return token = make_token( {"sub": user.username}, audience=Audience.spam_check, ttl=current_app.config["SPAMCHECK_TOKEN_EXPIRATION"], ) user_dict = user.as_dict() user_dict["email"] = user_dict["mail"] user_dict["human_name"] = user_dict["commonname"] response = requests.post( basset_url, json={ "action": "fedora.noggin.registration", "time": int(time.time()), "data": { "user": user_dict, "request_headers": dict(request.headers), "request_ip": request.remote_addr, "token": token, "callback": url_for('.spamcheck_hook', _external=True), }, }, ) if not response.ok: current_app.logger.warning( "Error requesting a Basset check: " f"{response.status_code} {response.reason}: {response.text}")
def token_for_dummy_user(dummy_stageuser): return make_token( { "sub": dummy_stageuser.username, "mail": dummy_stageuser.mail }, audience=Audience.email_validation, ttl=current_app.config["ACTIVATION_TOKEN_EXPIRATION"], )
def token_for_dummy_user(dummy_user): user = User(ipa_admin.user_show("dummy")["result"]) return make_token( { "sub": user.username, "lpc": user.last_password_change }, audience=Audience.password_reset, )
def test_spamcheck_wrong_status(client, dummy_user, mocker, spamcheck_on): token = make_token({"sub": "dummy"}, audience=Audience.spam_check) response = client.post( "/register/spamcheck-hook", json={ "token": token, "status": "this-is-wrong" }, ) assert response.status_code == 400 assert response.json == {"error": "Invalid status: this-is-wrong."}
def test_spamcheck_invalid_token(client, dummy_user, mocker, spamcheck_on): token = make_token({"sub": "dummy"}, audience=Audience.email_validation) response = client.post( "/register/spamcheck-hook", json={ "token": token, "status": "active" }, ) assert response.status_code == 400 assert response.json["error"] == "Invalid token: Invalid audience"
def test_spamcheck_expired_token(client, dummy_user, mocker, spamcheck_on): token = make_token({"sub": "dummy"}, audience=Audience.spam_check, ttl=-1) response = client.post( "/register/spamcheck-hook", json={ "token": token, "status": "active" }, ) assert response.status_code == 400 assert response.json == {"error": "The token has expired"}
def test_step_3_invalid_token(client, dummy_stageuser, mocker): """Registration activation page with an invalid token""" token = make_token( { "sub": dummy_stageuser.username, "mail": dummy_stageuser.mail }, audience=Audience.email_validation, ttl=-1, ) result = client.get(f'/register/activate?token={token}') assert_redirects_with_flash( result, expected_url="/?tab=register", expected_message= "This token is no longer valid, please register again.", expected_category="warning", )
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_data = read_token(token, audience=Audience.password_reset) except jwt.exceptions.DecodeError: flash(_("The token is invalid, please request a new one."), "warning") return redirect(url_for('.forgot_password_ask')) username = token_data["sub"] 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(a_uid=username)['result']) if user.last_password_change != token_data["lpc"]: 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(current_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', ) current_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 current_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(a_uid=username)['result']) token = make_token( { "sub": user.username, "lpc": user.last_password_change }, audience=Audience.password_reset, ) 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. current_app.logger.error( f'An unhandled error {e.__class__.__name__} happened while reseting ' f'the password for user {username}: {e.message}') form.non_field_errors.errors.append( _('Could not change password, please try again.')) else: lock.delete() flash(_('Your password has been changed.'), 'success') current_app.logger.info( f"Password for {username} was changed after completing the forgotten " f"password process.") messaging.publish( UserUpdateV1({ "msg": { "agent": username, "user": username, "fields": ["password"], } })) return redirect(url_for('.root')) return render_template('forgot-password-change.html', username=username, form=form, token=token)
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(a_uid=username)['result']) except python_freeipa.exceptions.NotFound: raise FormError( "username", _("User %(username)s does not exist", username=username)) token = make_token( { "sub": user.username, "lpc": user.last_password_change }, audience=Audience.password_reset, ) # 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: current_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 current_app.config["DEBUG"]: # pragma: no cover current_app.logger.debug(email) lock.store() current_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)