DEV: Hash tokens stored from email_tokens (#14493)

This commit adds token_hash and scopes columns to email_tokens table.
token_hash is a replacement for the token column to avoid storing email
tokens in plaintext as it can pose a security risk. The new scope column
ensures that email tokens cannot be used to perform a different action
than the one intended.

To sum up, this commit:

* Adds token_hash and scope to email_tokens

* Reuses code that schedules critical_user_email

* Refactors EmailToken.confirm and EmailToken.atomic_confirm methods

* Periodically cleans old, unconfirmed or expired email tokens
This commit is contained in:
Dan Ungureanu 2021-11-25 09:34:39 +02:00 committed by GitHub
parent 4c46c7e334
commit fa8cd629f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 482 additions and 599 deletions

View File

@ -328,7 +328,7 @@ class Admin::UsersController < Admin::AdminController
def activate def activate
guardian.ensure_can_activate!(@user) guardian.ensure_can_activate!(@user)
# ensure there is an active email token # ensure there is an active email token
@user.email_tokens.create(email: @user.email) unless @user.email_tokens.active.exists? @user.email_tokens.create!(email: @user.email, scope: EmailToken.scopes[:signup]) if !@user.email_tokens.active.exists?
@user.activate @user.activate
StaffActionLogger.new(current_user).log_user_activate(@user, I18n.t('user.activated_by_staff')) StaffActionLogger.new(current_user).log_user_activate(@user, I18n.t('user.activated_by_staff'))
render json: success_json render json: success_json

View File

@ -33,7 +33,6 @@ class FinishInstallationController < ApplicationController
send_signup_email send_signup_email
redirect_confirm(@user.email) redirect_confirm(@user.email)
end end
end end
end end
@ -50,14 +49,10 @@ class FinishInstallationController < ApplicationController
protected protected
def send_signup_email def send_signup_email
email_token = @user.email_tokens.unconfirmed.active.first return if @user.active && @user.email_confirmed?
if email_token.present? email_token = @user.email_tokens.create!(email: @user.email, scope: EmailToken.scopes[:signup])
Jobs.enqueue(:critical_user_email, EmailToken.enqueue_signup_email(email_token)
type: :signup,
user_id: @user.id,
email_token: email_token.token)
end
end end
def redirect_confirm(email) def redirect_confirm(email)

View File

@ -415,7 +415,10 @@ class InvitesController < ApplicationController
Group.refresh_automatic_groups!(:admins, :moderators, :staff) if user.staff? Group.refresh_automatic_groups!(:admins, :moderators, :staff) if user.staff?
if user.has_password? if user.has_password?
send_activation_email(user) unless user.active if !user.active
email_token = user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:signup])
EmailToken.enqueue_signup_email(email_token)
end
elsif !SiteSetting.enable_discourse_connect && SiteSetting.enable_local_logins elsif !SiteSetting.enable_discourse_connect && SiteSetting.enable_local_logins
Jobs.enqueue(:invite_password_instructions_email, username: user.username) Jobs.enqueue(:invite_password_instructions_email, username: user.username)
end end
@ -440,14 +443,4 @@ class InvitesController < ApplicationController
end end
end end
end end
def send_activation_email(user)
email_token = user.email_tokens.create!(email: user.email)
Jobs.enqueue(:critical_user_email,
type: :signup,
user_id: user.id,
email_token: email_token.token
)
end
end end

View File

@ -339,7 +339,7 @@ class SessionController < ApplicationController
def email_login_info def email_login_info
token = params[:token] token = params[:token]
matched_token = EmailToken.confirmable(token) matched_token = EmailToken.confirmable(token, scope: EmailToken.scopes[:email_login])
user = matched_token&.user user = matched_token&.user
check_local_login_allowed(user: user, check_login_via_email: true) check_local_login_allowed(user: user, check_login_via_email: true)
@ -377,7 +377,7 @@ class SessionController < ApplicationController
def email_login def email_login
token = params[:token] token = params[:token]
matched_token = EmailToken.confirmable(token) matched_token = EmailToken.confirmable(token, scope: EmailToken.scopes[:email_login])
user = matched_token&.user user = matched_token&.user
check_local_login_allowed(user: user, check_login_via_email: true) check_local_login_allowed(user: user, check_login_via_email: true)
@ -388,7 +388,7 @@ class SessionController < ApplicationController
return render(json: @second_factor_failure_payload) return render(json: @second_factor_failure_payload)
end end
if user = EmailToken.confirm(token) if user = EmailToken.confirm(token, scope: EmailToken.scopes[:email_login])
if login_not_approved_for?(user) if login_not_approved_for?(user)
return render json: login_not_approved return render json: login_not_approved
elsif payload = login_error_check(user) elsif payload = login_error_check(user)
@ -444,7 +444,7 @@ class SessionController < ApplicationController
user_presence = user.present? && user.human? && !user.staged user_presence = user.present? && user.human? && !user.staged
if user_presence if user_presence
email_token = user.email_tokens.create(email: user.email) email_token = user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:password_reset])
Jobs.enqueue(:critical_user_email, type: :forgot_password, user_id: user.id, email_token: email_token.token) Jobs.enqueue(:critical_user_email, type: :forgot_password, user_id: user.id, email_token: email_token.token)
end end

View File

@ -155,9 +155,8 @@ class Users::OmniauthCallbacksController < ApplicationController
user.update!(password: SecureRandom.hex) user.update!(password: SecureRandom.hex)
# Ensure there is an active email token # Ensure there is an active email token
unless EmailToken.where(email: user.email, confirmed: true).exists? || if !EmailToken.where(email: user.email, confirmed: true).exists? && !user.email_tokens.active.where(email: user.email).exists?
user.email_tokens.active.where(email: user.email).exists? user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:signup])
user.email_tokens.create!(email: user.email)
end end
user.activate user.activate

View File

@ -783,7 +783,6 @@ class UsersController < ApplicationController
# no point doing anything else if we can't even find # no point doing anything else if we can't even find
# a user from the token # a user from the token
if @user if @user
if !secure_session["second-factor-#{token}"] if !secure_session["second-factor-#{token}"]
second_factor_authentication_result = @user.authenticate_second_factor(params, secure_session) second_factor_authentication_result = @user.authenticate_second_factor(params, secure_session)
if !second_factor_authentication_result.ok if !second_factor_authentication_result.ok
@ -869,7 +868,7 @@ class UsersController < ApplicationController
def confirm_email_token def confirm_email_token
expires_now expires_now
EmailToken.confirm(params[:token]) EmailToken.confirm(params[:token], scope: EmailToken.scopes[:signup])
render json: success_json render json: success_json
end end
@ -895,7 +894,7 @@ class UsersController < ApplicationController
RateLimiter.new(nil, "admin-login-min-#{request.remote_ip}", 3, 1.minute).performed! RateLimiter.new(nil, "admin-login-min-#{request.remote_ip}", 3, 1.minute).performed!
if user = User.with_email(params[:email]).admins.human_users.first if user = User.with_email(params[:email]).admins.human_users.first
email_token = user.email_tokens.create(email: user.email) email_token = user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:email_login])
Jobs.enqueue(:critical_user_email, type: :admin_login, user_id: user.id, email_token: email_token.token) Jobs.enqueue(:critical_user_email, type: :admin_login, user_id: user.id, email_token: email_token.token)
@message = I18n.t("admin_login.success") @message = I18n.t("admin_login.success")
else else
@ -926,7 +925,7 @@ class UsersController < ApplicationController
RateLimiter.new(nil, "email-login-min-#{user.id}", 3, 1.minute).performed! RateLimiter.new(nil, "email-login-min-#{user.id}", 3, 1.minute).performed!
if user_presence if user_presence
email_token = user.email_tokens.create!(email: user.email) email_token = user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:email_login])
Jobs.enqueue(:critical_user_email, Jobs.enqueue(:critical_user_email,
type: :email_login, type: :email_login,
@ -996,7 +995,7 @@ class UsersController < ApplicationController
def perform_account_activation def perform_account_activation
raise Discourse::InvalidAccess.new if honeypot_or_challenge_fails?(params) raise Discourse::InvalidAccess.new if honeypot_or_challenge_fails?(params)
if @user = EmailToken.confirm(params[:token]) if @user = EmailToken.confirm(params[:token], scope: EmailToken.scopes[:signup])
# Log in the user unless they need to be approved # Log in the user unless they need to be approved
if Guardian.new(@user).can_access_forum? if Guardian.new(@user).can_access_forum?
@user.enqueue_welcome_message('welcome_user') if @user.send_welcome_message @user.enqueue_welcome_message('welcome_user') if @user.send_welcome_message
@ -1041,8 +1040,8 @@ class UsersController < ApplicationController
primary_email.skip_validate_email = false primary_email.skip_validate_email = false
if primary_email.save if primary_email.save
@user.email_tokens.create!(email: @user.email) @email_token = @user.email_tokens.create!(email: @user.email, scope: EmailToken.scopes[:signup])
enqueue_activation_email EmailToken.enqueue_signup_email(@email_token, to_address: @user.email)
render json: success_json render json: success_json
else else
render_json_error(primary_email) render_json_error(primary_email)
@ -1061,11 +1060,10 @@ class UsersController < ApplicationController
if params[:username].present? if params[:username].present?
@user = User.find_by_username_or_email(params[:username].to_s) @user = User.find_by_username_or_email(params[:username].to_s)
end end
raise Discourse::NotFound unless @user raise Discourse::NotFound unless @user
if !current_user&.staff? && if !current_user&.staff? && @user.id != session[SessionController::ACTIVATE_USER_KEY]
@user.id != session[SessionController::ACTIVATE_USER_KEY]
raise Discourse::InvalidAccess.new raise Discourse::InvalidAccess.new
end end
@ -1074,17 +1072,12 @@ class UsersController < ApplicationController
if @user.active && @user.email_confirmed? if @user.active && @user.email_confirmed?
render_json_error(I18n.t('activation.activated'), status: 409) render_json_error(I18n.t('activation.activated'), status: 409)
else else
@email_token = @user.email_tokens.unconfirmed.active.first @email_token = @user.email_tokens.create!(email: @user.email, scope: EmailToken.scopes[:signup])
enqueue_activation_email EmailToken.enqueue_signup_email(@email_token, to_address: @user.email)
render body: nil render body: nil
end end
end end
def enqueue_activation_email
@email_token ||= @user.email_tokens.create!(email: @user.email)
Jobs.enqueue(:critical_user_email, type: :signup, user_id: @user.id, email_token: @email_token.token, to_address: @user.email)
end
def search_users def search_users
term = params[:term].to_s.strip term = params[:term].to_s.strip
@ -1635,14 +1628,17 @@ class UsersController < ApplicationController
end end
def password_reset_find_user(token, committing_change:) def password_reset_find_user(token, committing_change:)
if EmailToken.valid_token_format?(token) @user = if committing_change
@user = committing_change ? EmailToken.confirm(token) : EmailToken.confirmable(token)&.user EmailToken.confirm(token, scope: EmailToken.scopes[:password_reset])
if @user else
secure_session["password-#{token}"] = @user.id EmailToken.confirmable(token, scope: EmailToken.scopes[:password_reset])&.user
else end
user_id = secure_session["password-#{token}"].to_i
@user = User.find(user_id) if user_id > 0 if @user
end secure_session["password-#{token}"] = @user.id
else
user_id = secure_session["password-#{token}"].to_i
@user = User.find(user_id) if user_id > 0
end end
@error = I18n.t('password_reset.no_token') if !@user @error = I18n.t('password_reset.no_token') if !@user

View File

@ -200,17 +200,17 @@ class UsersEmailController < ApplicationController
def load_change_request(type) def load_change_request(type)
expires_now expires_now
@token = EmailToken.confirmable(params[:token]) token = EmailToken.confirmable(params[:token], scope: EmailToken.scopes[:email_update])
if @token if token
if type == :old if type == :old
@change_request = @token.user&.email_change_requests.where(old_email_token_id: @token.id).first @change_request = token.user&.email_change_requests.where(old_email_token_id: token.id).first
elsif type == :new elsif type == :new
@change_request = @token.user&.email_change_requests.where(new_email_token_id: @token.id).first @change_request = token.user&.email_change_requests.where(new_email_token_id: token.id).first
end end
end end
@user = @token&.user @user = token&.user
if (!@user || !@change_request) if (!@user || !@change_request)
@error = I18n.t("change_email.already_done") @error = I18n.t("change_email.already_done")

View File

@ -13,11 +13,13 @@ module Jobs
user.custom_fields['activation_reminder'] = true user.custom_fields['activation_reminder'] = true
user.save_custom_fields user.save_custom_fields
email_token = user.email_tokens.create!(email: user.email) email_token = user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:signup])
::Jobs.enqueue(:user_email, ::Jobs.enqueue(
type: :activation_reminder, :user_email,
user_id: user.id, type: :activation_reminder,
email_token: email_token.token) user_id: user.id,
email_token: email_token.token
)
end end
end end
end end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
module Jobs
class CleanUpEmailTokens < ::Jobs::Scheduled
every 1.day
def execute(args)
EmailToken
.where('NOT confirmed OR expired')
.where('created_at < ?', 1.month.ago)
.delete_all
end
end
end

