# frozen_string_literal: true require "rotp" RSpec.describe UsersEmailController do fab!(:user) { Fabricate(:user) } let!(:email_token) { Fabricate(:email_token, user: user) } fab!(:moderator) { Fabricate(:moderator) } describe "#confirm-new-email" do it "does not redirect to login for signed out accounts, this route works fine as anon user" do get "/u/confirm-new-email/invalidtoken" expect(response.status).to eq(200) end 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 get "/u/confirm-new-email/invalidtoken" expect(response.status).to eq(200) end it "errors out for invalid tokens" do sign_in(user) get "/u/confirm-new-email/invalidtoken" expect(response.status).to eq(200) expect(response.body).to include(I18n.t("change_email.already_done")) end it "does not change email if accounts mismatch for a signed in user" do updater = EmailUpdater.new(guardian: user.guardian, user: user) updater.change_to("bubblegum@adventuretime.ooo") old_email = user.email sign_in(moderator) put "/u/confirm-new-email", params: { token: "#{email_token.token}" } expect(user.reload.email).to eq(old_email) end context "with a valid user" do let(:updater) { EmailUpdater.new(guardian: user.guardian, user: user) } before do sign_in(user) updater.change_to("bubblegum@adventuretime.ooo") end it "includes security_key_allowed_credential_ids in a hidden field" do key1 = Fabricate(:user_security_key_with_random_credential, user: user) key2 = Fabricate(:user_security_key_with_random_credential, user: user) get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}" doc = Nokogiri.HTML5(response.body) credential_ids = doc.css("#security-key-allowed-credential-ids").first["value"].split(",") expect(credential_ids).to contain_exactly(key1.credential_id, key2.credential_id) end it "confirms with a correct token" do user.user_stat.update_columns(bounce_score: 42, reset_bounce_score_after: 1.week.from_now) put "/u/confirm-new-email", params: { token: "#{updater.change_req.new_email_token.token}" } expect(response.status).to eq(302) expect(response.redirect_url).to include("done") user.reload expect(user.user_stat.bounce_score).to eq(0) expect(user.user_stat.reset_bounce_score_after).to eq(nil) expect(user.email).to eq("bubblegum@adventuretime.ooo") end context "when second factor is required" do fab!(:second_factor) { Fabricate(:user_second_factor_totp, user: user) } fab!(:backup_code) { Fabricate(:user_second_factor_backup, user: user) } it "requires a second factor token" do get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}" expect(response.status).to eq(200) 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 it "requires a backup token" do get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}?show_backup=true" expect(response.status).to eq(200) expect(response.body).to include(I18n.t("login.second_factor_backup_title")) end it "adds an error on a second factor attempt" do put "/u/confirm-new-email", params: { token: updater.change_req.new_email_token.token, second_factor_token: "000000", second_factor_method: UserSecondFactor.methods[:totp], } expect(response.status).to eq(302) expect(flash[:invalid_second_factor]).to eq(true) end it "confirms with a correct second token" do put "/u/confirm-new-email", params: { second_factor_token: ROTP::TOTP.new(second_factor.data).now, second_factor_method: UserSecondFactor.methods[:totp], token: updater.change_req.new_email_token.token, } expect(response.status).to eq(302) expect(user.reload.email).to eq("bubblegum@adventuretime.ooo") end context "with rate limiting" do before { RateLimiter.enable } use_redis_snapshotting it "rate limits by IP" do freeze_time 6.times do put "/u/confirm-new-email", params: { token: "blah", second_factor_token: "000000", second_factor_method: UserSecondFactor.methods[:totp], } expect(response.status).to eq(302) end put "/u/confirm-new-email", params: { token: "blah", second_factor_token: "000000", second_factor_method: UserSecondFactor.methods[:totp], } expect(response.status).to eq(429) end it "rate limits by username" do freeze_time 6.times do |x| user.email_change_requests.last.update( change_state: EmailChangeRequest.states[:complete], ) put "/u/confirm-new-email", params: { token: updater.change_req.new_email_token.token, second_factor_token: "000000", second_factor_method: UserSecondFactor.methods[:totp], }, env: { REMOTE_ADDR: "1.2.3.#{x}", } expect(response.status).to eq(302) end user.email_change_requests.last.update( change_state: EmailChangeRequest.states[:authorizing_new], ) put "/u/confirm-new-email", params: { token: updater.change_req.new_email_token.token, second_factor_token: "000000", second_factor_method: UserSecondFactor.methods[:totp], }, env: { REMOTE_ADDR: "1.2.3.4", } expect(response.status).to eq(429) end end end context "when security key is required" do fab!(:user_security_key) do Fabricate( :user_security_key, user: user, credential_id: valid_security_key_data[:credential_id], public_key: valid_security_key_data[:public_key], ) end before { simulate_localhost_webauthn_challenge } it "requires a security key" do get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}" expect(response.status).to eq(200) expect(response.body).to include(I18n.t("login.security_key_authenticate")) expect(response.body).to include(I18n.t("login.security_key_description")) end context "if the user has a TOTP enabled and wants to use that instead" do before { Fabricate(:user_second_factor_totp, user: user) } it "allows entering the totp code instead" do get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}?show_totp=true" expect(response.status).to eq(200) 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 it "adds an error on a security key attempt" do get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}" put "/u/confirm-new-email", params: { token: updater.change_req.new_email_token.token, second_factor_token: "{}", second_factor_method: UserSecondFactor.methods[:security_key], } expect(response.status).to eq(302) expect(flash[:invalid_second_factor]).to eq(true) end it "confirms with a correct security key token" do get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}" 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_method: UserSecondFactor.methods[:security_key], } expect(response.status).to eq(302) expect(user.reload.email).to eq("bubblegum@adventuretime.ooo") end context "if the security key data JSON is garbled" do it "raises an invalid parameters error" do get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}" put "/u/confirm-new-email", params: { token: updater.change_req.new_email_token.token, second_factor_token: "{someweird: 8notjson}", second_factor_method: UserSecondFactor.methods[:security_key], } expect(response.status).to eq(400) end end end end it "destroys email tokens associated with the old email after the new email is confirmed" do SiteSetting.enable_secondary_emails = true email_token = user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:password_reset]) updater = EmailUpdater.new(guardian: user.guardian, user: user) updater.change_to("bubblegum@adventuretime.ooo") sign_in(user) put "/u/confirm-new-email", params: { token: "#{updater.change_req.new_email_token.token}" } new_password = SecureRandom.hex put "/u/password-reset/#{email_token.token}.json", params: { password: new_password } expect(response.parsed_body["success"]).to eq(false) expect(response.parsed_body["message"]).to eq( I18n.t("password_reset.no_token", base_url: Discourse.base_url), ) expect(user.reload.confirm_password?(new_password)).to eq(false) end end describe "#confirm-old-email" do it "redirects to login for signed out accounts" do get "/u/confirm-old-email/invalidtoken" expect(response.status).to eq(302) expect(response.redirect_url).to eq("http://test.localhost/login") end it "errors out for invalid tokens" do sign_in(user) get "/u/confirm-old-email/invalidtoken" expect(response.status).to eq(200) expect(response.body).to include(I18n.t("change_email.already_done")) end it "bans change when accounts do not match" do sign_in(user) updater = EmailUpdater.new(guardian: moderator.guardian, user: moderator) email_change_request = updater.change_to("bubblegum@adventuretime.ooo") get "/u/confirm-old-email/#{email_change_request.old_email_token.token}" expect(response.status).to eq(200) expect(body).to include("alert-error") end context "with valid old token" do it "confirms with a correct token" do sign_in(moderator) updater = EmailUpdater.new(guardian: moderator.guardian, user: moderator) email_change_request = updater.change_to("bubblegum@adventuretime.ooo") get "/u/confirm-old-email/#{email_change_request.old_email_token.token}" expect(response.status).to eq(200) body = CGI.unescapeHTML(response.body) expect(body).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: { token: email_change_request.old_email_token.token } expect(response.status).to eq(302) expect(response.redirect_url).to include("done=true") end end end describe "#create" do it "has an email token" do sign_in(user) expect { post "/u/#{user.username}/preferences/email.json", params: { email: "bubblegum@adventuretime.ooo", } }.to change(EmailChangeRequest, :count) emailChangeRequest = EmailChangeRequest.last expect(emailChangeRequest.old_email).to eq(nil) expect(emailChangeRequest.new_email).to eq("bubblegum@adventuretime.ooo") end end describe "#update" do it "requires you to be logged in" do put "/u/#{user.username}/preferences/email.json", params: { email: "bubblegum@adventuretime.ooo", } expect(response.status).to eq(403) end context "when logged in" do before { sign_in(user) } it "raises an error without an email parameter" do put "/u/#{user.username}/preferences/email.json" expect(response.status).to eq(400) end it "raises an error without an invalid email" do put "/u/#{user.username}/preferences/email.json", params: { email: "sam@not-email.com'" } expect(response.status).to eq(422) expect(response.body).to include("Email is invalid") end it "raises an error if you can't edit the user's email" do SiteSetting.email_editable = false put "/u/#{user.username}/preferences/email.json", params: { email: "bubblegum@adventuretime.ooo", } expect(response).to be_forbidden end context "when the new email address is taken" do fab!(:other_user) { Fabricate(:coding_horror) } context "when hide_email_address_taken is disabled" do before { SiteSetting.hide_email_address_taken = false } it "raises an error" do put "/u/#{user.username}/preferences/email.json", params: { email: other_user.email } expect(response).to_not be_successful end it "raises an error if there is whitespace too" do put "/u/#{user.username}/preferences/email.json", params: { email: "#{other_user.email} ", } expect(response).to_not be_successful end end context "when hide_email_address_taken is enabled" do before { SiteSetting.hide_email_address_taken = true } it "responds with success" do put "/u/#{user.username}/preferences/email.json", params: { email: other_user.email } expect(response.status).to eq(200) end end end context "when new email is different case of existing email" do fab!(:other_user) { Fabricate(:user, email: "case.insensitive@gmail.com") } it "raises an error" do put "/u/#{user.username}/preferences/email.json", params: { email: other_user.email.upcase, } expect(response).to_not be_successful end end it "raises an error when new email domain is present in blocked_email_domains site setting" do SiteSetting.blocked_email_domains = "mailinator.com" put "/u/#{user.username}/preferences/email.json", params: { email: "not_good@mailinator.com", } expect(response).to_not be_successful end it "raises an error when new email domain is not present in allowed_email_domains site setting" do SiteSetting.allowed_email_domains = "discourse.org" put "/u/#{user.username}/preferences/email.json", params: { email: "bubblegum@adventuretime.ooo", } expect(response).to_not be_successful end context "with success" do it "has an email token" do expect do put "/u/#{user.username}/preferences/email.json", params: { email: "bubblegum@adventuretime.ooo", } end.to change(EmailChangeRequest, :count) end end end end end