app/controllers/two_factor_authentication/otp_verification_controller.rb
# frozen_string_literal: true
module TwoFactorAuthentication
class OtpVerificationController < ApplicationController
include TwoFactorAuthenticatable
include MfaSetupConcern
include NewDeviceConcern
before_action :check_sp_required_mfa
before_action :confirm_multiple_factors_enabled
before_action :redirect_if_blank_phone, only: [:show]
before_action :confirm_voice_capability, only: [:show]
helper_method :in_multi_mfa_selection_flow?
def show
analytics.multi_factor_auth_enter_otp_visit(**analytics_properties)
@landline_alert = landline_warning?
@presenter = presenter_for_two_factor_authentication_method
end
def create
result = otp_verification_form.submit
post_analytics(result)
if result.success?
handle_remember_device_preference(params[:remember_device])
if UserSessionContext.confirmation_context?(context)
handle_valid_confirmation_otp
else
handle_valid_verification_for_authentication_context(
auth_method: params[:otp_delivery_preference],
)
redirect_to after_sign_in_path_for(current_user)
end
reset_otp_session_data
else
handle_invalid_otp(context: context, type: 'otp')
end
end
private
def handle_valid_confirmation_otp
assign_phone
track_mfa_added
handle_valid_verification_for_confirmation_context(
auth_method: params[:otp_delivery_preference],
)
flash[:success] = t('notices.phone_confirmed')
redirect_to next_setup_path || after_mfa_setup_path
end
def otp_verification_form
OtpVerificationForm.new(current_user, sanitized_otp_code, phone_configuration)
end
def redirect_if_blank_phone
return if phone.present?
flash[:error] = t('errors.messages.phone_required')
redirect_to new_user_session_path
end
def track_mfa_added
analytics.multi_factor_auth_added_phone(
enabled_mfa_methods_count: MfaContext.new(current_user).enabled_mfa_methods_count,
in_account_creation_flow: user_session[:in_account_creation_flow] || false,
recaptcha_annotation: RecaptchaAnnotator.annotate(
assessment_id: user_session.delete(:phone_recaptcha_assessment_id),
reason: RecaptchaAnnotator::AnnotationReasons::PASSED_TWO_FACTOR,
),
)
Funnel::Registration::AddMfa.call(current_user.id, 'phone', analytics)
end
def confirm_multiple_factors_enabled
return if UserSessionContext.confirmation_context?(context)
phone_enabled = phone_enabled?
return if phone_enabled
if MfaPolicy.new(current_user).two_factor_enabled? &&
!phone_enabled && user_signed_in?
return redirect_to user_two_factor_authentication_url
end
redirect_to phone_setup_url
end
def phone_enabled?
TwoFactorAuthentication::PhonePolicy.new(current_user).enabled?
end
def landline_warning?
user_session[:phone_type] == 'landline' && params[:otp_delivery_preference] == 'sms'
end
def confirm_voice_capability
return if params[:otp_delivery_preference] == 'sms'
phone_is_confirmed = UserSessionContext.authentication_or_reauthentication_context?(context)
capabilities = PhoneNumberCapabilities.new(phone, phone_confirmed: phone_is_confirmed)
return if capabilities.supports_voice?
flash[:error] = t(
'two_factor_authentication.otp_delivery_preference.voice_unsupported',
location: capabilities.unsupported_location,
)
redirect_to login_two_factor_url(otp_delivery_preference: 'sms')
end
def phone
phone_configuration&.phone ||
user_session[:unconfirmed_phone]
end
def phone_configuration
return @phone_configuration if defined?(@phone_configuration)
@phone_configuration =
MfaContext.new(current_user).phone_configuration(user_session[:phone_id])
end
def sanitized_otp_code
form_params[:code].to_s.strip.sub(/^#/, '')
end
def form_params
params.permit(:code)
end
def post_analytics(result)
properties = result.to_h.merge(analytics_properties, new_device: new_device?)
analytics.multi_factor_auth_setup(**properties) if context == 'confirmation'
analytics.multi_factor_auth(**properties)
end
def analytics_properties
parsed_phone = Phonelib.parse(phone)
{
context: context,
multi_factor_auth_method: params[:otp_delivery_preference],
confirmation_for_add_phone: confirmation_for_add_phone?,
area_code: parsed_phone.area_code,
country_code: parsed_phone.country,
phone_fingerprint: Pii::Fingerprinter.fingerprint(parsed_phone.e164),
phone_configuration_id: phone_configuration&.id,
in_account_creation_flow: user_session[:in_account_creation_flow] || false,
enabled_mfa_methods_count: mfa_context.enabled_mfa_methods_count,
}
end
def presenter_for_two_factor_authentication_method
TwoFactorAuthCode::PhoneDeliveryPresenter.new(
data: phone_view_data,
view: view_context,
service_provider: current_sp,
remember_device_default: remember_device_default,
)
end
def phone_view_data
{
confirmation_for_add_phone: confirmation_for_add_phone?,
phone_number: display_phone_to_deliver_to,
code_value: direct_otp_code,
in_multi_mfa_selection_flow: in_multi_mfa_selection_flow?,
otp_expiration: otp_expiration,
otp_delivery_preference: params[:otp_delivery_preference],
otp_make_default_number: selected_otp_make_default_number,
unconfirmed_phone: unconfirmed_phone?,
}.merge(generic_data)
end
def display_phone_to_deliver_to
if UserSessionContext.authentication_or_reauthentication_context?(context)
phone_configuration.masked_phone
else
user_session[:unconfirmed_phone]
end
end
def unconfirmed_phone?
user_session[:unconfirmed_phone] && UserSessionContext.confirmation_context?(context)
end
def confirmation_for_add_phone?
UserSessionContext.confirmation_context?(context) && user_fully_authenticated?
end
def check_sp_required_mfa
check_sp_required_mfa_bypass(auth_method: params[:otp_delivery_preference])
end
def assign_phone
if updating_existing_number?
phone_changed
else
phone_confirmed
end
update_phone_attributes
end
def updating_existing_number?
user_session[:phone_id].present?
end
def update_phone_attributes
UpdateUserPhoneConfiguration.update!(
user: current_user,
attributes: { phone_id: user_session[:phone_id],
phone: user_session[:unconfirmed_phone],
phone_confirmed_at: Time.zone.now,
otp_make_default_number: selected_otp_make_default_number },
)
end
def phone_changed
create_user_event(:phone_changed)
send_phone_added_email
end
def phone_confirmed
create_user_event(:phone_confirmed)
# If the user has MFA configured, then they are not adding a phone during sign up and are
# instead adding it outside the sign up flow
return unless MfaPolicy.new(current_user).two_factor_enabled?
send_phone_added_email
end
def send_phone_added_email
_event, disavowal_token = create_user_event_with_disavowal(:phone_added, current_user)
current_user.confirmed_email_addresses.each do |email_address|
UserMailer.with(user: current_user, email_address: email_address).
phone_added(disavowal_token: disavowal_token).deliver_now_or_later
end
end
def selected_otp_make_default_number
params&.dig(:otp_make_default_number)
end
def direct_otp_code
current_user.direct_otp if FeatureManagement.prefill_otp_codes?
end
def reset_otp_session_data
user_session.delete(:unconfirmed_phone)
user_session[:context] = 'authentication'
end
end
end