View File

@ -56,7 +56,7 @@ class InviteMailer < ActionMailer::Base
def send_password_instructions(user) def send_password_instructions(user)
if user.present? if user.present?
email_token = user.email_tokens.create(email: user.email) email_token = user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:password_reset])
build_email(user.email, build_email(user.email,
template: 'invite_password_instructions', template: 'invite_password_instructions',
email_token: email_token.token) email_token: email_token.token)

View File

@ -1,9 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class EmailChangeRequest < ActiveRecord::Base class EmailChangeRequest < ActiveRecord::Base
belongs_to :user
belongs_to :old_email_token, class_name: 'EmailToken', dependent: :destroy belongs_to :old_email_token, class_name: 'EmailToken', dependent: :destroy
belongs_to :new_email_token, class_name: 'EmailToken', dependent: :destroy belongs_to :new_email_token, class_name: 'EmailToken', dependent: :destroy
belongs_to :user
belongs_to :requested_by, class_name: "User", foreign_key: :requested_by_user_id belongs_to :requested_by, class_name: "User", foreign_key: :requested_by_user_id
validates :new_email, presence: true, format: { with: EmailValidator.email_regex } validates :new_email, presence: true, format: { with: EmailValidator.email_regex }
@ -12,6 +12,13 @@ class EmailChangeRequest < ActiveRecord::Base
@states ||= Enum.new(authorizing_old: 1, authorizing_new: 2, complete: 3) @states ||= Enum.new(authorizing_old: 1, authorizing_new: 2, complete: 3)
end end
def self.find_by_new_token(token)
EmailChangeRequest
.joins("INNER JOIN email_tokens ON email_tokens.id = email_change_requests.new_email_token_id")
.where("email_tokens.token_hash = ?", EmailToken.hash_token(token))
.last
end
def requested_by_admin? def requested_by_admin?
self.requested_by&.admin? && !self.requested_by_self? self.requested_by&.admin? && !self.requested_by_self?
end end
@ -19,12 +26,6 @@ class EmailChangeRequest < ActiveRecord::Base
def requested_by_self? def requested_by_self?
self.requested_by_user_id == self.user_id self.requested_by_user_id == self.user_id
end end
def self.find_by_new_token(token)
joins(
"INNER JOIN email_tokens ON email_tokens.id = email_change_requests.new_email_token_id"
).where("email_tokens.token = ?", token).last
end
end end
# == Schema Information # == Schema Information

View File

@ -1,97 +1,106 @@
# frozen_string_literal: true # frozen_string_literal: true
class EmailToken < ActiveRecord::Base class EmailToken < ActiveRecord::Base
class TokenAccessError < StandardError; end
belongs_to :user belongs_to :user
validates :token, :user_id, :email, presence: true validates :user_id, :email, :token_hash, presence: true
before_validation(on: :create) do scope :unconfirmed, -> { where(confirmed: false) }
self.token = EmailToken.generate_token scope :active, -> { where(expired: false).where('created_at >= ?', SiteSetting.email_token_valid_hours.hours.ago) }
self.email = self.email.downcase if self.email
end
after_create do after_initialize do
# Expire the previous tokens if self.token_hash.blank?
EmailToken.where(user_id: self.user_id) @token ||= SecureRandom.hex
.where("id != ?", self.id) self.token = @token
.update_all(expired: true) self.token_hash = self.class.hash_token(@token)
end
def self.token_length
16
end
def self.valid_after
SiteSetting.email_token_valid_hours.hours.ago
end
def self.unconfirmed
where(confirmed: false)
end
def self.active
where(expired: false).where('created_at > ?', valid_after)
end
def self.generate_token
SecureRandom.hex(EmailToken.token_length)
end
def self.valid_token_format?(token)
token.present? && token =~ /\h{#{token.length / 2}}/i
end
def self.atomic_confirm(token)
failure = { success: false }
return failure unless valid_token_format?(token)
email_token = confirmable(token)
return failure if email_token.blank?
user = email_token.user
failure[:user] = user
row_count = EmailToken.where(confirmed: false, id: email_token.id, expired: false).update_all 'confirmed = true'
if row_count == 1
{ success: true, user: user, email_token: email_token }
else
failure
end end
end end
def self.confirm(token, skip_reviewable: false) after_create do
User.transaction do EmailToken
result = atomic_confirm(token) .where(user_id: self.user_id)
user = result[:user] .where(scope: [nil, self.scope])
if result[:success] .where.not(id: self.id)
# If we are activating the user, send the welcome message .update_all(expired: true)
user.send_welcome_message = !user.active? end
user.email = result[:email_token].email
user.active = true
user.custom_fields.delete('activation_reminder')
user.save!
user.create_reviewable unless skip_reviewable
user.set_automatic_groups
DiscourseEvent.trigger(:user_confirmed_email, user)
end
if user before_validation do
if Invite.redeem_from_email(user.email).present? self.email = self.email.downcase if self.email
user.reload end
end
user before_save do
end if self.scope.blank?
Discourse.deprecate("EmailToken#scope cannot be empty.", output_in_test: true)
end
end
def self.scopes
@scopes ||= Enum.new(
signup: 1,
password_reset: 2,
email_login: 3,
email_update: 4,
)
end
def token
raise TokenAccessError.new if @token.blank?
self[:token]
end
def self.confirm(token, scope: nil, skip_reviewable: false)
User.transaction do
email_token = confirmable(token, scope: scope)
return if email_token.blank?
email_token.update!(confirmed: true)
user = email_token.user
user.send_welcome_message = !user.active?
user.email = email_token.email
user.active = true
user.custom_fields.delete('activation_reminder')
user.save!
user.create_reviewable if !skip_reviewable
user.set_automatic_groups
DiscourseEvent.trigger(:user_confirmed_email, user)
Invite.redeem_from_email(user.email)
user.reload
end end
rescue ActiveRecord::RecordInvalid rescue ActiveRecord::RecordInvalid
# If the user's email is already taken, just return nil (failure) # If the user's email is already taken, just return nil (failure)
end end
def self.confirmable(token) def self.confirmable(token, scope: nil)
EmailToken.where(token: token) return nil if token.blank?
.where(expired: false, confirmed: false)
.where("created_at >= ?", EmailToken.valid_after) relation = unconfirmed.active
.includes(:user) .includes(:user)
.first .where(token_hash: hash_token(token))
# TODO(2022-01-01): All email tokens should have scopes by now
if !scope
relation.first
else
relation.where(scope: scope).first || relation.where(scope: nil).first
end
end
def self.enqueue_signup_email(email_token, to_address: nil)
Jobs.enqueue(
:critical_user_email,
type: :signup,
user_id: email_token.user_id,
email_token: email_token.token,
to_address: to_address
)
end
def self.hash_token(token)
Digest::SHA256.hexdigest(token)
end end
end end
@ -107,6 +116,8 @@ end
# expired :boolean default(FALSE), not null # expired :boolean default(FALSE), not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# token_hash :string not null
# scope :integer
# #
# Indexes # Indexes
# #

View File

@ -74,7 +74,7 @@ InviteRedeemer = Struct.new(:invite, :email, :username, :name, :password, :user_
authenticator.finish authenticator.finish
if invite.emailed_status != Invite.emailed_status_types[:not_required] && email == invite.email && invite.email_token.present? && email_token == invite.email_token if invite.emailed_status != Invite.emailed_status_types[:not_required] && email == invite.email && invite.email_token.present? && email_token == invite.email_token
user.email_tokens.create!(email: user.email) user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:signup])
user.activate user.activate
end end

View File

@ -1049,11 +1049,9 @@ class User < ActiveRecord::Base
end end
def activate def activate
if email_token = self.email_tokens.active.where(email: self.email).first email_token = self.email_tokens.create!(email: self.email, scope: EmailToken.scopes[:signup])
EmailToken.confirm(email_token.token, skip_reviewable: true) EmailToken.confirm(email_token.token, scope: EmailToken.scopes[:signup])
end reload
self.update!(active: true)
create_reviewable
end end
def deactivate(performed_by) def deactivate(performed_by)
@ -1495,7 +1493,7 @@ class User < ActiveRecord::Base
end end
def create_email_token def create_email_token
email_tokens.create!(email: email) email_tokens.create!(email: email, scope: EmailToken.scopes[:signup])
end end
def ensure_password_is_hashed def ensure_password_is_hashed

View File

@ -54,15 +54,8 @@ end
class EmailActivator < UserActivator class EmailActivator < UserActivator
def activate def activate
email_token = user.email_tokens.unconfirmed.active.first email_token = user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:signup])
email_token = user.email_tokens.create(email: user.email) if email_token.nil? EmailToken.enqueue_signup_email(email_token)
Jobs.enqueue(:critical_user_email,
type: :signup,
user_id: user.id,
email_token: email_token.token
)
success_message success_message
end end

View File

@ -49,10 +49,7 @@ class UserAuthenticator
private private
def confirm_email def confirm_email
if authenticated? @user.activate if authenticated?
EmailToken.confirm(@user.email_tokens.first.token)
@user.set_automatic_groups
end
end end
def authenticator def authenticator

View File

@ -24,7 +24,7 @@
</p> </p>
<%=form_tag(u_confirm_new_email_path, method: :put) do %> <%=form_tag(u_confirm_new_email_path, method: :put) do %>
<%= hidden_field_tag 'token', @token.token %> <%= hidden_field_tag 'token', params[:token] %>
<%= hidden_field_tag 'second_factor_token', nil, id: 'security-key-credential' %> <%= hidden_field_tag 'second_factor_token', nil, id: 'security-key-credential' %>
<div id="security-key-error"></div> <div id="security-key-error"></div>
@ -34,7 +34,7 @@
<% if @show_backup_codes %> <% if @show_backup_codes %>
<div id="backup-second-factor-form" style=""> <div id="backup-second-factor-form" style="">
<%= hidden_field_tag 'second_factor_method', UserSecondFactor.methods[:backup_code] %> <%= hidden_field_tag 'second_factor_method', UserSecondFactor.methods[:backup_code] %>
<h3><%= t('login.second_factor_backup_title') %></h3> <h3><%= t('login.second_factor_backup_title') %></h3>
<%= label_tag(:second_factor_token, t("login.second_factor_backup_description")) %> <%= label_tag(:second_factor_token, t("login.second_factor_backup_description")) %>
<div><%= render 'common/second_factor_backup_input' %></div> <div><%= render 'common/second_factor_backup_input' %></div>
@ -44,8 +44,8 @@
<%= link_to t("login.second_factor_toggle.totp"), show_backup: "false" %> <%= link_to t("login.second_factor_toggle.totp"), show_backup: "false" %>
<br/> <br/>
<% elsif @show_security_key %> <% elsif @show_security_key %>
<%= hidden_field_tag 'security_key_challenge', @security_key_challenge, id: 'security-key-challenge' %> <%= hidden_field_tag 'security_key_challenge', @security_key_challenge, id: 'security-key-challenge' %>
<%= hidden_field_tag 'second_factor_method', UserSecondFactor.methods[:security_key] %> <%= hidden_field_tag 'second_factor_method', UserSecondFactor.methods[:security_key] %>
<%= hidden_field_tag 'security_key_allowed_credential_ids', @security_key_allowed_credential_ids.join(","), id: 'security-key-allowed-credential-ids' %> <%= hidden_field_tag 'security_key_allowed_credential_ids', @security_key_allowed_credential_ids.join(","), id: 'security-key-allowed-credential-ids' %>
<div id="security-key-form"> <div id="security-key-form">
<h3><%= t('login.security_key_authenticate') %></h3> <h3><%= t('login.security_key_authenticate') %></h3>
@ -61,7 +61,7 @@
<% end %> <% end %>
<% elsif @show_second_factor %> <% elsif @show_second_factor %>
<div id="primary-second-factor-form"> <div id="primary-second-factor-form">
<%= hidden_field_tag 'second_factor_method', UserSecondFactor.methods[:totp] %> <%= hidden_field_tag 'second_factor_method', UserSecondFactor.methods[:totp] %>
<h3><%= t('login.second_factor_title') %></h3> <h3><%= t('login.second_factor_title') %></h3>
<%= label_tag(:second_factor_token, t('login.second_factor_description')) %> <%= label_tag(:second_factor_token, t('login.second_factor_description')) %>
<div><%= render 'common/second_factor_text_field' %></div> <div><%= render 'common/second_factor_text_field' %></div>

View File

