FEATURE: Allow site admin to mark a user's password as expired (#27314)

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.
This commit is contained in:
Alan Guo Xiang Tan
2024-06-04 15:42:53 +08:00
committed by GitHub
parent 30f55cd64b
commit e97ef7e9af
14 changed files with 370 additions and 15 deletions

View File

@ -1991,7 +1991,7 @@ RSpec.describe SessionController do
end
end
describe "success by username" do
describe "success by username and password" do
it "logs in correctly" do
events =
DiscourseEvent.track_events do
@ -2030,6 +2030,70 @@ RSpec.describe SessionController do
end
end
describe "when user's password has been marked as expired" do
let!(:expired_user_password) do
Fabricate(
:expired_user_password,
user:,
password: "myawesomepassword",
password_salt: user.salt,
password_algorithm: user.password_algorithm,
)
end
before { RateLimiter.enable }
use_redis_snapshotting
it "should return an error response code with the right error message and enqueues the password reset email" do
expect_enqueued_with(
job: :critical_user_email,
args: {
type: "forgot_password",
user_id: user.id,
},
) do
post "/session.json", params: { login: user.username, password: "myawesomepassword" }
end
expect(response.status).to eq(200)
expect(response.parsed_body["error"]).to eq(I18n.t("login.password_expired"))
expect(session[:current_user_id]).to eq(nil)
end
it "should limit the number of forgot password emails sent a day to the user when logging in with an expired password" do
SiteSetting.max_logins_per_ip_per_minute =
described_class::FORGOT_PASSWORD_EMAIL_LIMIT_PER_DAY + 1
SiteSetting.max_logins_per_ip_per_hour =
described_class::FORGOT_PASSWORD_EMAIL_LIMIT_PER_DAY + 1
described_class::FORGOT_PASSWORD_EMAIL_LIMIT_PER_DAY.times do
expect_enqueued_with(
job: :critical_user_email,
args: {
type: "forgot_password",
user_id: user.id,
},
) do
post "/session.json", params: { login: user.username, password: "myawesomepassword" }
expect(response.status).to eq(200)
end
end
expect_not_enqueued_with(
job: :critical_user_email,
args: {
type: "forgot_password",
user_id: user.id,
},
) do
post "/session.json", params: { login: user.username, password: "myawesomepassword" }
expect(response.status).to eq(200)
end
end
end
context "when a user has security key-only 2FA login" do
let!(:user_security_key) do
Fabricate(