Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/assets/images/2FA-sms.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions app/assets/images/2FA-voice.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ def confirm_two_factor_authenticated
end

def prompt_to_set_up_2fa
redirect_to phone_setup_url
redirect_to two_factor_options_url
end

def prompt_to_enter_otp
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/concerns/unconfirmed_user_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def after_confirmation_url_for(user)
elsif user.two_factor_enabled?
account_url
else
phone_setup_url
two_factor_options_url
end
end

Expand Down
45 changes: 45 additions & 0 deletions app/controllers/users/phone_setup_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
module Users
class PhoneSetupController < ApplicationController
include UserAuthenticator
include PhoneConfirmation

before_action :authenticate_user
before_action :authorize_phone_setup

def index
@user_phone_form = UserPhoneForm.new(current_user)
@presenter = PhoneSetupPresenter.new(current_user.otp_delivery_preference)
analytics.track_event(Analytics::USER_REGISTRATION_PHONE_SETUP_VISIT)
end

def create
@user_phone_form = UserPhoneForm.new(current_user)
@presenter = PhoneSetupPresenter.new(current_user.otp_delivery_preference)
result = @user_phone_form.submit(user_phone_form_params)
analytics.track_event(Analytics::MULTI_FACTOR_AUTH_PHONE_SETUP, result.to_h)

if result.success?
prompt_to_confirm_phone(phone: @user_phone_form.phone)
else
render :index
end
end

private

def authorize_phone_setup
if user_fully_authenticated?
redirect_to account_url
elsif current_user.two_factor_enabled?
redirect_to user_two_factor_authentication_url
end
end

def user_phone_form_params
params.require(:user_phone_form).permit(
:international_code,
:phone
)
end
end
end
3 changes: 2 additions & 1 deletion app/controllers/users/phones_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ class PhonesController < ReauthnRequiredController

def edit
@user_phone_form = UserPhoneForm.new(current_user)
@presenter = PhoneSetupPresenter.new(current_user.otp_delivery_preference)
end

def update
@user_phone_form = UserPhoneForm.new(current_user)

@presenter = PhoneSetupPresenter.new(current_user)
if @user_phone_form.submit(user_params).success?
process_updates
bypass_sign_in current_user
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def show
elsif current_user.two_factor_enabled?
validate_otp_delivery_preference_and_send_code
else
redirect_to phone_setup_url
redirect_to two_factor_options_url
end
end

Expand Down
33 changes: 20 additions & 13 deletions app/controllers/users/two_factor_authentication_setup_controller.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
module Users
class TwoFactorAuthenticationSetupController < ApplicationController
include UserAuthenticator
include PhoneConfirmation

before_action :authorize_otp_setup
before_action :authenticate_user
before_action :authorize_2fa_setup

def index
@user_phone_form = UserPhoneForm.new(current_user)
analytics.track_event(Analytics::USER_REGISTRATION_PHONE_SETUP_VISIT)
@two_factor_options_form = TwoFactorOptionsForm.new(current_user)
analytics.track_event(Analytics::USER_REGISTRATION_2FA_SETUP_VISIT)
end

def set
@user_phone_form = UserPhoneForm.new(current_user)
result = @user_phone_form.submit(params[:user_phone_form])

analytics.track_event(Analytics::MULTI_FACTOR_AUTH_PHONE_SETUP, result.to_h)
def create
@two_factor_options_form = TwoFactorOptionsForm.new(current_user)
result = @two_factor_options_form.submit(two_factor_options_form_params)
analytics.track_event(Analytics::USER_REGISTRATION_2FA_SETUP, result.to_h)

if result.success?
process_valid_form
Expand All @@ -26,16 +24,25 @@ def set

private

def authorize_otp_setup
def authorize_2fa_setup
if user_fully_authenticated?
redirect_to(request.referer || root_url)
elsif current_user&.two_factor_enabled?
redirect_to account_url
elsif current_user.two_factor_enabled?
redirect_to user_two_factor_authentication_url
end
end

def process_valid_form
prompt_to_confirm_phone(phone: @user_phone_form.phone)
case @two_factor_options_form.selection
when 'sms', 'voice'
redirect_to phone_setup_url
when 'auth_app'
redirect_to authenticator_setup_url
end
end

def two_factor_options_form_params
params.require(:two_factor_options_form).permit(:selection)
end
end
end
42 changes: 42 additions & 0 deletions app/forms/two_factor_options_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
class TwoFactorOptionsForm
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any specs covering this class

include ActiveModel::Model

attr_reader :selection

validates :selection, inclusion: { in: %w[voice sms auth_app piv_cac] }

def initialize(user)
self.user = user
end

def submit(params)
self.selection = params[:selection]

success = valid?

update_otp_delivery_preference_for_user if success && user_needs_updating?

FormResponse.new(success: success, errors: errors.messages, extra: extra_analytics_attributes)
end

private

attr_accessor :user
attr_writer :selection

def extra_analytics_attributes
{
selection: selection,
}
end

def user_needs_updating?
return false unless %w[voice sms].include?(selection)
selection != user.otp_delivery_preference
end

def update_otp_delivery_preference_for_user
user_attributes = { otp_delivery_preference: selection }
UpdateUser.new(user: user, attributes: user_attributes).call
end
end
12 changes: 9 additions & 3 deletions app/forms/user_phone_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ class UserPhoneForm
include FormPhoneValidator
include OtpDeliveryPreferenceValidator