@ -27,7 +27,7 @@
</p> </p>
<%=form_tag(u_confirm_old_email_path, method: :put) do %> <%=form_tag(u_confirm_old_email_path, method: :put) do %>
<%= hidden_field_tag 'token', @token.token %> <%= hidden_field_tag 'token', params[:token] %>
<%= submit_tag t('change_email.confirm'), class: "btn btn-primary" %> <%= submit_tag t('change_email.confirm'), class: "btn btn-primary" %>
<% end %> <% end %>
<% end %> <% end %>

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
class AddTokenHashToEmailToken < ActiveRecord::Migration[6.1]
def up
add_column :email_tokens, :token_hash, :string
loop do
rows = DB
.query("SELECT id, token FROM email_tokens WHERE token_hash IS NULL LIMIT 500")
.map { |row| { id: row.id, token_hash: Digest::SHA256.hexdigest(row.token) } }
break if rows.size == 0
data_string = rows.map { |r| "(#{r[:id]}, '#{r[:token_hash]}')" }.join(",")
execute <<~SQL
UPDATE email_tokens
SET token_hash = data.token_hash
FROM (VALUES #{data_string}) AS data(id, token_hash)
WHERE email_tokens.id = data.id
SQL
end
change_column_null :email_tokens, :token_hash, false
end
def down
drop_column :email_tokens, :token_hash, :string
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class AddScopeToEmailToken < ActiveRecord::Migration[6.1]
def up
add_column :email_tokens, :scope, :integer
end
def down
drop_column :email_tokens, :scope, :integer
end
end

View File

@ -4,6 +4,7 @@ class EmailUpdater
include HasErrors include HasErrors
attr_reader :user attr_reader :user
attr_reader :change_req
def self.human_attribute_name(name, options = {}) def self.human_attribute_name(name, options = {})
User.human_attribute_name(name, options) User.human_attribute_name(name, options)
@ -48,16 +49,16 @@ class EmailUpdater
UserHistory.create!(action: UserHistory.actions[:add_email], acting_user_id: @user.id) UserHistory.create!(action: UserHistory.actions[:add_email], acting_user_id: @user.id)
end end
change_req = EmailChangeRequest.find_or_initialize_by(user_id: @user.id, new_email: email) @change_req = EmailChangeRequest.find_or_initialize_by(user_id: @user.id, new_email: email)
if change_req.new_record? if @change_req.new_record?
change_req.requested_by = @guardian.user @change_req.requested_by = @guardian.user
change_req.old_email = old_email @change_req.old_email = old_email
change_req.new_email = email @change_req.new_email = email
end end
if change_req.change_state.blank? || change_req.change_state == EmailChangeRequest.states[:complete] if @change_req.change_state.blank? || @change_req.change_state == EmailChangeRequest.states[:complete]
change_req.change_state = if @user.staff? @change_req.change_state = if @user.staff?
# Staff users must confirm their old email address first. # Staff users must confirm their old email address first.
EmailChangeRequest.states[:authorizing_old] EmailChangeRequest.states[:authorizing_old]
else else
@ -65,51 +66,53 @@ class EmailUpdater
end end
end end
if change_req.change_state == EmailChangeRequest.states[:authorizing_old] if @change_req.change_state == EmailChangeRequest.states[:authorizing_old]
change_req.old_email_token = @user.email_tokens.create!(email: @user.email) @change_req.old_email_token = @user.email_tokens.create!(email: @user.email, scope: EmailToken.scopes[:email_update])
send_email(add ? :confirm_old_email_add : :confirm_old_email, change_req.old_email_token) send_email(add ? :confirm_old_email_add : :confirm_old_email, @change_req.old_email_token)
elsif change_req.change_state == EmailChangeRequest.states[:authorizing_new] elsif @change_req.change_state == EmailChangeRequest.states[:authorizing_new]
change_req.new_email_token = @user.email_tokens.create!(email: email) @change_req.new_email_token = @user.email_tokens.create!(email: email, scope: EmailToken.scopes[:email_update])
send_email(:confirm_new_email, change_req.new_email_token) send_email(:confirm_new_email, @change_req.new_email_token)
end end
change_req.save! @change_req.save!
@change_req
end end
def confirm(token) def confirm(token)
confirm_result = nil confirm_result = nil
User.transaction do User.transaction do
result = EmailToken.atomic_confirm(token) email_token = EmailToken.confirmable(token, scope: EmailToken.scopes[:email_update])
if result[:success] if email_token.blank?
token = result[:email_token]
@user = token.user
change_req = @user.email_change_requests
.where('old_email_token_id = :token_id OR new_email_token_id = :token_id', token_id: token.id)
.first
case change_req.try(:change_state)
when EmailChangeRequest.states[:authorizing_old]
change_req.update!(
change_state: EmailChangeRequest.states[:authorizing_new],
new_email_token: @user.email_tokens.create(email: change_req.new_email)
)
send_email(:confirm_new_email, change_req.new_email_token)
confirm_result = :authorizing_new
when EmailChangeRequest.states[:authorizing_new]
change_req.update!(change_state: EmailChangeRequest.states[:complete])
if !@user.staff?
# Send an email notification only to users who did not confirm old
# email.
send_email_notification(change_req.old_email, change_req.new_email)
end
update_user_email(change_req.old_email, change_req.new_email)
confirm_result = :complete
end
else
errors.add(:base, I18n.t('change_email.already_done')) errors.add(:base, I18n.t('change_email.already_done'))
confirm_result = :error confirm_result = :error
next
end
email_token.update!(confirmed: true)
@user = email_token.user
@change_req = @user.email_change_requests
.where('old_email_token_id = :token_id OR new_email_token_id = :token_id', token_id: email_token.id)
.first
case @change_req.try(:change_state)
when EmailChangeRequest.states[:authorizing_old]
@change_req.update!(
change_state: EmailChangeRequest.states[:authorizing_new],
new_email_token: @user.email_tokens.create!(email: @change_req.new_email, scope: EmailToken.scopes[:email_update])
)
send_email(:confirm_new_email, @change_req.new_email_token)
confirm_result = :authorizing_new
when EmailChangeRequest.states[:authorizing_new]
@change_req.update!(change_state: EmailChangeRequest.states[:complete])
if !@user.staff?
# Send an email notification only to users who did not confirm old
# email.
send_email_notification(@change_req.old_email, @change_req.new_email)
end
update_user_email(@change_req.old_email, @change_req.new_email)
confirm_result = :complete
end end
end end

View File

@ -27,7 +27,7 @@ task "admin:invite", [:email] => [:environment] do |_, args|
user.email_tokens.update_all confirmed: true user.email_tokens.update_all confirmed: true
puts "Sending email!" puts "Sending email!"
email_token = user.email_tokens.create(email: user.email) email_token = user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:signup])
Jobs.enqueue(:user_email, type: :account_created, user_id: user.id, email_token: email_token.token) Jobs.enqueue(:user_email, type: :account_created, user_id: user.id, email_token: email_token.token)
end end

View File

@ -47,22 +47,20 @@ describe EmailUpdater do
it "logs the admin user as the requester" do it "logs the admin user as the requester" do
updater.change_to(new_email) updater.change_to(new_email)
@change_req = user.email_change_requests.first expect(updater.change_req.requested_by).to eq(admin)
expect(@change_req.requested_by).to eq(admin)
end end
it "starts the new confirmation process" do it "starts the new confirmation process" do
updater.change_to(new_email) updater.change_to(new_email)
@change_req = user.email_change_requests.first
expect(updater.errors).to be_blank expect(updater.errors).to be_blank
expect(@change_req).to be_present expect(updater.change_req).to be_present
expect(@change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_new]) expect(updater.change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_new])
expect(@change_req.old_email).to eq(old_email) expect(updater.change_req.old_email).to eq(old_email)
expect(@change_req.new_email).to eq(new_email) expect(updater.change_req.new_email).to eq(new_email)
expect(@change_req.old_email_token).to be_blank expect(updater.change_req.old_email_token).to be_blank
expect(@change_req.new_email_token.email).to eq(new_email) expect(updater.change_req.new_email_token.email).to eq(new_email)
end end
end end
@ -73,24 +71,22 @@ describe EmailUpdater do
expect_enqueued_with(job: :critical_user_email, args: { type: :confirm_old_email, to_address: old_email }) do expect_enqueued_with(job: :critical_user_email, args: { type: :confirm_old_email, to_address: old_email }) do
updater.change_to(new_email) updater.change_to(new_email)
end end
@change_req = user.email_change_requests.first
end end
it "starts the old confirmation process" do it "starts the old confirmation process" do
expect(updater.errors).to be_blank expect(updater.errors).to be_blank
expect(@change_req.old_email).to eq(old_email) expect(updater.change_req.old_email).to eq(old_email)
expect(@change_req.new_email).to eq(new_email) expect(updater.change_req.new_email).to eq(new_email)
expect(@change_req).to be_present expect(updater.change_req).to be_present
expect(@change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_old]) expect(updater.change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_old])
expect(@change_req.old_email_token.email).to eq(old_email) expect(updater.change_req.old_email_token.email).to eq(old_email)
expect(@change_req.new_email_token).to be_blank expect(updater.change_req.new_email_token).to be_blank
end end
it "does not immediately confirm the request" do it "does not immediately confirm the request" do
expect(@change_req.change_state).not_to eq(EmailChangeRequest.states[:complete]) expect(updater.change_req.change_state).not_to eq(EmailChangeRequest.states[:complete])
end end
end end
@ -103,30 +99,27 @@ describe EmailUpdater do
expect_enqueued_with(job: :critical_user_email, args: { type: :confirm_old_email, to_address: old_email }) do expect_enqueued_with(job: :critical_user_email, args: { type: :confirm_old_email, to_address: old_email }) do
updater.change_to(new_email) updater.change_to(new_email)
end end
@change_req = user.email_change_requests.first
end end
it "logs the user as the requester" do it "logs the user as the requester" do
updater.change_to(new_email) updater.change_to(new_email)
@change_req = user.email_change_requests.first expect(updater.change_req.requested_by).to eq(user)
expect(@change_req.requested_by).to eq(user)
end end
it "starts the old confirmation process" do it "starts the old confirmation process" do
expect(updater.errors).to be_blank expect(updater.errors).to be_blank
expect(@change_req.old_email).to eq(old_email) expect(updater.change_req.old_email).to eq(old_email)
expect(@change_req.new_email).to eq(new_email) expect(updater.change_req.new_email).to eq(new_email)
expect(@change_req).to be_present expect(updater.change_req).to be_present
expect(@change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_old]) expect(updater.change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_old])
expect(@change_req.old_email_token.email).to eq(old_email) expect(updater.change_req.old_email_token.email).to eq(old_email)
expect(@change_req.new_email_token).to be_blank expect(updater.change_req.new_email_token).to be_blank
end end
it "does not immediately confirm the request" do it "does not immediately confirm the request" do
expect(@change_req.change_state).not_to eq(EmailChangeRequest.states[:complete]) expect(updater.change_req.change_state).not_to eq(EmailChangeRequest.states[:complete])
end end
end end
end end
@ -140,20 +133,18 @@ describe EmailUpdater do
expect_enqueued_with(job: :critical_user_email, args: { type: :confirm_new_email, to_address: new_email }) do expect_enqueued_with(job: :critical_user_email, args: { type: :confirm_new_email, to_address: new_email }) do
updater.change_to(new_email) updater.change_to(new_email)
end end
@change_req = user.email_change_requests.first
end end
it "starts the new confirmation process" do it "starts the new confirmation process" do
expect(updater.errors).to be_blank expect(updater.errors).to be_blank
expect(@change_req).to be_present expect(updater.change_req).to be_present
expect(@change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_new]) expect(updater.change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_new])
expect(@change_req.old_email).to eq(old_email) expect(updater.change_req.old_email).to eq(old_email)
expect(@change_req.new_email).to eq(new_email) expect(updater.change_req.new_email).to eq(new_email)
expect(@change_req.old_email_token).to be_blank expect(updater.change_req.old_email_token).to be_blank
expect(@change_req.new_email_token.email).to eq(new_email) expect(updater.change_req.new_email_token.email).to eq(new_email)
end end
context 'confirming an invalid token' do context 'confirming an invalid token' do
@ -168,7 +159,7 @@ describe EmailUpdater do
it "updates the user's email" do it "updates the user's email" do
event = DiscourseEvent.track_events { event = DiscourseEvent.track_events {
expect_enqueued_with(job: :critical_user_email, args: { type: :notify_old_email, to_address: old_email }) do expect_enqueued_with(job: :critical_user_email, args: { type: :notify_old_email, to_address: old_email }) do
updater.confirm(@change_req.new_email_token.token) updater.confirm(updater.change_req.new_email_token.token)
end end
}.last }.last
@ -178,8 +169,8 @@ describe EmailUpdater do
expect(event[:event_name]).to eq(:user_updated) expect(event[:event_name]).to eq(:user_updated)
expect(event[:params].first).to eq(user) expect(event[:params].first).to eq(user)
@change_req.reload updater.change_req.reload
expect(@change_req.change_state).to eq(EmailChangeRequest.states[:complete]) expect(updater.change_req.change_state).to eq(EmailChangeRequest.states[:complete])
end end
end end
end end
@ -189,8 +180,6 @@ describe EmailUpdater do
expect_enqueued_with(job: :critical_user_email, args: { type: :confirm_new_email, to_address: new_email }) do expect_enqueued_with(job: :critical_user_email, args: { type: :confirm_new_email, to_address: new_email }) do
updater.change_to(new_email, add: true) updater.change_to(new_email, add: true)
end end
@change_req = user.email_change_requests.first
end end
context 'confirming a valid token' do context 'confirming a valid token' do
@ -199,7 +188,7 @@ describe EmailUpdater do
event = DiscourseEvent.track_events { event = DiscourseEvent.track_events {
expect_enqueued_with(job: :critical_user_email, args: { type: :notify_old_email_add, to_address: old_email }) do expect_enqueued_with(job: :critical_user_email, args: { type: :notify_old_email_add, to_address: old_email }) do
updater.confirm(@change_req.new_email_token.token) updater.confirm(updater.change_req.new_email_token.token)
end end
}.last }.last
@ -209,15 +198,15 @@ describe EmailUpdater do
expect(event[:event_name]).to eq(:user_updated) expect(event[:event_name]).to eq(:user_updated)
expect(event[:params].first).to eq(user) expect(event[:params].first).to eq(user)
@change_req.reload updater.change_req.reload
expect(@change_req.change_state).to eq(EmailChangeRequest.states[:complete]) expect(updater.change_req.change_state).to eq(EmailChangeRequest.states[:complete])
end end
end end
context 'that was deleted before' do context 'that was deleted before' do
it 'works' do it 'works' do
expect_enqueued_with(job: :critical_user_email, args: { type: :notify_old_email_add, to_address: old_email }) do expect_enqueued_with(job: :critical_user_email, args: { type: :notify_old_email_add, to_address: old_email }) do
updater.confirm(@change_req.new_email_token.token) updater.confirm(updater.change_req.new_email_token.token)
end end
expect(user.reload.user_emails.pluck(:email)).to contain_exactly(old_email, new_email) expect(user.reload.user_emails.pluck(:email)).to contain_exactly(old_email, new_email)
@ -229,10 +218,8 @@ describe EmailUpdater do
updater.change_to(new_email, add: true) updater.change_to(new_email, add: true)
end end
@change_req = user.email_change_requests.first
expect_enqueued_with(job: :critical_user_email, args: { type: :notify_old_email_add, to_address: old_email }) do expect_enqueued_with(job: :critical_user_email, args: { type: :notify_old_email_add, to_address: old_email }) do
updater.confirm(@change_req.new_email_token.token) updater.confirm(updater.change_req.new_email_token.token)
end end
expect(user.reload.user_emails.pluck(:email)).to contain_exactly(old_email, new_email) expect(user.reload.user_emails.pluck(:email)).to contain_exactly(old_email, new_email)
@ -266,20 +253,18 @@ describe EmailUpdater do
expect_enqueued_with(job: :critical_user_email, args: { type: :confirm_old_email, to_address: old_email }) do expect_enqueued_with(job: :critical_user_email, args: { type: :confirm_old_email, to_address: old_email }) do
updater.change_to(new_email) updater.change_to(new_email)
end end
@change_req = user.email_change_requests.first
end end
it "starts the old confirmation process" do it "starts the old confirmation process" do
expect(updater.errors).to be_blank expect(updater.errors).to be_blank
expect(@change_req.old_email).to eq(old_email) expect(updater.change_req.old_email).to eq(old_email)
expect(@change_req.new_email).to eq(new_email) expect(updater.change_req.new_email).to eq(new_email)
expect(@change_req).to be_present expect(updater.change_req).to be_present
expect(@change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_old]) expect(updater.change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_old])
expect(@change_req.old_email_token.email).to eq(old_email) expect(updater.change_req.old_email_token.email).to eq(old_email)
expect(@change_req.new_email_token).to be_blank expect(updater.change_req.new_email_token).to be_blank
end end
context 'confirming an invalid token' do context 'confirming an invalid token' do
@ -293,34 +278,33 @@ describe EmailUpdater do
context 'confirming a valid token' do context 'confirming a valid token' do
before do before do
expect_enqueued_with(job: :critical_user_email, args: { type: :confirm_new_email, to_address: new_email }) do expect_enqueued_with(job: :critical_user_email, args: { type: :confirm_new_email, to_address: new_email }) do
updater.confirm(@change_req.old_email_token.token) @old_token = updater.change_req.old_email_token.token
updater.confirm(@old_token)
end end
@change_req.reload
end end
it "starts the new update process" do it "starts the new update process" do
expect(updater.errors).to be_blank expect(updater.errors).to be_blank
expect(user.reload.email).to eq(old_email) expect(user.reload.email).to eq(old_email)
expect(@change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_new]) expect(updater.change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_new])
expect(@change_req.new_email_token).to be_present expect(updater.change_req.new_email_token).to be_present
end end
it "cannot be confirmed twice" do it "cannot be confirmed twice" do
updater.confirm(@change_req.old_email_token.token) updater.confirm(@old_token)
expect(updater.errors).to be_present expect(updater.errors).to be_present
expect(user.reload.email).to eq(old_email) expect(user.reload.email).to eq(old_email)
@change_req.reload updater.change_req.reload
expect(@change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_new]) expect(updater.change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_new])
expect(@change_req.new_email_token.email).to eq(new_email) expect(updater.change_req.new_email_token.email).to eq(new_email)
end end
context "completing the new update process" do context "completing the new update process" do
before do before do
expect_not_enqueued_with(job: :critical_user_email, args: { type: :notify_old_email, to_address: old_email }) do expect_not_enqueued_with(job: :critical_user_email, args: { type: :notify_old_email, to_address: old_email }) do
updater.confirm(@change_req.new_email_token.token) updater.confirm(updater.change_req.new_email_token.token)
end end
end end
@ -328,8 +312,8 @@ describe EmailUpdater do
expect(updater.errors).to be_blank expect(updater.errors).to be_blank
expect(user.reload.email).to eq(new_email) expect(user.reload.email).to eq(new_email)
@change_req.reload updater.change_req.reload
expect(@change_req.change_state).to eq(EmailChangeRequest.states[:complete]) expect(updater.change_req.change_state).to eq(EmailChangeRequest.states[:complete])
end end
end end
end end

