mirror of
https://github.com/discourse/discourse.git
synced 2025-05-23 23:21:11 +08:00

This commit adds the ability for site administrators to mark users' passwords as expired. Note that this commit does not add any client side interface to mark a user's password as expired. The following changes are introduced in this commit: 1. Adds a `user_passwords` table and `UserPassword` model. While the `user_passwords` table is currently used to only store expired passwords, it will be used in the future to store a user's current password as well. 2. Adds a `UserPasswordExpirer.expire_user_password` method which can be used from the Rails console to mark a user's password as expired. 3. Updates `SessionsController#create` to check that the user's current password has not been marked as expired after confirming the password. If the password is determined to be expired based on the existence of a `UserPassword` record with the `password_expired_at` column set, we will not log the user in and will display a password expired notice. A forgot password email is automatically send out to the user as well.
183 lines
6.2 KiB
Ruby
183 lines
6.2 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "rotp"
|
|
|
|
shared_examples "login scenarios" do
|
|
let(:login_modal) { PageObjects::Modals::Login.new }
|
|
fab!(:user) { Fabricate(:user, username: "john", password: "supersecurepassword") }
|
|
|
|
before { Jobs.run_immediately! }
|
|
|
|
context "with username and password" do
|
|
it "can login" do
|
|
EmailToken.confirm(Fabricate(:email_token, user: user).token)
|
|
|
|
login_modal.open
|
|
login_modal.fill(username: "john", password: "supersecurepassword")
|
|
login_modal.click_login
|
|
expect(page).to have_css(".header-dropdown-toggle.current-user")
|
|
end
|
|
|
|
it "can login and activate account" do
|
|
login_modal.open
|
|
login_modal.fill(username: "john", password: "supersecurepassword")
|
|
login_modal.click_login
|
|
expect(page).to have_css(".not-activated-modal")
|
|
login_modal.click(".activation-controls button.resend")
|
|
|
|
wait_for(timeout: 5) { ActionMailer::Base.deliveries.count != 0 }
|
|
|
|
mail = ActionMailer::Base.deliveries.last
|
|
expect(mail.to).to contain_exactly(user.email)
|
|
activation_link = mail.body.to_s[%r{/u/activate-account/\S+}]
|
|
visit activation_link
|
|
|
|
find("#activate-account-button").click
|
|
|
|
visit "/"
|
|
expect(page).to have_css(".header-dropdown-toggle.current-user")
|
|
end
|
|
|
|
it "displays the right message when user's email has been marked as expired" do
|
|
password = "myawesomepassword"
|
|
user.update!(password:)
|
|
expired_user_password = Fabricate(:expired_user_password, user:, password:)
|
|
|
|
login_modal.open
|
|
login_modal.fill(username: user.username, password:)
|
|
login_modal.click_login
|
|
|
|
expect(login_modal).to have_content(I18n.t("login.password_expired"))
|
|
end
|
|
|
|
it "can reset password" do
|
|
login_modal.open
|
|
login_modal.fill_username("john")
|
|
login_modal.forgot_password
|
|
find("button.forgot-password-reset").click
|
|
|
|
wait_for(timeout: 5) { ActionMailer::Base.deliveries.count != 0 }
|
|
|
|
mail = ActionMailer::Base.deliveries.last
|
|
expect(mail.to).to contain_exactly(user.email)
|
|
reset_password_link = mail.body.to_s[%r{/u/password-reset/\S+}]
|
|
visit reset_password_link
|
|
|
|
find("#new-account-password").fill_in(with: "newsuperpassword")
|
|
find("form .btn-primary").click
|
|
expect(page).to have_css(".header-dropdown-toggle.current-user")
|
|
end
|
|
end
|
|
|
|
context "with login link" do
|
|
it "can login" do
|
|
login_modal.open
|
|
login_modal.fill_username("john")
|
|
login_modal.email_login_link
|
|
|
|
wait_for(timeout: 5) { ActionMailer::Base.deliveries.count != 0 }
|
|
|
|
mail = ActionMailer::Base.deliveries.last
|
|
expect(mail.to).to contain_exactly(user.email)
|
|
login_link = mail.body.to_s[%r{/session/email-login/\S+}]
|
|
visit login_link
|
|
|
|
find(".email-login-form .btn-primary").click
|
|
expect(page).to have_css(".header-dropdown-toggle.current-user")
|
|
end
|
|
end
|
|
|
|
context "with two-factor authentication" do
|
|
let!(:user_second_factor) { Fabricate(:user_second_factor_totp, user: user) }
|
|
let!(:user_second_factor_backup) { Fabricate(:user_second_factor_backup, user: user) }
|
|
fab!(:other_user) { Fabricate(:user, username: "jane", password: "supersecurepassword") }
|
|
|
|
before do
|
|
EmailToken.confirm(Fabricate(:email_token, user: user).token)
|
|
EmailToken.confirm(Fabricate(:email_token, user: other_user).token)
|
|
end
|
|
|
|
context "when it is required" do
|
|
before { SiteSetting.enforce_second_factor = "all" }
|
|
|
|
it "requires to set 2FA after login" do
|
|
login_modal.open
|
|
login_modal.fill(username: "jane", password: "supersecurepassword")
|
|
login_modal.click_login
|
|
expect(page).to have_css(".header-dropdown-toggle.current-user")
|
|
expect(page).to have_content(I18n.t("js.user.second_factor.enforced_notice"))
|
|
end
|
|
end
|
|
|
|
it "can login with totp" do
|
|
login_modal.open
|
|
login_modal.fill(username: "john", password: "supersecurepassword")
|
|
login_modal.click_login
|
|
expect(page).to have_css(".login-modal-body.second-factor")
|
|
|
|
totp = ROTP::TOTP.new(user_second_factor.data).now
|
|
find("#login-second-factor").fill_in(with: totp)
|
|
login_modal.click_login
|
|
expect(page).to have_css(".header-dropdown-toggle.current-user")
|
|
end
|
|
|
|
it "can login with backup code" do
|
|
login_modal.open
|
|
login_modal.fill(username: "john", password: "supersecurepassword")
|
|
login_modal.click_login
|
|
expect(page).to have_css(".login-modal-body.second-factor")
|
|
|
|
find(".toggle-second-factor-method").click
|
|
find(".second-factor-token-input").fill_in(with: "iAmValidBackupCode")
|
|
login_modal.click_login
|
|
expect(page).to have_css(".header-dropdown-toggle.current-user")
|
|
end
|
|
|
|
it "can login with login link and totp" do
|
|
login_modal.open
|
|
login_modal.fill_username("john")
|
|
login_modal.email_login_link
|
|
|
|
wait_for(timeout: 5) { ActionMailer::Base.deliveries.count != 0 }
|
|
|
|
mail = ActionMailer::Base.deliveries.last
|
|
expect(mail.to).to contain_exactly(user.email)
|
|
login_link = mail.body.to_s[%r{/session/email-login/\S+}]
|
|
visit login_link
|
|
|
|
totp = ROTP::TOTP.new(user_second_factor.data).now
|
|
find(".second-factor-token-input").fill_in(with: totp)
|
|
find(".email-login-form .btn-primary").click
|
|
expect(page).to have_css(".header-dropdown-toggle.current-user")
|
|
end
|
|
|
|
it "can login with login link and backup code" do
|
|
login_modal.open
|
|
login_modal.fill_username("john")
|
|
login_modal.email_login_link
|
|
|
|
wait_for(timeout: 5) { ActionMailer::Base.deliveries.count != 0 }
|
|
|
|
mail = ActionMailer::Base.deliveries.last
|
|
expect(mail.to).to contain_exactly(user.email)
|
|
login_link = mail.body.to_s[%r{/session/email-login/\S+}]
|
|
visit login_link
|
|
|
|
find(".toggle-second-factor-method").click
|
|
find(".second-factor-token-input").fill_in(with: "iAmValidBackupCode")
|
|
find(".email-login-form .btn-primary").click
|
|
expect(page).to have_css(".header-dropdown-toggle.current-user")
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "Login", type: :system do
|
|
context "when desktop" do
|
|
include_examples "login scenarios"
|
|
end
|
|
|
|
context "when mobile", mobile: true do
|
|
include_examples "login scenarios"
|
|
end
|
|
end
|