DEV: Allow run_second_factor! to be used before login (#25420)

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.
This commit is contained in:
David Taylor
2024-01-29 12:28:47 +00:00
committed by GitHub
parent 8e32c11ab4
commit 1bfccdd4f2
8 changed files with 259 additions and 152 deletions

View File

@ -11,8 +11,6 @@ class SessionController < ApplicationController
skip_before_action :check_xhr, only: %i[second_factor_auth_show]
requires_login only: %i[second_factor_auth_show second_factor_auth_perform]
allow_in_staff_writes_only_mode :create
allow_in_staff_writes_only_mode :email_login
@ -47,8 +45,10 @@ class SessionController < ApplicationController
result =
run_second_factor!(
SecondFactor::Actions::DiscourseConnectProvider,
payload: payload,
confirmed_2fa_during_login: confirmed_2fa_during_login,
action_data: {
payload: payload,
confirmed_2fa_during_login: confirmed_2fa_during_login,
},
)
if result.second_factor_auth_skipped?
@ -136,8 +136,10 @@ class SessionController < ApplicationController
skip_before_action :check_xhr, only: :test_second_factor_restricted_route
def test_second_factor_restricted_route
target_user = User.find_by_username(params[:username]) || current_user
raise "user required" if !target_user
result =
run_second_factor!(TestSecondFactorAction) do |manager|
run_second_factor!(TestSecondFactorAction, target_user: target_user) do |manager|
manager.allow_backup_codes! if params[:allow_backup_codes]
end
if result.no_second_factors_enabled?
@ -145,6 +147,15 @@ class SessionController < ApplicationController
else
render json: { result: "second_factor_auth_completed" }
end
rescue StandardError => e
# Normally this would be checked by the consumer before calling `run_second_factor!`
# but since this is a test route, we allow passing a bad value into the API, catch the error
# and return a JSON response to assert against.
if e.message == "running 2fa against another user is not allowed"
render json: { result: "wrong user" }, status: 400
else
raise e
end
end
end
@ -464,14 +475,18 @@ class SessionController < ApplicationController
end
def second_factor_auth_show
user = current_user
nonce = params.require(:nonce)
challenge = nil
error_key = nil
user = nil
status_code = 200
begin
challenge = SecondFactor::AuthManager.find_second_factor_challenge(nonce, secure_session)
challenge =
SecondFactor::AuthManager.find_second_factor_challenge(
nonce: nonce,
secure_session: secure_session,
target_user: current_user,
)
rescue SecondFactor::BadChallenge => exception
error_key = exception.error_translation_key
status_code = exception.status_code
@ -479,6 +494,7 @@ class SessionController < ApplicationController
json = {}
if challenge
user = User.find(challenge[:target_user_id])
json.merge!(
totp_enabled: user.totp_enabled?,
backup_enabled: user.backup_codes_enabled?,
@ -510,9 +526,16 @@ class SessionController < ApplicationController
nonce = params.require(:nonce)
challenge = nil
error_key = nil
user = nil
status_code = 200
begin
challenge = SecondFactor::AuthManager.find_second_factor_challenge(nonce, secure_session)
challenge =
SecondFactor::AuthManager.find_second_factor_challenge(
nonce: nonce,
secure_session: secure_session,
target_user: current_user,
)
user = User.find(challenge[:target_user_id])
rescue SecondFactor::BadChallenge => exception
error_key = exception.error_translation_key
status_code = exception.status_code
@ -535,7 +558,7 @@ class SessionController < ApplicationController
# they're redirected to the 2fa page and then uses the same method they've
# disabled.
second_factor_method = params[:second_factor_method].to_i
if !current_user.valid_second_factor_method_for_user?(second_factor_method)
if !user.valid_second_factor_method_for_user?(second_factor_method)
raise Discourse::InvalidAccess.new
end
# and this happens if someone tries to use a 2FA method that's not accepted
@ -546,8 +569,8 @@ class SessionController < ApplicationController
end
if !challenge[:successful]
rate_limit_second_factor!(current_user)
second_factor_auth_result = current_user.authenticate_second_factor(params, secure_session)
rate_limit_second_factor!(user)
second_factor_auth_result = user.authenticate_second_factor(params, secure_session)
if second_factor_auth_result.ok
challenge[:successful] = true
challenge[:generated_at] += 1.minute.to_i