View File

@ -3,4 +3,5 @@
Fabricator(:email_token) do Fabricator(:email_token) do
user user
email { |attrs| attrs[:user].email } email { |attrs| attrs[:user].email }
scope EmailToken.scopes[:signup]
end end

View File

@ -12,9 +12,9 @@ describe Jobs::AutomaticGroupMembership do
user1 = Fabricate(:user, email: "no@bar.com") user1 = Fabricate(:user, email: "no@bar.com")
user2 = Fabricate(:user, email: "no@wat.com") user2 = Fabricate(:user, email: "no@wat.com")
user3 = Fabricate(:user, email: "noo@wat.com", staged: true) user3 = Fabricate(:user, email: "noo@wat.com", staged: true)
EmailToken.confirm(user3.email_tokens.last.token) EmailToken.confirm(Fabricate(:email_token, user: user3).token)
user4 = Fabricate(:user, email: "yes@wat.com") user4 = Fabricate(:user, email: "yes@wat.com")
EmailToken.confirm(user4.email_tokens.last.token) EmailToken.confirm(Fabricate(:email_token, user: user4).token)
user5 = Fabricate(:user, email: "sso@wat.com") user5 = Fabricate(:user, email: "sso@wat.com")
user5.create_single_sign_on_record(external_id: 123, external_email: "hacker@wat.com", last_payload: "") user5.create_single_sign_on_record(external_id: 123, external_email: "hacker@wat.com", last_payload: "")
user6 = Fabricate(:user, email: "sso2@wat.com") user6 = Fabricate(:user, email: "sso2@wat.com")

View File

@ -121,7 +121,7 @@ describe UserNotifications do
end end
describe '.email_login' do describe '.email_login' do
let(:email_token) { user.email_tokens.create!(email: user.email).token } let(:email_token) { Fabricate(:email_token, user: user, scope: EmailToken.scopes[:email_login]).token }
subject { UserNotifications.email_login(user, email_token: email_token) } subject { UserNotifications.email_login(user, email_token: email_token) }
it "generates the right email" do it "generates the right email" do

View File

@ -3,7 +3,6 @@
require 'rails_helper' require 'rails_helper'
describe EmailToken do describe EmailToken do
it { is_expected.to validate_presence_of :user_id } it { is_expected.to validate_presence_of :user_id }
it { is_expected.to validate_presence_of :email } it { is_expected.to validate_presence_of :email }
it { is_expected.to belong_to :user } it { is_expected.to belong_to :user }
@ -11,14 +10,14 @@ describe EmailToken do
context '#create' do context '#create' do
fab!(:user) { Fabricate(:user, active: false) } fab!(:user) { Fabricate(:user, active: false) }
let!(:original_token) { user.email_tokens.first } let!(:original_token) { user.email_tokens.first }
let!(:email_token) { user.email_tokens.create(email: 'bubblegum@adventuretime.ooo') } let!(:email_token) { Fabricate(:email_token, user: user, email: 'bubblegum@adventuretime.ooo') }
it 'should create the email token' do it 'should create the email token' do
expect(email_token).to be_present expect(email_token).to be_present
end end
it 'should downcase the email' do it 'should downcase the email' do
token = user.email_tokens.create(email: "UpperCaseSoWoW@GMail.com") token = Fabricate(:email_token, user: user, email: "UpperCaseSoWoW@GMail.com")
expect(token.email).to eq "uppercasesowow@gmail.com" expect(token.email).to eq "uppercasesowow@gmail.com"
end end
@ -45,20 +44,15 @@ describe EmailToken do
end end
context '#confirm' do context '#confirm' do
fab!(:user) { Fabricate(:user, active: false) } fab!(:user) { Fabricate(:user, active: false) }
let(:email_token) { user.email_tokens.first } let!(:email_token) { Fabricate(:email_token, user: user) }
it 'returns nil with a nil token' do it 'returns nil with a nil token' do
expect(EmailToken.confirm(nil)).to be_blank expect(EmailToken.confirm(nil)).to be_blank
end end
it 'returns nil with a made up token' do it 'returns nil with an invalid token' do
expect(EmailToken.confirm(EmailToken.generate_token)).to be_blank expect(EmailToken.confirm("random token")).to be_blank
end
it 'returns nil unless the token is the right length' do
expect(EmailToken.confirm('a')).to be_blank
end end
it 'returns nil when a token is expired' do it 'returns nil when a token is expired' do
@ -73,7 +67,6 @@ describe EmailToken do
end end
context 'taken email address' do context 'taken email address' do
before do before do
@other_user = Fabricate(:coding_horror) @other_user = Fabricate(:coding_horror)
email_token.update_attribute :email, @other_user.email email_token.update_attribute :email, @other_user.email
@ -82,7 +75,6 @@ describe EmailToken do
it 'returns nil when the email has been taken since the token has been generated' do it 'returns nil when the email has been taken since the token has been generated' do
expect(EmailToken.confirm(email_token.token)).to be_blank expect(EmailToken.confirm(email_token.token)).to be_blank
end end
end end
context 'welcome message' do context 'welcome message' do
@ -94,7 +86,6 @@ describe EmailToken do
end end
context 'success' do context 'success' do
let!(:confirmed_user) { EmailToken.confirm(email_token.token) } let!(:confirmed_user) { EmailToken.confirm(email_token.token) }
it "returns the correct user" do it "returns the correct user" do
@ -124,7 +115,7 @@ describe EmailToken do
fab!(:invite) { Fabricate(:invite, email: 'test@example.com') } fab!(:invite) { Fabricate(:invite, email: 'test@example.com') }
fab!(:invited_user) { Fabricate(:user, active: false, email: invite.email) } fab!(:invited_user) { Fabricate(:user, active: false, email: invite.email) }
let(:user_email_token) { invited_user.email_tokens.first } let!(:user_email_token) { Fabricate(:email_token, user: invited_user) }
let!(:confirmed_invited_user) { EmailToken.confirm(user_email_token.token) } let!(:confirmed_invited_user) { EmailToken.confirm(user_email_token.token) }
it "returns the correct user" do it "returns the correct user" do
@ -151,5 +142,4 @@ describe EmailToken do
end end
end end
end end
end end

View File

