mirror of
https://github.com/discourse/discourse.git
synced 2025-06-06 13:06:56 +08:00

Added button to remove password from account if user has a linked external account or passkey The button only displays if the user has at least one associated account or a passkey set up. Uses the ConfirmSession dialog in addition to a warning about deleting the password. Users can still reset their password via the Reset Password button (which will now display "Set Password" if they've removed it). Also prevent user from removing their last remaining associated account or passkey if they have no password set. Replaces PR #31489 from my personal repo, with some fixes for conflicts since then.
155 lines
5.2 KiB
Ruby
155 lines
5.2 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
describe "User preferences | Security", type: :system do
|
|
fab!(:password) { "kungfukenny" }
|
|
fab!(:email) { "email@user.com" }
|
|
fab!(:user) { Fabricate(:user, email: email, password: password) }
|
|
let(:user_preferences_security_page) { PageObjects::Pages::UserPreferencesSecurity.new }
|
|
let(:user_menu) { PageObjects::Components::UserMenu.new }
|
|
|
|
before do
|
|
user.activate
|
|
# testing the enforced 2FA flow requires a user that was created > 5 minutes ago
|
|
user.created_at = 6.minutes.ago
|
|
user.save!
|
|
sign_in(user)
|
|
|
|
# system specs run on their own host + port
|
|
DiscourseWebauthn.stubs(:origin).returns(current_host + ":" + Capybara.server_port.to_s)
|
|
end
|
|
|
|
shared_examples "security keys" do
|
|
it "adds a 2FA security key and logs in with it" do
|
|
options = ::Selenium::WebDriver::VirtualAuthenticatorOptions.new
|
|
authenticator = page.driver.browser.add_virtual_authenticator(options)
|
|
|
|
user_preferences_security_page.visit(user)
|
|
user_preferences_security_page.visit_second_factor(user, password)
|
|
|
|
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
|
|
|
|
# login flow
|
|
find(".d-header .login-button").click
|
|
find("input#login-account-name").fill_in(with: user.username)
|
|
find("input#login-account-password").fill_in(with: password)
|
|
|
|
find("#login-button.btn-primary").click
|
|
find("#security-key .btn-primary").click
|
|
|
|
expect(page).to have_css(".header-dropdown-toggle.current-user")
|
|
ensure
|
|
# clear authenticator (otherwise it will interfere with other tests)
|
|
authenticator&.remove!
|
|
end
|
|
end
|
|
|
|
shared_examples "passkeys" do
|
|
before { SiteSetting.enable_passkeys = true }
|
|
|
|
it "adds a passkey, removes user password, logs in with passkey" do
|
|
options =
|
|
::Selenium::WebDriver::VirtualAuthenticatorOptions.new(
|
|
user_verification: true,
|
|
user_verified: true,
|
|
resident_key: true,
|
|
)
|
|
authenticator = page.driver.browser.add_virtual_authenticator(options)
|
|
|
|
page.driver.browser.manage.add_cookie(
|
|
domain: Discourse.current_hostname,
|
|
name: "destination_url",
|
|
value: "/new",
|
|
path: "/",
|
|
)
|
|
|
|
user_preferences_security_page.visit(user)
|
|
|
|
find(".pref-passkeys__add .btn").click
|
|
expect(user_preferences_security_page).to have_css("input#password")
|
|
|
|
find(".dialog-body input#password").fill_in(with: password)
|
|
find(".confirm-session .btn-primary").click
|
|
|
|
expect(user_preferences_security_page).to have_css(".rename-passkey__form")
|
|
|
|
find(".dialog-close").click
|
|
|
|
expect(user_preferences_security_page).to have_css(".pref-passkeys__rows .row")
|
|
|
|
select_kit = PageObjects::Components::SelectKit.new(".passkey-options-dropdown")
|
|
select_kit.expand
|
|
select_kit.select_row_by_name("Delete")
|
|
|
|
# confirm deletion screen shown without requiring session confirmation
|
|
# since this was already done when adding the passkey
|
|
expect(user_preferences_security_page).to have_css(".dialog-footer .btn-danger")
|
|
|
|
# close the dialog (don't delete the key, we need it to login in the next step)
|
|
find(".dialog-close").click
|
|
|
|
find("#remove-password-button").click
|
|
# already confirmed session for the passkey, so this will go straight for the confirmation dialog
|
|
find(".dialog-footer .btn-danger").click
|
|
expect(user_preferences_security_page).to have_no_css("#remove-password-button")
|
|
|
|
user_menu.sign_out
|
|
|
|
# login with the key we just created
|
|
# this triggers the conditional UI for passkeys
|
|
# which uses the virtual authenticator
|
|
find(".d-header .login-button").click
|
|
|
|
expect(page).to have_css(".header-dropdown-toggle.current-user")
|
|
|
|
# ensures that we are redirected to the destination_url cookie
|
|
expect(page.driver.current_url).to include("/new")
|
|
ensure
|
|
# clear authenticator (otherwise it will interfere with other tests)
|
|
authenticator&.remove!
|
|
end
|
|
end
|
|
|
|
shared_examples "enforced second factor" do
|
|
it "allows user to add 2FA" do
|
|
SiteSetting.enforce_second_factor = "all"
|
|
|
|
visit("/")
|
|
|
|
expect(page).to have_selector(
|
|
".alert-error",
|
|
text: "You are required to enable two-factor authentication before accessing this site.",
|
|
)
|
|
|
|
expect(page).to have_css(".user-preferences .totp")
|
|
expect(page).to have_css(".user-preferences .security-key")
|
|
|
|
find(".user-preferences .totp .btn.new-totp").click
|
|
|
|
find(".dialog-body input#password").fill_in(with: password)
|
|
find(".confirm-session .btn-primary").click
|
|
|
|
expect(page).to have_css(".qr-code")
|
|
end
|
|
end
|
|
|
|
context "when desktop" do
|
|
include_examples "security keys"
|
|
include_examples "passkeys"
|
|
include_examples "enforced second factor"
|
|
end
|
|
|
|
context "when mobile", mobile: true do
|
|
include_examples "security keys"
|
|
include_examples "passkeys"
|
|
include_examples "enforced second factor"
|
|
end
|
|
end
|