# 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(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(".d-modal__footer .btn-primary").click find("#security-key .btn-primary").click expect(page).to have_css(".header-dropdown-toggle.current-user") # 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 and logs in with it" 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 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") # 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