@ -234,7 +234,7 @@ describe User do
reviewable = ReviewableUser.find_by(target: user) reviewable = ReviewableUser.find_by(target: user)
expect(reviewable).to be_blank expect(reviewable).to be_blank
EmailToken.confirm(user.email_tokens.first.token) EmailToken.confirm(Fabricate(:email_token, user: user).token)
expect(user.reload.active).to eq(true) expect(user.reload.active).to eq(true)
reviewable = ReviewableUser.find_by(target: user) reviewable = ReviewableUser.find_by(target: user)
expect(reviewable).to be_present expect(reviewable).to be_present
@ -876,7 +876,7 @@ describe User do
expect(@user.active).to eq(false) expect(@user.active).to eq(false)
expect(@user.confirm_password?("ilovepasta")).to eq(true) expect(@user.confirm_password?("ilovepasta")).to eq(true)
email_token = @user.email_tokens.create(email: 'pasta@delicious.com') email_token = Fabricate(:email_token, user: @user, email: 'pasta@delicious.com')
UserAuthToken.generate!(user_id: @user.id) UserAuthToken.generate!(user_id: @user.id)
@ -1073,7 +1073,7 @@ describe User do
context 'when email has been confirmed' do context 'when email has been confirmed' do
it 'should return true' do it 'should return true' do
token = user.email_tokens.find_by(email: user.email) token = Fabricate(:email_token, user: user)
EmailToken.confirm(token.token) EmailToken.confirm(token.token)
expect(user.email_confirmed?).to eq(true) expect(user.email_confirmed?).to eq(true)
end end
@ -1549,14 +1549,14 @@ describe User do
it "doesn't automatically add staged users" do it "doesn't automatically add staged users" do
staged_user = Fabricate(:user, active: true, staged: true, email: "wat@wat.com") staged_user = Fabricate(:user, active: true, staged: true, email: "wat@wat.com")
EmailToken.confirm(staged_user.email_tokens.last.token) EmailToken.confirm(Fabricate(:email_token, user: staged_user).token)
group.reload group.reload
expect(group.users.include?(staged_user)).to eq(false) expect(group.users.include?(staged_user)).to eq(false)
end end
it "is automatically added to a group when the email matches" do it "is automatically added to a group when the email matches" do
user = Fabricate(:user, active: true, email: "foo@bar.com") user = Fabricate(:user, active: true, email: "foo@bar.com")
EmailToken.confirm(user.email_tokens.last.token) EmailToken.confirm(Fabricate(:email_token, user: user).token)
group.reload group.reload
expect(group.users.include?(user)).to eq(true) expect(group.users.include?(user)).to eq(true)
@ -1585,7 +1585,7 @@ describe User do
user.password_required! user.password_required!
user.save! user.save!
EmailToken.confirm(user.email_tokens.last.token) EmailToken.confirm(Fabricate(:email_token, user: user).token)
user.reload user.reload
expect(user.title).to eq("bars and wats") expect(user.title).to eq("bars and wats")

View File

@ -333,7 +333,7 @@ describe WebHook do
payload = JSON.parse(job_args["payload"]) payload = JSON.parse(job_args["payload"])
expect(payload["id"]).to eq(user.id) expect(payload["id"]).to eq(user.id)
email_token = user.email_tokens.create(email: user.email) email_token = Fabricate(:email_token, user: user)
EmailToken.confirm(email_token.token) EmailToken.confirm(email_token.token)
job_args = Jobs::EmitWebHookEvent.jobs.last["args"].first job_args = Jobs::EmitWebHookEvent.jobs.last["args"].first

View File

@ -567,11 +567,13 @@ describe 'users' do
expected_response_schema = nil expected_response_schema = nil
let(:user) { Fabricate(:user) } let(:user) { Fabricate(:user) }
let(:token) { user.email_tokens.create(email: user.email).token } let(:token) { Fabricate(:email_token, user: user, scope: EmailToken.scopes[:password_reset]).token }
let(:params) { { let(:params) do
'username' => user.username, {
'password' => 'NH8QYbxYS5Zv5qEFzA4jULvM' 'username' => user.username,
} } 'password' => 'NH8QYbxYS5Zv5qEFzA4jULvM'
}
end
it_behaves_like "a JSON endpoint", 200 do it_behaves_like "a JSON endpoint", 200 do
let(:expected_response_schema) { expected_response_schema } let(:expected_response_schema) { expected_response_schema }

View File

