2
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-03-03 23:54:20 +08:00
discourse/spec/models/user_auth_token_spec.rb
Ted Johansson b24a3d81ed
DEV: Allow impersonation without session swapping (#34213)
The current impersonation feature works by signing you in as the user you are impersonating. This has the side effect of invalidating your own session and forcing you to log out and in again.

In this experimental implementation you keep your existing session, but DefaultCurrentUserProvider returns the user being impersonated, allowing you to see the site from their perspective.
2025-08-21 14:18:15 +08:00

392 lines
11 KiB
Ruby

# frozen_string_literal: true
require "discourse_ip_info"
RSpec.describe UserAuthToken do
fab!(:user)
fab!(:admin)
describe "#user" do
context "when no impersonation is happening" do
it "returns the user associated with the session" do
token =
UserAuthToken.generate!(
user_id: user.id,
user_agent: "some user agent 2",
client_ip: "1.1.2.3",
)
expect(token.user).to eq(user)
expect(token.user.is_impersonating).to be_falsey
end
end
context "when impersonating another user" do
it "returns the user being impersonated if not expired" do
token =
UserAuthToken.generate!(
user_id: admin.id,
user_agent: "some user agent 2",
client_ip: "1.1.2.3",
)
token.update!(impersonated_user_id: user.id, impersonation_expires_at: 15.minutes.from_now)
expect(token.user).to eq(user)
expect(token.user.is_impersonating).to eq(true)
end
it "returns the user associated with the session if expired" do
token =
UserAuthToken.generate!(
user_id: admin.id,
user_agent: "some user agent 2",
client_ip: "1.1.2.3",
)
token.update!(impersonated_user_id: user.id, impersonation_expires_at: 1.hour.ago)
expect(token.user).to eq(admin)
expect(token.user.is_impersonating).to be_falsey
end
it "returns the user associated with the session if can no longer impersonate" do
token =
UserAuthToken.generate!(
user_id: admin.id,
user_agent: "some user agent 2",
client_ip: "1.1.2.3",
)
token.update!(impersonated_user_id: user.id, impersonation_expires_at: 15.minutes.from_now)
Guardian.any_instance.stubs(:can_impersonate?).returns(false)
expect(token.user).to eq(admin)
expect(token.user.is_impersonating).to be_falsey
end
end
end
describe ".cleanup!" do
it "can remove old expired tokens" do
freeze_time Time.zone.now
SiteSetting.maximum_session_age = 1
token =
UserAuthToken.generate!(
user_id: user.id,
user_agent: "some user agent 2",
client_ip: "1.1.2.3",
)
freeze_time 1.hour.from_now
UserAuthToken.cleanup!
expect(UserAuthToken.where(id: token.id).count).to eq(1)
freeze_time 1.second.from_now
UserAuthToken.cleanup!
expect(UserAuthToken.where(id: token.id).count).to eq(1)
freeze_time UserAuthToken::ROTATE_TIME.from_now
UserAuthToken.cleanup!
expect(UserAuthToken.where(id: token.id).count).to eq(0)
end
it "deletes old logs excluding the `suspicious` and `generate` types" do
SiteSetting.maximum_session_age = 1
UserAuthTokenLog.delete_all
preserved = []
preserved << UserAuthTokenLog.create!(action: "suspicious").id
preserved << UserAuthTokenLog.create!(action: "suspicious", created_at: 10.days.ago).id
preserved << UserAuthTokenLog.create!(action: "generate").id
preserved << UserAuthTokenLog.create!(action: "generate", created_at: 10.years.ago).id
preserved << UserAuthTokenLog.create!(action: "random but fresh").id
UserAuthTokenLog.create!(action: "random but not very fresh", created_at: 2.hours.ago)
expect do UserAuthToken.cleanup! end.to change { UserAuthTokenLog.count }.by(-1)
expect(UserAuthTokenLog.pluck(:id)).to contain_exactly(*preserved)
end
end
it "can lookup hashed" do
token =
UserAuthToken.generate!(
user_id: user.id,
user_agent: "some user agent 2",
client_ip: "1.1.2.3",
)
lookup_token = UserAuthToken.lookup(token.unhashed_auth_token)
expect(user.id).to eq(lookup_token.user.id)
lookup_token = UserAuthToken.lookup(token.auth_token)
expect(lookup_token).to eq(nil)
end
it "can validate token was seen at lookup time" do
user_token =
UserAuthToken.generate!(
user_id: user.id,
user_agent: "some user agent 2",
client_ip: "1.1.2.3",
)
expect(user_token.auth_token_seen).to eq(false)
UserAuthToken.lookup(user_token.unhashed_auth_token, seen: true)
user_token.reload
expect(user_token.auth_token_seen).to eq(true)
end
it "can rotate with no params maintaining data" do
user_token =
UserAuthToken.generate!(
user_id: user.id,
user_agent: "some user agent 2",
client_ip: "1.1.2.3",
)
user_token.update_columns(auth_token_seen: true)
expect(user_token.rotate!).to eq(true)
user_token.reload
expect(user_token.client_ip.to_s).to eq("1.1.2.3")
expect(user_token.user_agent).to eq("some user agent 2")
end
it "expires correctly" do
freeze_time Time.zone.now
user_token =
UserAuthToken.generate!(
user_id: user.id,
user_agent: "some user agent 2",
client_ip: "1.1.2.3",
)
UserAuthToken.lookup(user_token.unhashed_auth_token, seen: true)
freeze_time SiteSetting.maximum_session_age.hours.from_now - 1.second
user_token.reload
user_token.rotate!
UserAuthToken.lookup(user_token.unhashed_auth_token, seen: true)
freeze_time SiteSetting.maximum_session_age.hours.from_now - 1.second
still_good = UserAuthToken.lookup(user_token.unhashed_auth_token, seen: true)
expect(still_good).not_to eq(nil)
freeze_time 2.hours.from_now
not_good = UserAuthToken.lookup(user_token.unhashed_auth_token, seen: true)
expect(not_good).to eq(nil)
end
it "can properly rotate tokens" do
freeze_time 3.days.ago
user_token =
UserAuthToken.generate!(
user_id: user.id,
user_agent: "some user agent 2",
client_ip: "1.1.2.3",
)
prev_auth_token = user_token.auth_token
unhashed_prev = user_token.unhashed_auth_token
rotated = user_token.rotate!(user_agent: "a new user agent", client_ip: "1.1.2.4")
expect(rotated).to eq(false)
user_token.update_columns(auth_token_seen: true)
rotation_time = freeze_time 1.day.from_now
rotated = user_token.rotate!(user_agent: "a new user agent", client_ip: "1.1.2.4")
expect(rotated).to eq(true)
user_token.reload
expect(user_token.rotated_at).to eq_time(rotation_time)
expect(user_token.client_ip).to eq("1.1.2.4")
expect(user_token.user_agent).to eq("a new user agent")
expect(user_token.auth_token_seen).to eq(false)
expect(user_token.seen_at).to eq(nil)
expect(user_token.prev_auth_token).to eq(prev_auth_token)
# ability to auth using an old token
seen_at = freeze_time 1.day.from_now
looked_up = UserAuthToken.lookup(user_token.unhashed_auth_token, seen: true)
expect(looked_up.id).to eq(user_token.id)
expect(looked_up.auth_token_seen).to eq(true)
expect(looked_up.seen_at).to eq_time(seen_at)
looked_up = UserAuthToken.lookup(unhashed_prev, seen: true)
expect(looked_up.id).to eq(user_token.id)
freeze_time 2.minutes.from_now
looked_up = UserAuthToken.lookup(unhashed_prev)
expect(looked_up).not_to eq(nil)
looked_up.reload
expect(looked_up.auth_token_seen).to eq(false)
rotated = user_token.rotate!(user_agent: "a new user agent", client_ip: "1.1.2.4")
expect(rotated).to eq(true)
user_token.reload
expect(user_token.seen_at).to eq(nil)
end
it "keeps prev token valid for 1 minute after it is confirmed" do
token =
UserAuthToken.generate!(user_id: user.id, user_agent: "some user agent", client_ip: "1.1.2.3")
UserAuthToken.lookup(token.unhashed_auth_token, seen: true)
freeze_time(10.minutes.from_now)
prev_token = token.unhashed_auth_token
token.rotate!(user_agent: "firefox", client_ip: "1.1.1.1")
freeze_time(10.minutes.from_now)
expect(UserAuthToken.lookup(token.unhashed_auth_token, seen: true)).not_to eq(nil)
expect(UserAuthToken.lookup(prev_token, seen: true)).not_to eq(nil)
end
it "can correctly log auth tokens" do
SiteSetting.verbose_auth_token_logging = true
token =
UserAuthToken.generate!(user_id: user.id, user_agent: "some user agent", client_ip: "1.1.2.3")
expect(
UserAuthTokenLog.where(
action: "generate",
user_id: user.id,
user_agent: "some user agent",
client_ip: "1.1.2.3",
user_auth_token_id: token.id,
).count,
).to eq(1)
UserAuthToken.lookup(
token.unhashed_auth_token,
seen: true,
user_agent: "something diff",
client_ip: "1.2.3.3",
)
UserAuthToken.lookup(
token.unhashed_auth_token,
seen: true,
user_agent: "something diff2",
client_ip: "1.2.3.3",
)
expect(
UserAuthTokenLog.where(
action: "seen token",
user_id: user.id,
auth_token: token.auth_token,
client_ip: "1.2.3.3",
user_auth_token_id: token.id,
).count,
).to eq(1)
fake_token = SecureRandom.hex
UserAuthToken.lookup(
fake_token,
seen: true,
user_agent: "bob",
client_ip: "127.0.0.1",
path: "/path",
)
expect(
UserAuthTokenLog.where(
action: "miss token",
auth_token: UserAuthToken.hash_token(fake_token),
user_agent: "bob",
client_ip: "127.0.0.1",
path: "/path",
).count,
).to eq(1)
freeze_time(UserAuthToken::ROTATE_TIME.from_now)
token.rotate!(user_agent: "firefox", client_ip: "1.1.1.1")
expect(
UserAuthTokenLog.where(
action: "rotate",
auth_token: token.auth_token,
user_agent: "firefox",
client_ip: "1.1.1.1",
user_auth_token_id: token.id,
).count,
).to eq(1)
end
it "calls before_destroy" do
SiteSetting.verbose_auth_token_logging = true
token =
UserAuthToken.generate!(user_id: user.id, user_agent: "some user agent", client_ip: "1.1.2.3")
expect(user.user_auth_token_logs.count).to eq(1)
token.destroy
expect(user.user_auth_token_logs.count).to eq(2)
expect(user.user_auth_token_logs.last.action).to eq("destroy")
expect(user.user_auth_token_logs.last.user_agent).to eq("some user agent")
expect(user.user_auth_token_logs.last.client_ip).to eq("1.1.2.3")
end
it "will not mark token unseen when prev and current are the same" do
token =
UserAuthToken.generate!(user_id: user.id, user_agent: "some user agent", client_ip: "1.1.2.3")
lookup = UserAuthToken.lookup(token.unhashed_auth_token, seen: true)
lookup = UserAuthToken.lookup(token.unhashed_auth_token, seen: true)
lookup.reload
expect(lookup.auth_token_seen).to eq(true)
end
context "with suspicious login" do
fab!(:admin)
it "is not checked when generated for non-staff" do
UserAuthToken.generate!(user_id: user.id, staff: user.staff?)
expect(Jobs::SuspiciousLogin.jobs.size).to eq(0)
end
it "is checked when generated for staff" do
UserAuthToken.generate!(user_id: admin.id, staff: admin.staff?)
expect(Jobs::SuspiciousLogin.jobs.size).to eq(1)
end
it "is not checked when generated by impersonate" do
UserAuthToken.generate!(user_id: admin.id, staff: admin.staff?, impersonate: true)
expect(Jobs::SuspiciousLogin.jobs.size).to eq(0)
end
end
end