mirror of
https://github.com/discourse/discourse.git
synced 2024-12-25 08:43:44 +08:00
1bfccdd4f2
In a handful of situations, we need to verify a user's 2fa credentials before `current_user` is assigned. For example: login, email_login and change-email confirmation. This commit adds an explicit `target_user:` parameter to the centralized 2fa system so that it can be used for those situations. For safety and clarity, this new parameter only works for anon. If some user is logged in, and target_user is set to a different user, an exception will be raised.
237 lines
9.7 KiB
Ruby
237 lines
9.7 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
RSpec.describe SecondFactor::AuthManager do
|
|
fab!(:user)
|
|
let(:guardian) { Guardian.new(user) }
|
|
fab!(:user_totp) { Fabricate(:user_second_factor_totp, user: user) }
|
|
|
|
def create_request(request_method: "GET", path: "/")
|
|
ActionDispatch::TestRequest.create({ "REQUEST_METHOD" => request_method, "PATH_INFO" => path })
|
|
end
|
|
|
|
def create_manager(action)
|
|
SecondFactor::AuthManager.new(guardian, action, target_user: user)
|
|
end
|
|
|
|
def create_action(request = nil)
|
|
request ||= create_request
|
|
TestSecondFactorAction.new(guardian, request, target_user: user)
|
|
end
|
|
|
|
def stage_challenge(successful:)
|
|
request = create_request(request_method: "POST", path: "/abc/xyz")
|
|
action = create_action(request)
|
|
action.expects(:no_second_factors_enabled!).never
|
|
action
|
|
.expects(:second_factor_auth_required!)
|
|
.with({ random_param: "hello" })
|
|
.returns({ callback_params: { call_me_back: 4314 } })
|
|
.once
|
|
manager = create_manager(action)
|
|
secure_session = {}
|
|
expect { manager.run!(request, { random_param: "hello" }, secure_session) }.to raise_error(
|
|
SecondFactor::AuthManager::SecondFactorRequired,
|
|
) do |ex|
|
|
expect(ex.nonce).to be_present
|
|
end
|
|
|
|
challenge =
|
|
JSON.parse(secure_session["current_second_factor_auth_challenge"]).deep_symbolize_keys
|
|
|
|
if successful
|
|
challenge[:successful] = true
|
|
secure_session["current_second_factor_auth_challenge"] = challenge.to_json
|
|
end
|
|
[challenge[:nonce], secure_session]
|
|
end
|
|
|
|
describe "#allow_backup_codes!" do
|
|
it "adds the backup codes method to the allowed methods set" do
|
|
manager = create_manager(create_action)
|
|
expect(manager.allowed_methods).not_to include(UserSecondFactor.methods[:backup_codes])
|
|
manager.allow_backup_codes!
|
|
expect(manager.allowed_methods).to include(UserSecondFactor.methods[:backup_codes])
|
|
end
|
|
end
|
|
|
|
describe "#run!" do
|
|
context "when the user does not have a suitable 2FA method" do
|
|
before { user_totp.destroy! }
|
|
|
|
it "calls the no_second_factors_enabled! method of the action" do
|
|
action = create_action
|
|
action.expects(:no_second_factors_enabled!).with({ hello_world: 331 }).once
|
|
action.expects(:second_factor_auth_required!).never
|
|
action.expects(:second_factor_auth_completed!).never
|
|
manager = create_manager(action)
|
|
manager.run!(create_request, { hello_world: 331 }, {})
|
|
end
|
|
end
|
|
|
|
it "initiates the 2FA process and stages a challenge in secure session when there is no nonce in params" do
|
|
request = create_request(request_method: "POST", path: "/abc/xyz")
|
|
action = create_action(request)
|
|
action.expects(:no_second_factors_enabled!).never
|
|
action
|
|
.expects(:second_factor_auth_required!)
|
|
.with({ expect_me: 131 })
|
|
.returns(
|
|
callback_params: {
|
|
call_me_back: 4314,
|
|
},
|
|
redirect_url: "/gg",
|
|
description: "hello world!",
|
|
)
|
|
.once
|
|
action.expects(:second_factor_auth_completed!).never
|
|
manager = create_manager(action)
|
|
secure_session = {}
|
|
expect { manager.run!(request, { expect_me: 131 }, secure_session) }.to raise_error(
|
|
SecondFactor::AuthManager::SecondFactorRequired,
|
|
)
|
|
json = secure_session["current_second_factor_auth_challenge"]
|
|
challenge = JSON.parse(json).deep_symbolize_keys
|
|
expect(challenge[:nonce]).to be_present
|
|
expect(challenge[:callback_method]).to eq("POST")
|
|
expect(challenge[:callback_path]).to eq("/abc/xyz")
|
|
expect(challenge[:redirect_url]).to eq("/gg")
|
|
expect(challenge[:allowed_methods]).to eq(manager.allowed_methods.to_a)
|
|
expect(challenge[:callback_params]).to eq({ call_me_back: 4314 })
|
|
expect(challenge[:description]).to eq("hello world!")
|
|
end
|
|
|
|
it "prefers callback_method and callback_path from the output of the action's second_factor_auth_required! method if they're present" do
|
|
request = create_request(request_method: "POST", path: "/abc/xyz")
|
|
action = create_action(request)
|
|
action
|
|
.expects(:second_factor_auth_required!)
|
|
.with({})
|
|
.returns(
|
|
callback_params: {
|
|
call_me_back: 4314,
|
|
},
|
|
callback_method: "PUT",
|
|
callback_path: "/test/443",
|
|
)
|
|
.once
|
|
manager = create_manager(action)
|
|
secure_session = {}
|
|
expect { manager.run!(request, {}, secure_session) }.to raise_error(
|
|
SecondFactor::AuthManager::SecondFactorRequired,
|
|
)
|
|
json = secure_session["current_second_factor_auth_challenge"]
|
|
challenge = JSON.parse(json).deep_symbolize_keys
|
|
expect(challenge[:callback_method]).to eq("PUT")
|
|
expect(challenge[:callback_path]).to eq("/test/443")
|
|
end
|
|
|
|
it "calls the second_factor_auth_completed! method of the action if the challenge is successful and not expired" do
|
|
nonce, secure_session = stage_challenge(successful: true)
|
|
|
|
request = create_request(request_method: "POST", path: "/abc/xyz")
|
|
action = create_action(request)
|
|
|
|
action.expects(:no_second_factors_enabled!).never
|
|
action.expects(:second_factor_auth_required!).never
|
|
action.expects(:second_factor_auth_completed!).with({ call_me_back: 4314 }).once
|
|
manager = create_manager(action)
|
|
manager.run!(request, { second_factor_nonce: nonce }, secure_session)
|
|
end
|
|
|
|
it "does not call the second_factor_auth_completed! method of the action if the challenge is not marked successful" do
|
|
nonce, secure_session = stage_challenge(successful: false)
|
|
|
|
request = create_request(request_method: "POST", path: "/abc/xyz")
|
|
action = create_action(request)
|
|
action.expects(:no_second_factors_enabled!).never
|
|
action.expects(:second_factor_auth_required!).never
|
|
action.expects(:second_factor_auth_completed!).never
|
|
manager = create_manager(action)
|
|
expect {
|
|
manager.run!(request, { second_factor_nonce: nonce }, secure_session)
|
|
}.to raise_error(SecondFactor::BadChallenge) do |ex|
|
|
expect(ex.error_translation_key).to eq("second_factor_auth.challenge_not_completed")
|
|
end
|
|
end
|
|
|
|
it "does not call the second_factor_auth_completed! method of the action if the challenge is expired" do
|
|
nonce, secure_session = stage_challenge(successful: true)
|
|
|
|
request = create_request(request_method: "POST", path: "/abc/xyz")
|
|
action = create_action(request)
|
|
action.expects(:no_second_factors_enabled!).never
|
|
action.expects(:second_factor_auth_required!).never
|
|
action.expects(:second_factor_auth_completed!).never
|
|
manager = create_manager(action)
|
|
|
|
freeze_time (SecondFactor::AuthManager::MAX_CHALLENGE_AGE + 1.minute).from_now
|
|
expect {
|
|
manager.run!(request, { second_factor_nonce: nonce }, secure_session)
|
|
}.to raise_error(SecondFactor::BadChallenge) do |ex|
|
|
expect(ex.error_translation_key).to eq("second_factor_auth.challenge_expired")
|
|
end
|
|
end
|
|
|
|
it "calls second_factor_auth_skipped! if skip_second_factor_auth? return true" do
|
|
action = create_action
|
|
params = { a: 1 }
|
|
action.expects(:skip_second_factor_auth?).with(params).returns(true).once
|
|
action.expects(:second_factor_auth_skipped!).with(params).once
|
|
action.expects(:no_second_factors_enabled!).never
|
|
action.expects(:second_factor_auth_required!).never
|
|
action.expects(:second_factor_auth_completed!).never
|
|
manager = create_manager(action)
|
|
manager.run!(action.request, params, {})
|
|
end
|
|
|
|
it "doesn't call second_factor_auth_skipped! if skip_second_factor_auth? return false" do
|
|
action = create_action
|
|
params = { a: 1 }
|
|
action.expects(:skip_second_factor_auth?).with(params).returns(false).once
|
|
action.expects(:second_factor_auth_skipped!).never
|
|
action.expects(:no_second_factors_enabled!).never
|
|
action.expects(:second_factor_auth_required!).with(params).returns({}).once
|
|
action.expects(:second_factor_auth_completed!).never
|
|
manager = create_manager(action)
|
|
expect { manager.run!(action.request, params, {}) }.to raise_error(
|
|
SecondFactor::AuthManager::SecondFactorRequired,
|
|
) do |ex|
|
|
expect(ex.nonce).to be_present
|
|
end
|
|
end
|
|
|
|
context "with returned results object" do
|
|
it "has the correct status and contains the return value of the action hook that's called" do
|
|
action = create_action
|
|
action.expects(:skip_second_factor_auth?).with({}).returns(true).once
|
|
action.expects(:second_factor_auth_skipped!).with({}).returns("yeah whatever").once
|
|
manager = create_manager(action)
|
|
results = manager.run!(action.request, {}, {})
|
|
expect(results.data).to eq("yeah whatever")
|
|
expect(results.second_factor_auth_skipped?).to eq(true)
|
|
|
|
nonce, secure_session = stage_challenge(successful: true)
|
|
request = create_request(request_method: "POST", path: "/abc/xyz")
|
|
action = create_action(request)
|
|
action
|
|
.expects(:second_factor_auth_completed!)
|
|
.with({ call_me_back: 4314 })
|
|
.returns({ eviltrout: "goodbye :(" })
|
|
.once
|
|
manager = create_manager(action)
|
|
results = manager.run!(request, { second_factor_nonce: nonce }, secure_session)
|
|
expect(results.data).to eq({ eviltrout: "goodbye :(" })
|
|
expect(results.second_factor_auth_completed?).to eq(true)
|
|
|
|
user_totp.destroy!
|
|
action = create_action
|
|
action.expects(:no_second_factors_enabled!).with({}).returns("NOTHING WORKS").once
|
|
manager = create_manager(action)
|
|
results = manager.run!(action.request, {}, {})
|
|
expect(results.data).to eq("NOTHING WORKS")
|
|
expect(results.no_second_factors_enabled?).to eq(true)
|
|
end
|
|
end
|
|
end
|
|
end
|