def send_signup_email(flow): logger.info(f"Sending signup email to {flow.email=}:") # whether we've sent an email at all yet email_sent_before = flow.email_sent if flow.email_verified: # we just send a link to continue, not a verification link signup_link = urls.signup_link(token=flow.flow_token) elif flow.email_token and flow.token_is_valid: # if the verification email was sent and still is not expired, just resend the verification email signup_link = urls.signup_link(token=flow.email_token) else: # otherwise send a fresh email with new token token = urlsafe_secure_token() flow.email_verified = False flow.email_token = token flow.email_token_expiry = now() + SIGNUP_EMAIL_TOKEN_VALIDITY signup_link = urls.signup_link(token=flow.email_token) flow.email_sent = True logger.info(f"Link is: {signup_link}") template = "signup_verify" if not email_sent_before else "signup_continue" email.enqueue_email_from_template(flow.email, template, template_args={ "flow": flow, "signup_link": signup_link })
def test_purge_password_reset_tokens(db): user, api_token = generate_user() with session_scope() as session: password_reset_token = PasswordResetToken(token=urlsafe_secure_token(), user=user, expiry=now()) session.add(password_reset_token) assert session.execute( select(func.count()).select_from( PasswordResetToken)).scalar_one() == 1 queue_job(BackgroundJobType.purge_password_reset_tokens, empty_pb2.Empty()) process_job() with session_scope() as session: assert session.execute( select(func.count()).select_from( PasswordResetToken)).scalar_one() == 0 with session_scope() as session: assert (session.execute( select(func.count()).select_from(BackgroundJob).where( BackgroundJob.state == BackgroundJobState.completed)).scalar_one() == 1) assert (session.execute( select(func.count()).select_from(BackgroundJob).where( BackgroundJob.state != BackgroundJobState.completed)). scalar_one() == 0)
def ConfirmDeleteAccount(self, request, context): """ Confirm account deletion using account delete token """ with session_scope() as session: res = session.execute( select(User, AccountDeletionToken).join( AccountDeletionToken, AccountDeletionToken.user_id == User.id).where( AccountDeletionToken.token == request.token).where( AccountDeletionToken.is_valid)).one_or_none() if not res: context.abort(grpc.StatusCode.NOT_FOUND, errors.INVALID_TOKEN) user, account_deletion_token = res session.execute( delete(AccountDeletionToken).where( AccountDeletionToken.user_id == user.id)) undelete_days = 7 user.is_deleted = True user.undelete_until = now() + timedelta(days=undelete_days) user.undelete_token = urlsafe_secure_token() send_account_deletion_successful_email(user, undelete_days) return empty_pb2.Empty()
def test_purge_account_deletion_tokens(db): user, api_token = generate_user() user2, api_token2 = generate_user() user3, api_token3 = generate_user() with session_scope() as session: """ 3 cases: 1) Token is valid 2) Token expired but account retrievable 3) Account is irretrievable (and expired) """ account_deletion_tokens = [ AccountDeletionToken(token=urlsafe_secure_token(), user=user, expiry=now() - timedelta(hours=2)), AccountDeletionToken(token=urlsafe_secure_token(), user=user2, expiry=now()), AccountDeletionToken(token=urlsafe_secure_token(), user=user3, expiry=now() + timedelta(hours=5)), ] for token in account_deletion_tokens: session.add(token) assert session.execute( select(func.count()).select_from( AccountDeletionToken)).scalar_one() == 3 queue_job(BackgroundJobType.purge_account_deletion_tokens, empty_pb2.Empty()) process_job() with session_scope() as session: assert session.execute( select(func.count()).select_from( AccountDeletionToken)).scalar_one() == 1 with session_scope() as session: assert (session.execute( select(func.count()).select_from(BackgroundJob).where( BackgroundJob.state == BackgroundJobState.completed)).scalar_one() == 1) assert (session.execute( select(func.count()).select_from(BackgroundJob).where( BackgroundJob.state != BackgroundJobState.completed)). scalar_one() == 0)
def new_login_token(session, user, hours=2): """ Make a login token that's valid for `hours` hours Returns token and expiry text """ token = urlsafe_secure_token() login_token = LoginToken(token=token, user=user, expiry=datetime.datetime.utcnow() + datetime.timedelta(hours=hours)) session.add(login_token) session.commit() return login_token, f"{hours} hours"
def new_signup_token(session, email, hours=2): """ Make a signup token that's valid for `hours` hours Returns token and expiry text """ token = urlsafe_secure_token() signup_token = SignupToken(token=token, email=email, expiry=datetime.datetime.utcnow() + datetime.timedelta(hours=hours)) session.add(signup_token) session.commit() return signup_token, f"{hours} hours"
def new_password_reset_token(session, user, hours=2): """ Make a password reset token that's valid for `hours` hours Returns token and expiry text """ token = urlsafe_secure_token() password_reset_token = PasswordResetToken(token=token, user=user, expiry=now() + timedelta(hours=hours)) session.add(password_reset_token) session.commit() return password_reset_token, f"{hours} hours"
def set_email_change_token(session, user, hours=2): """ Make a new email change token that's valid for `hours` hours for this user Note: does not call session.commit() Returns token and expiry text """ token = urlsafe_secure_token() user.new_email_token = token user.new_email_token_created = now() user.new_email_token_expiry = now() + datetime.timedelta(hours=hours) return token, f"{hours} hours"
def _create_session(self, session, user): """ Creates a session for the given user and returns the bearer token. You need to give an active DB session as nested sessions don't really work here due to the active User object. """ token = urlsafe_secure_token() user_session = UserSession( user=user, token=token ) session.add(user_session) session.commit() logging.debug(f"Handing out {token=} to {user=}") return token
def send_account_deletion_confirmation_email(user): logger.info(f"Sending account deletion confirmation email to {user=}.") logger.info(f"Email for {user.username=} sent to {user.email}.") token = AccountDeletionToken(token=urlsafe_secure_token(), user=user, expiry=now() + timedelta(hours=2)) deletion_link = urls.delete_account_link( account_deletion_token=token.token) email.enqueue_email_from_template( user.email, "account_deletion_confirmation", template_args={ "user": user, "deletion_link": deletion_link }, ) return token
def send_login_email(session, user): login_token = LoginToken(token=urlsafe_secure_token(), user=user, expiry=now() + timedelta(hours=2)) session.add(login_token) logger.info(f"Sending login email to {user=}:") logger.info(f"Email for {user.username=} to {user.email=}") logger.info(f"Token: {login_token=} ({login_token.created=}") login_link = urls.login_link(login_token=login_token.token) logger.info(f"Link is: {login_link}") email.enqueue_email_from_template(user.email, "login", template_args={ "user": user, "login_link": login_link }) return login_token
def send_password_reset_email(session, user): password_reset_token = PasswordResetToken(token=urlsafe_secure_token(), user=user, expiry=now() + timedelta(hours=2)) session.add(password_reset_token) logger.info(f"Sending password reset email to {user=}:") password_reset_link = urls.password_reset_link( password_reset_token=password_reset_token.token) logger.info(f"Link is: {password_reset_link}") email.enqueue_email_from_template(user.email, "password_reset", template_args={ "user": user, "password_reset_link": password_reset_link }) return password_reset_token
def _create_session(self, context, session, user): """ Creates a session for the given user and returns the bearer token. You need to give an active DB session as nested sessions don't really work here due to the active User object. Will abort the API calling context if the user is banned from logging in. """ if user.is_banned: context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.ACCOUNT_SUSPENDED) token = urlsafe_secure_token() user_session = UserSession(user=user, token=token) session.add(user_session) session.commit() logger.debug(f"Handing out {token=} to {user=}") return token
def test_email_changed_confirmation_sent_to_new_email(db): confirmation_token = urlsafe_secure_token() user, user_token = generate_user() user.new_email = f"{random_hex(12)}@couchers.org.invalid" user.new_email_token = confirmation_token with patch("couchers.email.queue_email") as mock: send_email_changed_confirmation_to_new_email(user) assert mock.call_count == 1 (sender_name, sender_email, recipient, subject, plain, html), _ = mock.call_args assert "new email" in subject assert recipient == user.new_email assert user.name in plain assert user.name in html assert user.email in plain assert user.email in html assert "via a similar email sent to your old email address" in plain assert "via a similar email sent to your old email address" in html assert f"{config['BASE_URL']}/confirm-email?token={confirmation_token}" in plain assert f"{config['BASE_URL']}/confirm-email?token={confirmation_token}" in html assert "*****@*****.**" in plain assert "*****@*****.**" in html
def ChangeEmail(self, request, context): """ Change the user's email address. If the user has a password, a notification is sent to the old email, and a confirmation is sent to the new one. Otherwise they need to confirm twice, via an email sent to each of their old and new emails. In all confirmation emails, the user must click on the confirmation link. """ with session_scope() as session: user = session.execute( select(User).where(User.id == context.user_id)).scalar_one() # check password first _check_password(user, "password", request, context) # not a valid email if not is_valid_email(request.new_email): context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_EMAIL) # email already in use (possibly by this user) if session.execute( select(User).where( User.email == request.new_email)).scalar_one_or_none(): context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_EMAIL) user.new_email = request.new_email user.new_email_token = urlsafe_secure_token() user.new_email_token_created = now() user.new_email_token_expiry = now() + timedelta(hours=2) user.need_to_confirm_via_new_email = True if user.has_password: user.old_email_token = None user.old_email_token_created = None user.old_email_token_expiry = None user.need_to_confirm_via_old_email = False send_email_changed_notification_email(user) send_email_changed_confirmation_to_new_email(user) notify( user_id=user.id, topic="email_address", key="", action="change", icon="wrench", title=f"Your email was changed", link=urls.account_settings_link(), ) else: user.old_email_token = urlsafe_secure_token() user.old_email_token_created = now() user.old_email_token_expiry = now() + timedelta(hours=2) user.need_to_confirm_via_old_email = True send_email_changed_confirmation_to_old_email(user) send_email_changed_confirmation_to_new_email(user) # session autocommit return empty_pb2.Empty()