discourse/spec/system/forgot_password_spec.rb
Alan Guo Xiang Tan 952f69ce60
FIX: User can't reset password with backup codes when only security key is enabled (#27368)
This commit fixes a problem where the user will not be able to reset
their password when they only have security keys and backup codes
configured.

This commit also makes the following changes/fixes:

1. Splits password reset system tests to
   `spec/system/forgot_password_spec.rb` instead of missing the system
   tests in `spec/system/login_spec.rb` which is mainly used to test
   the login flow.

2. Fixes a UX issue where the `Use backup codes` or `Use authenticator
   app` text is shown on the reset password form when the user does
   not have either backup codes or an authenticator app configured.
2024-06-06 14:30:42 +08:00

221 lines
6.9 KiB
Ruby

# frozen_string_literal: true
require "rotp"
shared_examples "forgot password scenarios" do
let(:login_modal) { PageObjects::Modals::Login.new }
let(:user_preferences_security_page) { PageObjects::Pages::UserPreferencesSecurity.new }
fab!(:user) { Fabricate(:user, username: "john", password: "supersecurepassword") }
fab!(:password_reset_token) do
Fabricate(
:email_token,
user:,
scope: EmailToken.scopes[:password_reset],
email: user.email,
).token
end
let(:user_menu) { PageObjects::Components::UserMenu.new }
let(:user_reset_password_page) { PageObjects::Pages::UserResetPassword.new }
def visit_reset_password_link
visit("/u/password-reset/#{password_reset_token}")
end
def create_user_security_key(user)
# testing the 2FA flow requires a user that was created > 5 minutes ago
user.update!(created_at: 6.minutes.ago)
sign_in(user)
user_preferences_security_page.visit(user)
user_preferences_security_page.visit_second_factor("supersecurepassword")
find(".security-key .new-security-key").click
expect(user_preferences_security_page).to have_css("input#security-key-name")
find(".d-modal__body input#security-key-name").fill_in(with: "First Key")
find(".add-security-key").click
expect(user_preferences_security_page).to have_css(".security-key .second-factor-item")
user_menu.sign_out
end
context "when user does not have any multi-factor authentication configured" do
it "should allow a user to reset their password" do
visit_reset_password_link
user_reset_password_page.fill_in_new_password("newsuperpassword").submit_new_password
expect(user_reset_password_page).to have_logged_in_user
end
end
context "when user has multi-factor authentication configured" do
context "when user only has TOTP configured" do
fab!(:user_second_factor_totp) { Fabricate(:user_second_factor_totp, user:) }
it "should allow a user to reset password with TOTP" do
visit_reset_password_link
expect(user_reset_password_page).to have_no_toggle_button_to_second_factor_form
user_reset_password_page
.fill_in_totp(ROTP::TOTP.new(user_second_factor_totp.data).now)
.submit_totp
.fill_in_new_password("newsuperpassword")
.submit_new_password
expect(user_reset_password_page).to have_logged_in_user
end
end
context "when user only has security key configured" do
before do
@authenticator =
page.driver.browser.add_virtual_authenticator(
Selenium::WebDriver::VirtualAuthenticatorOptions.new,
)
create_user_security_key(user)
end
after { @authenticator.remove! }
it "should allow a user to reset password with a security key" do
visit_reset_password_link
expect(user_reset_password_page).to have_no_toggle_button_to_second_factor_form
user_reset_password_page.submit_security_key
user_reset_password_page.fill_in_new_password("newsuperpassword").submit_new_password
expect(user_reset_password_page).to have_logged_in_user
end
end
context "when user has TOTP and backup codes configured" do
fab!(:user_second_factor_backup) { Fabricate(:user_second_factor_backup, user:) }
fab!(:user_second_factor_totp) { Fabricate(:user_second_factor_totp, user:) }
it "should allow a user to reset password with backup code" do
visit_reset_password_link
user_reset_password_page
.use_backup_codes
.fill_in_backup_code("iAmValidBackupCode")
.submit_backup_code
.fill_in_new_password("newsuperpassword")
.submit_new_password
expect(user_reset_password_page).to have_logged_in_user
end
end
context "when user has security key and backup codes configured" do
fab!(:user_second_factor_backup) { Fabricate(:user_second_factor_backup, user:) }
before do
@authenticator =
page.driver.browser.add_virtual_authenticator(
Selenium::WebDriver::VirtualAuthenticatorOptions.new,
)
create_user_security_key(user)
end
after { @authenticator.remove! }
it "should allow a user to reset password with backup code instead of security key" do
visit_reset_password_link
user_reset_password_page.try_another_way
expect(user_reset_password_page).to have_no_toggle_button_in_second_factor_form
user_reset_password_page
.fill_in_backup_code("iAmValidBackupCode")
.submit_backup_code
.fill_in_new_password("newsuperpassword")
.submit_new_password
expect(user_reset_password_page).to have_logged_in_user
end
end
context "when user has TOTP, security key and backup codes configured" do
fab!(:user_second_factor_totp) { Fabricate(:user_second_factor_totp, user:) }
fab!(:user_second_factor_backup) { Fabricate(:user_second_factor_backup, user:) }
before do
@authenticator =
page.driver.browser.add_virtual_authenticator(
Selenium::WebDriver::VirtualAuthenticatorOptions.new,
)
create_user_security_key(user)
end
after { @authenticator.remove! }
it "should allow a user to toggle from security key to TOTP and between TOTP and backup codes" do
visit_reset_password_link
user_reset_password_page.try_another_way
expect(user_reset_password_page).to have_totp_description
user_reset_password_page.use_backup_codes
expect(user_reset_password_page).to have_backup_codes_description
user_reset_password_page.use_totp
expect(user_reset_password_page).to have_totp_description
end
end
context "when user has TOTP and security key configured but no backup codes" do
fab!(:user_second_factor_totp) { Fabricate(:user_second_factor_totp, user:) }
before do
@authenticator =
page.driver.browser.add_virtual_authenticator(
Selenium::WebDriver::VirtualAuthenticatorOptions.new,
)
create_user_security_key(user)
end
after { @authenticator.remove! }
it "should allow a user to reset password with TOTP instead of security key" do
visit_reset_password_link
user_reset_password_page.try_another_way
expect(user_reset_password_page).to have_no_toggle_button_in_second_factor_form
user_reset_password_page
.fill_in_totp(ROTP::TOTP.new(user_second_factor_totp.data).now)
.submit_totp
.fill_in_new_password("newsuperpassword")
.submit_new_password
expect(user_reset_password_page).to have_logged_in_user
end
end
end
end
describe "User resetting password", type: :system do
context "when desktop" do
include_examples "forgot password scenarios"
end
context "when mobile", mobile: true do
include_examples "forgot password scenarios"
end
end