validates :otp_delivery_preference, inclusion: { in: %w[voice sms] }

attr_accessor :phone, :international_code, :otp_delivery_preference

def initialize(user)
Expand All @@ -16,9 +18,10 @@ def submit(params)
ingest_submitted_params(params)

success = valid?

self.phone = submitted_phone unless success
update_otp_delivery_preference_for_user if otp_delivery_preference_changed? && success

update_otp_delivery_preference_for_user if
success && otp_delivery_preference.present? && otp_delivery_preference_changed?

FormResponse.new(success: success, errors: errors.messages, extra: extra_analytics_attributes)
end
Expand All @@ -44,7 +47,10 @@ def ingest_submitted_params(params)
submitted_phone,
country_code: international_code
)
self.otp_delivery_preference = params[:otp_delivery_preference]

tfa_prefs = params[:otp_delivery_preference]

self.otp_delivery_preference = tfa_prefs if tfa_prefs
end

def otp_delivery_preference_changed?
Expand Down
2 changes: 0 additions & 2 deletions app/javascript/app/form-field-format.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { SocialSecurityNumberFormatter, TextField } from 'field-kit';
import DateFormatter from './modules/date-formatter';
import InternationalPhoneFormatter from './modules/international-phone-formatter';
import NumericFormatter from './modules/numeric-formatter';
import PersonalKeyFormatter from './modules/personal-key-formatter';
import USPhoneFormatter from './modules/us-phone-formatter';
Expand All @@ -11,7 +10,6 @@ function formatForm() {
const formats = [
['.dob', new DateFormatter()],
['.mfa', new NumericFormatter()],
['.phone', new InternationalPhoneFormatter()],
['.us-phone', new USPhoneFormatter()],
['.personal-key', new PersonalKeyFormatter()],
['.ssn', new SocialSecurityNumberFormatter()],
Expand Down
77 changes: 0 additions & 77 deletions app/javascript/app/modules/international-phone-formatter.js

This file was deleted.

6 changes: 3 additions & 3 deletions app/javascript/app/phone-internationalization.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const updateOTPDeliveryMethods = () => {
return;
}

const phoneInput = document.querySelector('[data-international-phone-form] .phone') || document.querySelector('[data-international-phone-form] .new-phone');
const phoneInput = document.querySelector('[data-international-phone-form] .phone');
const phoneLabel = phoneRadio.parentNode.parentNode;
const deliveryMethodHint = document.querySelector('#otp_delivery_preference_instruction');
const optPhoneLabelInfo = document.querySelector('#otp_phone_label_info');
Expand Down Expand Up @@ -111,7 +111,7 @@ const updateInternationalCodeInPhone = (phone, newCode) => {
};

const updateInternationalCodeInput = () => {
const phoneInput = document.querySelector('[data-international-phone-form] .phone') || document.querySelector('[data-international-phone-form] .new-phone');
const phoneInput = document.querySelector('[data-international-phone-form] .phone');
const phone = phoneInput.value;
const inputInternationalCode = internationalCodeFromPhone(phone);
const selectedInternationalCode = selectedInternationCodeOption().dataset.countryCode;
Expand All @@ -122,7 +122,7 @@ const updateInternationalCodeInput = () => {
};

document.addEventListener('DOMContentLoaded', () => {
const phoneInput = document.querySelector('[data-international-phone-form] .phone') || document.querySelector('[data-international-phone-form] .new-phone');
const phoneInput = document.querySelector('[data-international-phone-form] .phone');
const codeInput = document.querySelector('[data-international-phone-form] .international-code');
if (phoneInput) {
phoneInput.addEventListener('countryChange', updateOTPDeliveryMethods);
Expand Down
25 changes: 25 additions & 0 deletions app/presenters/phone_setup_presenter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class PhoneSetupPresenter
include ActionView::Helpers::TranslationHelper

attr_reader :otp_delivery_preference

def initialize(otp_delivery_preference)
@otp_delivery_preference = otp_delivery_preference
end

def heading
t("titles.phone_setup.#{otp_delivery_preference}")
end

def label
t("devise.two_factor_authentication.phone_#{otp_delivery_preference}_label")
end

def info
t("devise.two_factor_authentication.phone_#{otp_delivery_preference}_info_html")
end

def image
"2FA-#{otp_delivery_preference}.svg"
end
end
2 changes: 2 additions & 0 deletions app/services/analytics.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ def browser
USER_REGISTRATION_EMAIL_CONFIRMATION_RESEND = 'User Registration: Email Confirmation requested due to invalid token'.freeze
USER_REGISTRATION_ENTER_EMAIL_VISIT = 'User Registration: enter email visited'.freeze
USER_REGISTRATION_INTRO_VISIT = 'User Registration: intro visited'.freeze
USER_REGISTRATION_2FA_SETUP = 'User Registration: 2FA Setup'.freeze
USER_REGISTRATION_2FA_SETUP_VISIT = 'User Registration: 2FA Setup visited'.freeze
USER_REGISTRATION_PHONE_SETUP_VISIT = 'User Registration: phone setup visited'.freeze
USER_REGISTRATION_PERSONAL_KEY_VISIT = 'User Registration: personal key visited'.freeze
USER_REGISTRATION_PIV_CAC_DISABLED = 'User Registration: piv cac disabled'.freeze
Expand Down
Loading