@ -3,10 +3,9 @@
require 'rails_helper' require 'rails_helper'
require 'rotp' require 'rotp'
RSpec.describe SessionController do describe SessionController do
let(:email_token) { Fabricate(:email_token) } let(:user) { Fabricate(:user) }
let(:user) { email_token.user } let(:email_token) { Fabricate(:email_token, user: user) }
let(:logo_fixture) { "http://#{Discourse.current_hostname}/uploads/logo.png" }
shared_examples 'failed to continue local login' do shared_examples 'failed to continue local login' do
it 'should return the right response' do it 'should return the right response' do
@ -16,6 +15,8 @@ RSpec.describe SessionController do
end end
describe '#email_login_info' do describe '#email_login_info' do
let(:email_token) { Fabricate(:email_token, user: user, scope: EmailToken.scopes[:email_login]) }
before do before do
SiteSetting.enable_local_logins_via_email = true SiteSetting.enable_local_logins_via_email = true
end end
@ -118,6 +119,8 @@ RSpec.describe SessionController do
end end
describe '#email_login' do describe '#email_login' do
let(:email_token) { Fabricate(:email_token, user: user, scope: EmailToken.scopes[:email_login]) }
before do before do
SiteSetting.enable_local_logins_via_email = true SiteSetting.enable_local_logins_via_email = true
end end
@ -200,7 +203,6 @@ RSpec.describe SessionController do
post "/session/email-login/#{email_token.token}.json" post "/session/email-login/#{email_token.token}.json"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.parsed_body["error"]).to eq(I18n.t("login.not_approved")) expect(response.parsed_body["error"]).to eq(I18n.t("login.not_approved"))
expect(session[:current_user_id]).to eq(nil) expect(session[:current_user_id]).to eq(nil)
end end
@ -1112,6 +1114,8 @@ RSpec.describe SessionController do
let(:headers) { { host: Discourse.current_hostname } } let(:headers) { { host: Discourse.current_hostname } }
describe 'can act as an SSO provider' do describe 'can act as an SSO provider' do
let(:logo_fixture) { "http://#{Discourse.current_hostname}/uploads/logo.png" }
before do before do
stub_request(:any, /#{Discourse.current_hostname}\/uploads/).to_return( stub_request(:any, /#{Discourse.current_hostname}\/uploads/).to_return(
status: 200, status: 200,
@ -1315,8 +1319,6 @@ RSpec.describe SessionController do
end end
describe '#create' do describe '#create' do
let(:user) { Fabricate(:user) }
context 'local login is disabled' do context 'local login is disabled' do
before do before do
SiteSetting.enable_local_logins = false SiteSetting.enable_local_logins = false
@ -1354,8 +1356,7 @@ RSpec.describe SessionController do
context 'when email is confirmed' do context 'when email is confirmed' do
before do before do
token = user.email_tokens.find_by(email: user.email) EmailToken.confirm(email_token.token)
EmailToken.confirm(token.token)
end end
it "raises an error when the login isn't present" do it "raises an error when the login isn't present" do
@ -1508,6 +1509,7 @@ RSpec.describe SessionController do
)) ))
end end
end end
context "when the security key params are invalid" do context "when the security key params are invalid" do
it "shows an error message and denies login" do it "shows an error message and denies login" do
@ -1532,9 +1534,9 @@ RSpec.describe SessionController do
)) ))
end end
end end
context "when the security key params are valid" do context "when the security key params are valid" do
it "logs the user in" do it "logs the user in" do
post "/session.json", params: { post "/session.json", params: {
login: user.username, login: user.username,
password: 'myawesomepassword', password: 'myawesomepassword',
@ -1549,6 +1551,7 @@ RSpec.describe SessionController do
expect(user.user_auth_tokens.count).to eq(1) expect(user.user_auth_tokens.count).to eq(1)
end end
end end
context "when the security key is disabled in the background by the user and TOTP is enabled" do context "when the security key is disabled in the background by the user and TOTP is enabled" do
before do before do
user_security_key.destroy! user_security_key.destroy!
@ -1556,7 +1559,6 @@ RSpec.describe SessionController do
end end
it "shows an error message and denies login" do it "shows an error message and denies login" do
post "/session.json", params: { post "/session.json", params: {
login: user.username, login: user.username,
password: 'myawesomepassword', password: 'myawesomepassword',
@ -1609,6 +1611,7 @@ RSpec.describe SessionController do
)) ))
end end
end end
context 'when using backup code method' do context 'when using backup code method' do
it 'should return the right response' do it 'should return the right response' do
post "/session.json", params: { post "/session.json", params: {
@ -1646,6 +1649,7 @@ RSpec.describe SessionController do
.to eq(user.user_auth_tokens.first.auth_token) .to eq(user.user_auth_tokens.first.auth_token)
end end
end end
context 'when using backup code method' do context 'when using backup code method' do
it 'should log the user in' do it 'should log the user in' do
post "/session.json", params: { post "/session.json", params: {

View File

@ -42,12 +42,7 @@ describe UsersController do
end end
describe '#perform_account_activation' do describe '#perform_account_activation' do
let(:token) do let(:email_token) { Fabricate(:email_token, user: user) }
return @token if @token.present?
email_token = EmailToken.create!(expired: false, confirmed: false, user: user, email: user.email)
@token = email_token.token
@token
end
before do before do
UsersController.any_instance.stubs(:honeypot_or_challenge_fails?).returns(false) UsersController.any_instance.stubs(:honeypot_or_challenge_fails?).returns(false)
@ -66,7 +61,7 @@ describe UsersController do
it 'enqueues a welcome message if the user object indicates so' do it 'enqueues a welcome message if the user object indicates so' do
SiteSetting.send_welcome_message = true SiteSetting.send_welcome_message = true
user.update(active: false) user.update(active: false)
put "/u/activate-account/#{token}" put "/u/activate-account/#{email_token.token}"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(Jobs::SendSystemMessage.jobs.size).to eq(1) expect(Jobs::SendSystemMessage.jobs.size).to eq(1)
expect(Jobs::SendSystemMessage.jobs.first["args"].first["message_type"]).to eq("welcome_user") expect(Jobs::SendSystemMessage.jobs.first["args"].first["message_type"]).to eq("welcome_user")
@ -74,7 +69,7 @@ describe UsersController do
it "doesn't enqueue the welcome message if the object returns false" do it "doesn't enqueue the welcome message if the object returns false" do
user.update(active: true) user.update(active: true)
put "/u/activate-account/#{token}" put "/u/activate-account/#{email_token.token}"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(Jobs::SendSystemMessage.jobs.size).to eq(0) expect(Jobs::SendSystemMessage.jobs.size).to eq(0)
end end
@ -83,24 +78,21 @@ describe UsersController do
context "honeypot" do context "honeypot" do
it "raises an error if the honeypot is invalid" do it "raises an error if the honeypot is invalid" do
UsersController.any_instance.stubs(:honeypot_or_challenge_fails?).returns(true) UsersController.any_instance.stubs(:honeypot_or_challenge_fails?).returns(true)
put "/u/activate-account/#{token}" put "/u/activate-account/#{email_token.token}"
expect(response.status).to eq(403) expect(response.status).to eq(403)
end end
end end
context 'response' do context 'response' do
before do
Guardian.any_instance.expects(:can_access_forum?).returns(true)
EmailToken.expects(:confirm).with("#{token}").returns(user)
end
it 'correctly logs on user' do it 'correctly logs on user' do
email_token
events = DiscourseEvent.track_events do events = DiscourseEvent.track_events do
put "/u/activate-account/#{token}" put "/u/activate-account/#{email_token.token}"
end end
expect(events.map { |event| event[:event_name] }).to contain_exactly( expect(events.map { |event| event[:event_name] }).to contain_exactly(
:user_logged_in, :user_first_logged_in :user_confirmed_email, :user_first_logged_in, :user_logged_in
) )
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -115,11 +107,10 @@ describe UsersController do
context 'user is not approved' do context 'user is not approved' do
before do before do
SiteSetting.must_approve_users = true SiteSetting.must_approve_users = true
EmailToken.expects(:confirm).with("#{token}").returns(user)
put "/u/activate-account/#{token}"
end end
it 'should return the right response' do it 'should return the right response' do
put "/u/activate-account/#{email_token.token}"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(CGI.unescapeHTML(response.body)) expect(CGI.unescapeHTML(response.body))
@ -140,7 +131,7 @@ describe UsersController do
destination_url = 'http://thisisasite.com/somepath' destination_url = 'http://thisisasite.com/somepath'
cookies[:destination_url] = destination_url cookies[:destination_url] = destination_url
put "/u/activate-account/#{token}" put "/u/activate-account/#{email_token.token}"
expect(response).to redirect_to(destination_url) expect(response).to redirect_to(destination_url)
end end
@ -211,23 +202,21 @@ describe UsersController do
end end
context 'valid token' do context 'valid token' do
let!(:user) { Fabricate(:user) }
let!(:user_auth_token) { UserAuthToken.generate!(user_id: user.id) }
let!(:email_token) { Fabricate(:email_token, user: user, scope: EmailToken.scopes[:password_reset]) }
context 'when rendered' do context 'when rendered' do
it 'renders referrer never on get requests' do it 'renders referrer never on get requests' do
user = Fabricate(:user) get "/u/password-reset/#{email_token.token}"
token = user.email_tokens.create(email: user.email).token
get "/u/password-reset/#{token}"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.body).to include('<meta name="referrer" content="never">') expect(response.body).to include('<meta name="referrer" content="never">')
end end
end end
it 'returns success' do it 'returns success' do
user = Fabricate(:user)
user_auth_token = UserAuthToken.generate!(user_id: user.id)
token = user.email_tokens.create(email: user.email).token
events = DiscourseEvent.track_events do events = DiscourseEvent.track_events do
put "/u/password-reset/#{token}", params: { password: 'hg9ow8yhg98o' } put "/u/password-reset/#{email_token.token}", params: { password: 'hg9ow8yhg98o' }
end end
expect(events.map { |event| event[:event_name] }).to contain_exactly( expect(events.map { |event| event[:event_name] }).to contain_exactly(
@ -240,87 +229,57 @@ describe UsersController do
expect(json['password_reset']).to include('{"is_developer":false,"admin":false,"second_factor_required":false,"security_key_required":false,"backup_enabled":false,"multiple_second_factor_methods":false}') expect(json['password_reset']).to include('{"is_developer":false,"admin":false,"second_factor_required":false,"security_key_required":false,"backup_enabled":false,"multiple_second_factor_methods":false}')
end end
expect(session["password-#{token}"]).to be_blank expect(session["password-#{email_token.token}"]).to be_blank
expect(UserAuthToken.where(id: user_auth_token.id).count).to eq(0) expect(UserAuthToken.where(id: user_auth_token.id).count).to eq(0)
end end
it 'disallows double password reset' do it 'disallows double password reset' do
user = Fabricate(:user) put "/u/password-reset/#{email_token.token}", params: { password: 'hg9ow8yHG32O' }
token = user.email_tokens.create(email: user.email).token put "/u/password-reset/#{email_token.token}", params: { password: 'test123987AsdfXYZ' }
expect(user.reload.confirm_password?('hg9ow8yHG32O')).to eq(true)
put "/u/password-reset/#{token}", params: { password: 'hg9ow8yHG32O' }
put "/u/password-reset/#{token}", params: { password: 'test123987AsdfXYZ' }
user.reload
expect(user.confirm_password?('hg9ow8yHG32O')).to eq(true)
# logged in now
expect(user.user_auth_tokens.count).to eq(1) expect(user.user_auth_tokens.count).to eq(1)
end end
it "doesn't redirect to wizard on get" do it "doesn't redirect to wizard on get" do
user = Fabricate(:admin) user.update!(admin: true)
UserAuthToken.generate!(user_id: user.id)
token = user.email_tokens.create(email: user.email).token get "/u/password-reset/#{email_token.token}.json"
get "/u/password-reset/#{token}.json"
expect(response).not_to redirect_to(wizard_path) expect(response).not_to redirect_to(wizard_path)
end end
it "redirects to the wizard if you're the first admin" do it "redirects to the wizard if you're the first admin" do
user = Fabricate(:admin) user.update!(admin: true)
UserAuthToken.generate!(user_id: user.id)
token = user.email_tokens.create(email: user.email).token
get "/u/password-reset/#{token}"
put "/u/password-reset/#{token}", params: { password: 'hg9ow8yhg98oadminlonger' }
get "/u/password-reset/#{email_token.token}"
put "/u/password-reset/#{email_token.token}", params: { password: 'hg9ow8yhg98oadminlonger' }
expect(response).to redirect_to(wizard_path) expect(response).to redirect_to(wizard_path)
end end
it "sets the users timezone if the param is present" do it "sets the users timezone if the param is present" do
user = Fabricate(:admin) get "/u/password-reset/#{email_token.token}"
UserAuthToken.generate!(user_id: user.id)
token = user.email_tokens.create(email: user.email).token
get "/u/password-reset/#{token}"
expect(user.user_option.timezone).to eq(nil) expect(user.user_option.timezone).to eq(nil)
put "/u/password-reset/#{token}", params: { password: 'hg9ow8yhg98oadminlonger', timezone: "America/Chicago" }
put "/u/password-reset/#{email_token.token}", params: { password: 'hg9ow8yhg98oadminlonger', timezone: "America/Chicago" }
expect(user.user_option.reload.timezone).to eq("America/Chicago") expect(user.user_option.reload.timezone).to eq("America/Chicago")
end end
it "logs the password change" do it "logs the password change" do
user = Fabricate(:admin) get "/u/password-reset/#{email_token.token}"
UserAuthToken.generate!(user_id: user.id)
token = user.email_tokens.create(email: user.email).token
get "/u/password-reset/#{token}"
expect do expect do
put "/u/password-reset/#{token}", params: { password: 'hg9ow8yhg98oadminlonger' } put "/u/password-reset/#{email_token.token}", params: { password: 'hg9ow8yhg98oadminlonger' }
end.to change { UserHistory.count }.by (1) end.to change { UserHistory.count }.by (1)
entry = UserHistory.last user_history = UserHistory.last
expect(user_history.target_user_id).to eq(user.id)
expect(entry.target_user_id).to eq(user.id) expect(user_history.action).to eq(UserHistory.actions[:change_password])
expect(entry.action).to eq(UserHistory.actions[:change_password])
end end
it "doesn't invalidate the token when loading the page" do it "doesn't invalidate the token when loading the page" do
user = Fabricate(:user)
user_token = UserAuthToken.generate!(user_id: user.id)
email_token = user.email_tokens.create(email: user.email)
get "/u/password-reset/#{email_token.token}.json" get "/u/password-reset/#{email_token.token}.json"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(email_token.reload.confirmed).to eq(false)
email_token.reload expect(UserAuthToken.where(id: user_auth_token.id).count).to eq(1)
expect(email_token.confirmed).to eq(false)
expect(UserAuthToken.where(id: user_token.id).count).to eq(1)
end end
context "rate limiting" do context "rate limiting" do
@ -329,10 +288,8 @@ describe UsersController do
it "rate limits reset passwords" do it "rate limits reset passwords" do
freeze_time freeze_time
token = user.email_tokens.create!(email: user.email).token
6.times do 6.times do
put "/u/password-reset/#{token}", params: { put "/u/password-reset/#{email_token.token}", params: {
second_factor_token: 123456, second_factor_token: 123456,
second_factor_method: 1 second_factor_method: 1
} }
@ -340,7 +297,7 @@ describe UsersController do
expect(response.status).to eq(200) expect(response.status).to eq(200)
end end
put "/u/password-reset/#{token}", params: { put "/u/password-reset/#{email_token.token}", params: {
second_factor_token: 123456, second_factor_token: 123456,
second_factor_method: 1 second_factor_method: 1
} }
@ -351,10 +308,8 @@ describe UsersController do
it "rate limits reset passwords by username" do it "rate limits reset passwords by username" do
freeze_time freeze_time
token = user.email_tokens.create!(email: user.email).token
6.times do |x| 6.times do |x|
put "/u/password-reset/#{token}", params: { put "/u/password-reset/#{email_token.token}", params: {
second_factor_token: 123456, second_factor_token: 123456,
second_factor_method: 1 second_factor_method: 1
}, env: { "REMOTE_ADDR": "1.2.3.#{x}" } }, env: { "REMOTE_ADDR": "1.2.3.#{x}" }
@ -362,7 +317,7 @@ describe UsersController do
expect(response.status).to eq(200) expect(response.status).to eq(200)
end end
put "/u/password-reset/#{token}", params: { put "/u/password-reset/#{email_token.token}", params: {
second_factor_token: 123456, second_factor_token: 123456,
second_factor_method: 1 second_factor_method: 1
}, env: { "REMOTE_ADDR": "1.2.3.4" } }, env: { "REMOTE_ADDR": "1.2.3.4" }
@ -375,16 +330,16 @@ describe UsersController do
let!(:second_factor) { Fabricate(:user_second_factor_totp, user: user) } let!(:second_factor) { Fabricate(:user_second_factor_totp, user: user) }
it 'does not change with an invalid token' do it 'does not change with an invalid token' do
token = user.email_tokens.create!(email: user.email).token user.user_auth_tokens.destroy_all
get "/u/password-reset/#{token}" get "/u/password-reset/#{email_token.token}"
expect(response.body).to have_tag("div#data-preloaded") do |element| expect(response.body).to have_tag("div#data-preloaded") do |element|
json = JSON.parse(element.current_scope.attribute('data-preloaded').value) json = JSON.parse(element.current_scope.attribute('data-preloaded').value)
expect(json['password_reset']).to include('{"is_developer":false,"admin":false,"second_factor_required":true,"security_key_required":false,"backup_enabled":false,"multiple_second_factor_methods":false}') expect(json['password_reset']).to include('{"is_developer":false,"admin":false,"second_factor_required":true,"security_key_required":false,"backup_enabled":false,"multiple_second_factor_methods":false}')
end end
put "/u/password-reset/#{token}", params: { put "/u/password-reset/#{email_token.token}", params: {
password: 'hg9ow8yHG32O', password: 'hg9ow8yHG32O',
second_factor_token: '000000', second_factor_token: '000000',
second_factor_method: UserSecondFactor.methods[:totp] second_factor_method: UserSecondFactor.methods[:totp]
@ -398,11 +353,9 @@ describe UsersController do
end end
it 'changes password with valid 2-factor tokens' do it 'changes password with valid 2-factor tokens' do
token = user.email_tokens.create(email: user.email).token get "/u/password-reset/#{email_token.token}"
get "/u/password-reset/#{token}" put "/u/password-reset/#{email_token.token}", params: {
put "/u/password-reset/#{token}", params: {
password: 'hg9ow8yHG32O', password: 'hg9ow8yHG32O',
second_factor_token: ROTP::TOTP.new(second_factor.data).now, second_factor_token: ROTP::TOTP.new(second_factor.data).now,
second_factor_method: UserSecondFactor.methods[:totp] second_factor_method: UserSecondFactor.methods[:totp]
@ -424,13 +377,12 @@ describe UsersController do
public_key: valid_security_key_data[:public_key] public_key: valid_security_key_data[:public_key]
) )
end end
let(:token) { user.email_tokens.create!(email: user.email).token }
before do before do
simulate_localhost_webauthn_challenge simulate_localhost_webauthn_challenge
# store challenge in secure session by visiting the email login page # store challenge in secure session by visiting the email login page
get "/u/password-reset/#{token}" get "/u/password-reset/#{email_token.token}"
end end
it 'preloads with a security key challenge and allowed credential ids' do it 'preloads with a security key challenge and allowed credential ids' do
@ -450,34 +402,32 @@ describe UsersController do
end end
it 'changes password with valid security key challenge and authentication' do it 'changes password with valid security key challenge and authentication' do
put "/u/password-reset/#{token}.json", params: { put "/u/password-reset/#{email_token.token}.json", params: {
password: 'hg9ow8yHG32O', password: 'hg9ow8yHG32O',
second_factor_token: valid_security_key_auth_post_data, second_factor_token: valid_security_key_auth_post_data,
second_factor_method: UserSecondFactor.methods[:security_key] second_factor_method: UserSecondFactor.methods[:security_key]
} }
user.reload
expect(response.status).to eq(200) expect(response.status).to eq(200)
user.reload
expect(user.confirm_password?('hg9ow8yHG32O')).to eq(true) expect(user.confirm_password?('hg9ow8yHG32O')).to eq(true)
expect(user.user_auth_tokens.count).to eq(1) expect(user.user_auth_tokens.count).to eq(1)
end end
it "does not change a password if a fake TOTP token is provided" do it "does not change a password if a fake TOTP token is provided" do
put "/u/password-reset/#{token}.json", params: { put "/u/password-reset/#{email_token.token}.json", params: {
password: 'hg9ow8yHG32O', password: 'hg9ow8yHG32O',
second_factor_token: 'blah', second_factor_token: 'blah',
second_factor_method: UserSecondFactor.methods[:security_key] second_factor_method: UserSecondFactor.methods[:security_key]
} }
user.reload
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(user.confirm_password?('hg9ow8yHG32O')).to eq(false) expect(user.reload.confirm_password?('hg9ow8yHG32O')).to eq(false)
end end
context "when security key authentication fails" do context "when security key authentication fails" do
it 'shows an error message and does not change password' do it 'shows an error message and does not change password' do
put "/u/password-reset/#{token}", params: { put "/u/password-reset/#{email_token.token}", params: {
password: 'hg9ow8yHG32O', password: 'hg9ow8yHG32O',
second_factor_token: { second_factor_token: {
signature: 'bad', signature: 'bad',
@ -488,24 +438,19 @@ describe UsersController do
second_factor_method: UserSecondFactor.methods[:security_key] second_factor_method: UserSecondFactor.methods[:security_key]
} }
user.reload
expect(user.confirm_password?('hg9ow8yHG32O')).to eq(false)
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.body).to include(I18n.t("webauthn.validation.not_found_error")) expect(response.body).to include(I18n.t("webauthn.validation.not_found_error"))
expect(user.reload.confirm_password?('hg9ow8yHG32O')).to eq(false)
end end
end end
end end
end end
context 'submit change' do context 'submit change' do
let(:token) { EmailToken.generate_token } let(:email_token) { Fabricate(:email_token, user: user, scope: EmailToken.scopes[:password_reset]) }
before do
EmailToken.expects(:confirm).with(token).returns(user)
end
it "fails when the password is blank" do it "fails when the password is blank" do
put "/u/password-reset/#{token}.json", params: { password: '' } put "/u/password-reset/#{email_token.token}.json", params: { password: '' }
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.parsed_body["errors"]).to be_present expect(response.parsed_body["errors"]).to be_present
@ -513,7 +458,7 @@ describe UsersController do
end end
it "fails when the password is too long" do it "fails when the password is too long" do
put "/u/password-reset/#{token}.json", params: { password: ('x' * (User.max_password_length + 1)) } put "/u/password-reset/#{email_token.token}.json", params: { password: ('x' * (User.max_password_length + 1)) }
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.parsed_body["errors"]).to be_present expect(response.parsed_body["errors"]).to be_present
@ -521,7 +466,7 @@ describe UsersController do
end end
it "logs in the user" do it "logs in the user" do
put "/u/password-reset/#{token}.json", params: { password: 'ksjafh928r' } put "/u/password-reset/#{email_token.token}.json", params: { password: 'ksjafh928r' }
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.parsed_body["errors"]).to be_blank expect(response.parsed_body["errors"]).to be_blank
@ -530,8 +475,9 @@ describe UsersController do
it "doesn't log in the user when not approved" do it "doesn't log in the user when not approved" do
SiteSetting.must_approve_users = true SiteSetting.must_approve_users = true
user.update!(approved: false)
put "/u/password-reset/#{token}.json", params: { password: 'ksjafh928r' } put "/u/password-reset/#{email_token.token}.json", params: { password: 'ksjafh928r' }
expect(response.parsed_body["errors"]).to be_blank expect(response.parsed_body["errors"]).to be_blank
expect(session[:current_user_id]).to be_blank expect(session[:current_user_id]).to be_blank
end end
@ -540,16 +486,15 @@ describe UsersController do
describe '#confirm_email_token' do describe '#confirm_email_token' do
fab!(:user) { Fabricate(:user) } fab!(:user) { Fabricate(:user) }
let!(:email_token) { Fabricate(:email_token, user: user) }
it "token doesn't match any records" do it "token doesn't match any records" do
email_token = user.email_tokens.create(email: user.email)
get "/u/confirm-email-token/#{SecureRandom.hex}.json" get "/u/confirm-email-token/#{SecureRandom.hex}.json"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(email_token.reload.confirmed).to eq(false) expect(email_token.reload.confirmed).to eq(false)
end end
it "token matches" do it "token matches" do
email_token = user.email_tokens.create(email: user.email)
get "/u/confirm-email-token/#{email_token.token}.json" get "/u/confirm-email-token/#{email_token.token}.json"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(email_token.reload.confirmed).to eq(true) expect(email_token.reload.confirmed).to eq(true)
@ -833,18 +778,13 @@ describe UsersController do
expect(Jobs::SendSystemMessage.jobs.size).to eq(0) expect(Jobs::SendSystemMessage.jobs.size).to eq(0)
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body expect(response.parsed_body['active']).to be_truthy
new_user = User.find(response.parsed_body["user_id"])
new_user = User.find(json["user_id"])
email_token = new_user.email_tokens.active.where(email: new_user.email).first
expect(json['active']).to be_truthy
expect(new_user.active).to eq(true) expect(new_user.active).to eq(true)
expect(new_user.approved).to eq(true) expect(new_user.approved).to eq(true)
expect(new_user.approved_by_id).to eq(admin.id) expect(new_user.approved_by_id).to eq(admin.id)
expect(new_user.approved_at).to_not eq(nil) expect(new_user.approved_at).to_not eq(nil)
expect(email_token.confirmed?).to eq(true) expect(new_user.email_tokens.where(confirmed: true, email: new_user.email)).to exist
end end
it "will create a reviewable when a user is created as active but not approved" do it "will create a reviewable when a user is created as active but not approved" do
@ -2358,7 +2298,7 @@ describe UsersController do
context 'for an activated account with email confirmed' do context 'for an activated account with email confirmed' do
it 'fails' do it 'fails' do
user = post_user user = post_user
email_token = user.email_tokens.create(email: user.email).token email_token = Fabricate(:email_token, user: user).token
EmailToken.confirm(email_token) EmailToken.confirm(email_token)
post "/u/action/send_activation_email.json", params: { username: user.username } post "/u/action/send_activation_email.json", params: { username: user.username }
@ -2375,7 +2315,7 @@ describe UsersController do
it 'should send an email' do it 'should send an email' do
user = post_user user = post_user
user.update!(active: true) user.update!(active: true)
user.email_tokens.create!(email: user.email) Fabricate(:email_token, user: user)
expect_enqueued_with(job: :critical_user_email, args: { type: :signup, to_address: user.email }) do expect_enqueued_with(job: :critical_user_email, args: { type: :signup, to_address: user.email }) do
post "/u/action/send_activation_email.json", params: { post "/u/action/send_activation_email.json", params: {
@ -2398,7 +2338,7 @@ describe UsersController do
user = post_user user = post_user
user.update(active: true) user.update(active: true)
user.save! user.save!
user.email_tokens.create(email: user.email) Fabricate(:email_token, user: user)
post "/u/action/send_activation_email.json", params: { post "/u/action/send_activation_email.json", params: {
username: user.username username: user.username
} }
@ -4289,10 +4229,9 @@ describe UsersController do
expect(response.parsed_body['user_found']).to eq(true) expect(response.parsed_body['user_found']).to eq(true)
job_args = Jobs::CriticalUserEmail.jobs.last["args"].first job_args = Jobs::CriticalUserEmail.jobs.last["args"].first
expect(job_args["user_id"]).to eq(user.id) expect(job_args["user_id"]).to eq(user.id)
expect(job_args["type"]).to eq("email_login") expect(job_args["type"]).to eq("email_login")
expect(job_args["email_token"]).to eq(user.email_tokens.last.token) expect(EmailToken.hash_token(job_args["email_token"])).to eq(user.email_tokens.last.token_hash)
end end
describe 'when enable_local_logins_via_email is disabled' do describe 'when enable_local_logins_via_email is disabled' do

View File

@ -6,18 +6,19 @@ require 'rotp'
describe UsersEmailController do describe UsersEmailController do
fab!(:user) { Fabricate(:user) } fab!(:user) { Fabricate(:user) }
let!(:email_token) { Fabricate(:email_token, user: user) }
fab!(:moderator) { Fabricate(:moderator) } fab!(:moderator) { Fabricate(:moderator) }
describe "#confirm-new-email" do describe "#confirm-new-email" do
it 'does not redirect to login for signed out accounts, this route works fine as anon user' do it 'does not redirect to login for signed out accounts, this route works fine as anon user' do
get "/u/confirm-new-email/asdfasdf" get "/u/confirm-new-email/invalidtoken"
expect(response.status).to eq(200) expect(response.status).to eq(200)
end end
it 'does not redirect to login for signed out accounts on login_required sites, this route works fine as anon user' do it 'does not redirect to login for signed out accounts on login_required sites, this route works fine as anon user' do
SiteSetting.login_required = true SiteSetting.login_required = true
get "/u/confirm-new-email/asdfasdf" get "/u/confirm-new-email/invalidtoken"
expect(response.status).to eq(200) expect(response.status).to eq(200)
end end
@ -25,7 +26,7 @@ describe UsersEmailController do
it 'errors out for invalid tokens' do it 'errors out for invalid tokens' do
sign_in(user) sign_in(user)
get "/u/confirm-new-email/asdfasdf" get "/u/confirm-new-email/invalidtoken"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.body).to include(I18n.t('change_email.already_done')) expect(response.body).to include(I18n.t('change_email.already_done'))
@ -33,18 +34,14 @@ describe UsersEmailController do
it 'does not change email if accounts mismatch for a signed in user' do it 'does not change email if accounts mismatch for a signed in user' do
updater = EmailUpdater.new(guardian: user.guardian, user: user) updater = EmailUpdater.new(guardian: user.guardian, user: user)
updater.change_to('new.n.cool@example.com') updater.change_to('bubblegum@adventuretime.ooo')
old_email = user.email old_email = user.email
sign_in(moderator) sign_in(moderator)
put "/u/confirm-new-email", params: { put "/u/confirm-new-email", params: { token: "#{email_token.token}" }
token: "#{user.email_tokens.last.token}" expect(user.reload.email).to eq(old_email)
}
user.reload
expect(user.email).to eq(old_email)
end end
context "with a valid user" do context "with a valid user" do
@ -52,14 +49,14 @@ describe UsersEmailController do
before do before do
sign_in(user) sign_in(user)
updater.change_to('new.n.cool@example.com') updater.change_to('bubblegum@adventuretime.ooo')
end end
it 'includes security_key_allowed_credential_ids in a hidden field' do it 'includes security_key_allowed_credential_ids in a hidden field' do
key1 = Fabricate(:user_security_key_with_random_credential, user: user) key1 = Fabricate(:user_security_key_with_random_credential, user: user)
key2 = Fabricate(:user_security_key_with_random_credential, user: user) key2 = Fabricate(:user_security_key_with_random_credential, user: user)
get "/u/confirm-new-email/#{user.email_tokens.last.token}" get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}"
doc = Nokogiri::HTML5(response.body) doc = Nokogiri::HTML5(response.body)
credential_ids = doc.css("#security-key-allowed-credential-ids").first["value"].split(",") credential_ids = doc.css("#security-key-allowed-credential-ids").first["value"].split(",")
@ -70,17 +67,15 @@ describe UsersEmailController do
user.user_stat.update_columns(bounce_score: 42, reset_bounce_score_after: 1.week.from_now) user.user_stat.update_columns(bounce_score: 42, reset_bounce_score_after: 1.week.from_now)
put "/u/confirm-new-email", params: { put "/u/confirm-new-email", params: {
token: "#{user.email_tokens.last.token}" token: "#{updater.change_req.new_email_token.token}"
} }
expect(response.status).to eq(302) expect(response.status).to eq(302)
expect(response.redirect_url).to include("done") expect(response.redirect_url).to include("done")
user.reload user.reload
expect(user.user_stat.bounce_score).to eq(0) expect(user.user_stat.bounce_score).to eq(0)
expect(user.user_stat.reset_bounce_score_after).to eq(nil) expect(user.user_stat.reset_bounce_score_after).to eq(nil)
expect(user.email).to eq("new.n.cool@example.com") expect(user.email).to eq('bubblegum@adventuretime.ooo')
end end
context 'second factor required' do context 'second factor required' do
@ -88,29 +83,23 @@ describe UsersEmailController do
fab!(:backup_code) { Fabricate(:user_second_factor_backup, user: user) } fab!(:backup_code) { Fabricate(:user_second_factor_backup, user: user) }
it 'requires a second factor token' do it 'requires a second factor token' do
get "/u/confirm-new-email/#{user.email_tokens.last.token}" get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.body).to include(I18n.t("login.second_factor_title"))
response_body = response.body expect(response.body).not_to include(I18n.t("login.invalid_second_factor_code"))
expect(response_body).to include(I18n.t("login.second_factor_title"))
expect(response_body).not_to include(I18n.t("login.invalid_second_factor_code"))
end end
it 'requires a backup token' do it 'requires a backup token' do
get "/u/confirm-new-email/#{user.email_tokens.last.token}?show_backup=true" get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}?show_backup=true"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.body).to include(I18n.t("login.second_factor_backup_title"))
response_body = response.body
expect(response_body).to include(I18n.t("login.second_factor_backup_title"))
end end
it 'adds an error on a second factor attempt' do it 'adds an error on a second factor attempt' do
put "/u/confirm-new-email", params: { put "/u/confirm-new-email", params: {
token: user.email_tokens.last.token, token: updater.change_req.new_email_token.token,
second_factor_token: "000000", second_factor_token: "000000",
second_factor_method: UserSecondFactor.methods[:totp] second_factor_method: UserSecondFactor.methods[:totp]
} }
@ -123,13 +112,11 @@ describe UsersEmailController do
put "/u/confirm-new-email", params: { put "/u/confirm-new-email", params: {
second_factor_token: ROTP::TOTP.new(second_factor.data).now, second_factor_token: ROTP::TOTP.new(second_factor.data).now,
second_factor_method: UserSecondFactor.methods[:totp], second_factor_method: UserSecondFactor.methods[:totp],
token: user.email_tokens.last.token token: updater.change_req.new_email_token.token
} }
expect(response.status).to eq(302) expect(response.status).to eq(302)
expect(user.reload.email).to eq('bubblegum@adventuretime.ooo')
user.reload
expect(user.email).to eq("new.n.cool@example.com")
end end
context "rate limiting" do context "rate limiting" do
@ -163,7 +150,7 @@ describe UsersEmailController do
6.times do |x| 6.times do |x|
user.email_change_requests.last.update(change_state: EmailChangeRequest.states[:complete]) user.email_change_requests.last.update(change_state: EmailChangeRequest.states[:complete])
put "/u/confirm-new-email", params: { put "/u/confirm-new-email", params: {
token: user.email_tokens.last.token, token: updater.change_req.new_email_token.token,
second_factor_token: "000000", second_factor_token: "000000",
second_factor_method: UserSecondFactor.methods[:totp] second_factor_method: UserSecondFactor.methods[:totp]
}, env: { "REMOTE_ADDR": "1.2.3.#{x}" } }, env: { "REMOTE_ADDR": "1.2.3.#{x}" }
@ -173,7 +160,7 @@ describe UsersEmailController do
user.email_change_requests.last.update(change_state: EmailChangeRequest.states[:authorizing_new]) user.email_change_requests.last.update(change_state: EmailChangeRequest.states[:authorizing_new])
put "/u/confirm-new-email", params: { put "/u/confirm-new-email", params: {
token: user.email_tokens.last.token, token: updater.change_req.new_email_token.token,
second_factor_token: "000000", second_factor_token: "000000",
second_factor_method: UserSecondFactor.methods[:totp] second_factor_method: UserSecondFactor.methods[:totp]
}, env: { "REMOTE_ADDR": "1.2.3.4" } }, env: { "REMOTE_ADDR": "1.2.3.4" }
@ -198,14 +185,11 @@ describe UsersEmailController do
end end
it 'requires a security key' do it 'requires a security key' do
get "/u/confirm-new-email/#{user.email_tokens.last.token}" get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.body).to include(I18n.t("login.security_key_authenticate"))
response_body = response.body expect(response.body).to include(I18n.t("login.security_key_description"))
expect(response_body).to include(I18n.t("login.security_key_authenticate"))
expect(response_body).to include(I18n.t("login.security_key_description"))
end end
context "if the user has a TOTP enabled and wants to use that instead" do context "if the user has a TOTP enabled and wants to use that instead" do
@ -214,21 +198,18 @@ describe UsersEmailController do
end end
it 'allows entering the totp code instead' do it 'allows entering the totp code instead' do
get "/u/confirm-new-email/#{user.email_tokens.last.token}?show_totp=true" get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}?show_totp=true"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.body).to include(I18n.t("login.second_factor_title"))
response_body = response.body expect(response.body).not_to include(I18n.t("login.security_key_authenticate"))
expect(response_body).to include(I18n.t("login.second_factor_title"))
expect(response_body).not_to include(I18n.t("login.security_key_authenticate"))
end end
end end
it 'adds an error on a security key attempt' do it 'adds an error on a security key attempt' do
get "/u/confirm-new-email/#{user.email_tokens.last.token}" get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}"
put "/u/confirm-new-email", params: { put "/u/confirm-new-email", params: {
token: user.email_tokens.last.token, token: updater.change_req.new_email_token.token,
second_factor_token: "{}", second_factor_token: "{}",
second_factor_method: UserSecondFactor.methods[:security_key] second_factor_method: UserSecondFactor.methods[:security_key]
} }
@ -238,26 +219,24 @@ describe UsersEmailController do
end end
it 'confirms with a correct security key token' do it 'confirms with a correct security key token' do
get "/u/confirm-new-email/#{user.email_tokens.last.token}" get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}"
put "/u/confirm-new-email", params: { put "/u/confirm-new-email", params: {
token: updater.change_req.new_email_token.token,
second_factor_token: valid_security_key_auth_post_data.to_json, second_factor_token: valid_security_key_auth_post_data.to_json,
second_factor_method: UserSecondFactor.methods[:security_key], second_factor_method: UserSecondFactor.methods[:security_key]
token: user.email_tokens.last.token
} }
expect(response.status).to eq(302) expect(response.status).to eq(302)
expect(user.reload.email).to eq('bubblegum@adventuretime.ooo')
user.reload
expect(user.email).to eq("new.n.cool@example.com")
end end
context "if the security key data JSON is garbled" do context "if the security key data JSON is garbled" do
it "raises an invalid parameters error" do it "raises an invalid parameters error" do
get "/u/confirm-new-email/#{user.email_tokens.last.token}" get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}"
put "/u/confirm-new-email", params: { put "/u/confirm-new-email", params: {
token: updater.change_req.new_email_token.token,
second_factor_token: "{someweird: 8notjson}", second_factor_token: "{someweird: 8notjson}",
second_factor_method: UserSecondFactor.methods[:security_key], second_factor_method: UserSecondFactor.methods[:security_key]
token: user.email_tokens.last.token
} }
expect(response.status).to eq(400) expect(response.status).to eq(400)
@ -268,9 +247,8 @@ describe UsersEmailController do
end end
describe '#confirm-old-email' do describe '#confirm-old-email' do
it 'redirects to login for signed out accounts' do it 'redirects to login for signed out accounts' do
get "/u/confirm-old-email/asdfasdf" get "/u/confirm-old-email/invalidtoken"
expect(response.status).to eq(302) expect(response.status).to eq(302)
expect(response.redirect_url).to eq("http://test.localhost/login") expect(response.redirect_url).to eq("http://test.localhost/login")
@ -279,75 +257,62 @@ describe UsersEmailController do
it 'errors out for invalid tokens' do it 'errors out for invalid tokens' do
sign_in(user) sign_in(user)
get "/u/confirm-old-email/asdfasdf" get "/u/confirm-old-email/invalidtoken"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.body).to include(I18n.t('change_email.already_done')) expect(response.body).to include(I18n.t('change_email.already_done'))
end end
it 'bans change when accounts do not match' do it 'bans change when accounts do not match' do
sign_in(user) sign_in(user)
updater = EmailUpdater.new(guardian: moderator.guardian, user: moderator) updater = EmailUpdater.new(guardian: moderator.guardian, user: moderator)
updater.change_to('new.n.cool@example.com') email_change_request = updater.change_to('bubblegum@adventuretime.ooo')
get "/u/confirm-old-email/#{moderator.email_tokens.last.token}" get "/u/confirm-old-email/#{email_change_request.old_email_token.token}"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(body).to include("alert-error") expect(body).to include("alert-error")
end end
context 'valid old address token' do context 'valid old token' do
it 'confirms with a correct token' do it 'confirms with a correct token' do
# NOTE: only moderators need to confirm both old and new
sign_in(moderator) sign_in(moderator)
updater = EmailUpdater.new(guardian: moderator.guardian, user: moderator) updater = EmailUpdater.new(guardian: moderator.guardian, user: moderator)
updater.change_to('new.n.cool@example.com') email_change_request = updater.change_to('bubblegum@adventuretime.ooo')
get "/u/confirm-old-email/#{moderator.email_tokens.last.token}" get "/u/confirm-old-email/#{email_change_request.old_email_token.token}"
expect(response.status).to eq(200) expect(response.status).to eq(200)
body = CGI.unescapeHTML(response.body) body = CGI.unescapeHTML(response.body)
expect(body).to include(I18n.t('change_email.authorizing_old.title'))
expect(body) expect(body).to include(I18n.t('change_email.authorizing_old.description'))
.to include(I18n.t('change_email.authorizing_old.title'))
expect(body)
.to include(I18n.t('change_email.authorizing_old.description'))
put "/u/confirm-old-email", params: { put "/u/confirm-old-email", params: {
token: moderator.email_tokens.last.token token: email_change_request.old_email_token.token
} }
expect(response.status).to eq(302) expect(response.status).to eq(302)
expect(response.redirect_url).to include("done=true") expect(response.redirect_url).to include("done=true")
end end
end end
end end
describe '#create' do describe '#create' do
let(:new_email) { 'bubblegum@adventuretime.ooo' }
it 'has an email token' do it 'has an email token' do
sign_in(user) sign_in(user)
expect { post "/u/#{user.username}/preferences/email.json", params: { email: new_email } } expect { post "/u/#{user.username}/preferences/email.json", params: { email: 'bubblegum@adventuretime.ooo' } }
.to change(EmailChangeRequest, :count) .to change(EmailChangeRequest, :count)
emailChangeRequest = EmailChangeRequest.last emailChangeRequest = EmailChangeRequest.last
expect(emailChangeRequest.old_email).to eq(nil) expect(emailChangeRequest.old_email).to eq(nil)
expect(emailChangeRequest.new_email).to eq(new_email) expect(emailChangeRequest.new_email).to eq('bubblegum@adventuretime.ooo')
end end
end end
describe '#update' do describe '#update' do
let(:new_email) { 'bubblegum@adventuretime.ooo' }
it "requires you to be logged in" do it "requires you to be logged in" do
put "/u/#{user.username}/preferences/email.json", params: { email: new_email } put "/u/#{user.username}/preferences/email.json", params: { email: 'bubblegum@adventuretime.ooo' }
expect(response.status).to eq(403) expect(response.status).to eq(403)
end end
@ -368,10 +333,9 @@ describe UsersEmailController do
end end
it "raises an error if you can't edit the user's email" do it "raises an error if you can't edit the user's email" do
Guardian.any_instance.expects(:can_edit_email?).with(user).returns(false) SiteSetting.email_editable = false
put "/u/#{user.username}/preferences/email.json", params: { email: new_email }
put "/u/#{user.username}/preferences/email.json", params: { email: 'bubblegum@adventuretime.ooo' }
expect(response).to be_forbidden expect(response).to be_forbidden
end end
@ -384,18 +348,12 @@ describe UsersEmailController do
end end
it 'raises an error' do it 'raises an error' do
put "/u/#{user.username}/preferences/email.json", params: { put "/u/#{user.username}/preferences/email.json", params: { email: other_user.email }
email: other_user.email
}
expect(response).to_not be_successful expect(response).to_not be_successful
end end
it 'raises an error if there is whitespace too' do it 'raises an error if there is whitespace too' do
put "/u/#{user.username}/preferences/email.json", params: { put "/u/#{user.username}/preferences/email.json", params: { email: "#{other_user.email} " }
email: "#{other_user.email} "
}
expect(response).to_not be_successful expect(response).to_not be_successful
end end
end end
@ -406,10 +364,7 @@ describe UsersEmailController do
end end
it 'responds with success' do it 'responds with success' do
put "/u/#{user.username}/preferences/email.json", params: { put "/u/#{user.username}/preferences/email.json", params: { email: other_user.email }
email: other_user.email
}
expect(response.status).to eq(200) expect(response.status).to eq(200)
end end
end end
@ -419,10 +374,7 @@ describe UsersEmailController do
fab!(:other_user) { Fabricate(:user, email: 'case.insensitive@gmail.com') } fab!(:other_user) { Fabricate(:user, email: 'case.insensitive@gmail.com') }
it 'raises an error' do it 'raises an error' do
put "/u/#{user.username}/preferences/email.json", params: { put "/u/#{user.username}/preferences/email.json", params: { email: other_user.email.upcase }
email: other_user.email.upcase
}
expect(response).to_not be_successful expect(response).to_not be_successful
end end
end end
@ -430,34 +382,24 @@ describe UsersEmailController do
it 'raises an error when new email domain is present in blocked_email_domains site setting' do it 'raises an error when new email domain is present in blocked_email_domains site setting' do
SiteSetting.blocked_email_domains = "mailinator.com" SiteSetting.blocked_email_domains = "mailinator.com"
put "/u/#{user.username}/preferences/email.json", params: { put "/u/#{user.username}/preferences/email.json", params: { email: "not_good@mailinator.com" }
email: "not_good@mailinator.com"
}
expect(response).to_not be_successful expect(response).to_not be_successful
end end
it 'raises an error when new email domain is not present in allowed_email_domains site setting' do it 'raises an error when new email domain is not present in allowed_email_domains site setting' do
SiteSetting.allowed_email_domains = "discourse.org" SiteSetting.allowed_email_domains = "discourse.org"
put "/u/#{user.username}/preferences/email.json", params: { put "/u/#{user.username}/preferences/email.json", params: { email: 'bubblegum@adventuretime.ooo' }
email: new_email
}
expect(response).to_not be_successful expect(response).to_not be_successful
end end
context 'success' do context 'success' do
it 'has an email token' do it 'has an email token' do
expect do expect do
put "/u/#{user.username}/preferences/email.json", params: { put "/u/#{user.username}/preferences/email.json", params: { email: 'bubblegum@adventuretime.ooo' }
email: new_email
}
end.to change(EmailChangeRequest, :count) end.to change(EmailChangeRequest, :count)
end end
end end
end end
end end
end end

