discourse/spec/system/user_page/user_preferences_security_spec.rb
Penar Musaraj 974b3a2a6f
DEV: Do not require session confirmation for new users (#24799)
When making sensitive changes to an account (adding 2FA or passkeys), we
require users to confirm their password. This is to prevent an attacker
from adding 2FA to an account they have access to.

However, on newly created accounts, we should not require this, it's an
extra step and it doesn't provide extra security (since the account was
just created). This commit makes it so that we don't require session
confirmation for accounts created less than 5 minutes ago.
2024-02-15 12:29:16 -05:00

150 lines
4.9 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(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