View File

@ -3,42 +3,21 @@
require 'rails_helper' require 'rails_helper'
describe UserActivator do describe UserActivator do
fab!(:user) { Fabricate(:user) }
let!(:email_token) { Fabricate(:email_token, user: user) }
describe 'email_activator' do describe 'email_activator' do
let(:activator) { EmailActivator.new(user, nil, nil, nil) }
it 'does not create new email token unless required' do it 'create email token and enqueues user email' do
SiteSetting.email_token_valid_hours = 24
user = Fabricate(:user)
activator = EmailActivator.new(user, nil, nil, nil)
expect_enqueued_with(job: :critical_user_email, args: { type: :signup, email_token: user.email_tokens.first.token }) do
activator.activate
end
end
it 'creates and send new email token if the existing token expired' do
now = freeze_time now = freeze_time
activator.activate
SiteSetting.email_token_valid_hours = 24
user = Fabricate(:user)
email_token = user.email_tokens.first
email_token.update_column(:created_at, 48.hours.ago)
activator = EmailActivator.new(user, nil, nil, nil)
expect_not_enqueued_with(job: :critical_user_email, args: { type: :signup, user_id: user.id, email_token: email_token.token }) do
activator.activate
end
email_token = user.reload.email_tokens.last email_token = user.reload.email_tokens.last
expect(job_enqueued?(job: :critical_user_email, args: {
type: :signup,
user_id: user.id,
email_token: email_token.token
})).to eq(true)
expect(email_token.created_at).to eq_time(now) expect(email_token.created_at).to eq_time(now)
job_args = Jobs::CriticalUserEmail.jobs.last["args"].first
expect(job_args["user_id"]).to eq(user.id)
expect(job_args["type"]).to eq("signup")
expect(EmailToken.hash_token(job_args["email_token"])).to eq(email_token.token_hash)
end end
end end
end end