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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ lint_analytics_events: .yardoc ## Checks that all methods on AnalyticsEvents are

lint_analytics_events_sorted:
@test "$(shell grep '^ def ' app/services/analytics_events.rb)" = "$(shell grep '^ def ' app/services/analytics_events.rb | sort)" \
|| (echo 'Error: methods in analytics_events.rb are not sorted alphabetically' && exit 1)
|| (echo '\033[1;31mError: methods in analytics_events.rb are not sorted alphabetically\033[0m' && exit 1)

lint_tracker_events: .yardoc ## Checks that all methods on AnalyticsEvents are documented
bundle exec ruby lib/analytics_events_documenter.rb --class-name="IrsAttemptsApi::TrackerEvents" --check --skip-extra-params $<
Expand Down
25 changes: 25 additions & 0 deletions app/components/manageable_authenticator_component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,28 @@
.manageable-authenticator__actions {
@include grid-col('auto');
}

.manageable-authenticator__done-button,
.manageable-authenticator__rename-button,
.manageable-authenticator__delete-button .usa-button,
.manageable-authenticator__save-rename-button .usa-button,
.manageable-authenticator__cancel-rename-button {
@include at-media-max('tablet') {
@include button-unstyled;
width: auto;
}
}

.manageable-authenticator__rename-button,
.manageable-authenticator__save-rename-button {
@include at-media-max('tablet') {
margin-right: 1rem;
}
}

.manageable-authenticator__delete-button
.usa-button.usa-button--danger.usa-button--outline:not(:disabled, [aria-disabled='true']) {
@include at-media-max('tablet') {
box-shadow: none;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
module Api
module Internal
module TwoFactorAuthentication
class PivCacController < ApplicationController
include CsrfTokenConcern
include ReauthenticationRequiredConcern

before_action :render_unauthorized, unless: :recently_authenticated_2fa?

after_action :add_csrf_token_header_to_response

respond_to :json

def update
result = ::TwoFactorAuthentication::PivCacUpdateForm.new(
user: current_user,
configuration_id: params[:id],
).submit(name: params[:name])

analytics.piv_cac_update_name_submitted(**result.to_h)

if result.success?
render json: { success: true }
else
render json: { success: false, error: result.first_error_message }, status: :bad_request
end
end

def destroy
result = ::TwoFactorAuthentication::PivCacDeleteForm.new(
user: current_user,
configuration_id: params[:id],
).submit

analytics.piv_cac_delete_submitted(**result.to_h)

if result.success?
create_user_event(:piv_cac_disabled)
revoke_remember_device(current_user)
deliver_push_notification
render json: { success: true }
else
render json: { success: false, error: result.first_error_message }, status: :bad_request
end
end

private

def deliver_push_notification
event = PushNotification::RecoveryInformationChangedEvent.new(user: current_user)
PushNotification::HttpPush.deliver(event)
end

def render_unauthorized
render json: { error: 'Unauthorized' }, status: :unauthorized
end
end
end
end
end
2 changes: 0 additions & 2 deletions app/controllers/frontend_log_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ class FrontendLogController < ApplicationController
'IdV: warning action triggered' => :idv_warning_action_triggered,
'IdV: warning shown' => :idv_warning_shown,
'Multi-Factor Authentication: download backup code' => :multi_factor_auth_backup_code_download,
'User prompted before navigation' => :user_prompted_before_navigation,
'User prompted before navigation and still on page' => :user_prompted_before_navigation_and_still_on_page,
}.freeze
# rubocop:enable Layout/LineLength

Expand Down
3 changes: 1 addition & 2 deletions app/controllers/idv/in_person/address_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ def update
attrs.each do |attr|
pii_from_user[attr] = flow_params[attr]
end
flow_session['Idv::Steps::InPerson::AddressStep'] = true
redirect_to_next_page
else
render :show, locals: extra_view_variables
Expand Down Expand Up @@ -112,7 +111,7 @@ def confirm_in_person_state_id_step_complete
end

def confirm_in_person_address_step_needed
return if pii_from_user && pii_from_user[:same_address_as_id] == 'false' &&
return if pii_from_user&.dig(:same_address_as_id) == 'false' &&
!pii_from_user.has_key?(:address1)
return if request.referer == idv_in_person_verify_info_url
redirect_to idv_in_person_ssn_url
Expand Down
6 changes: 1 addition & 5 deletions app/controllers/idv/in_person/ssn_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,7 @@ def analytics_arguments

def confirm_in_person_address_step_complete
return if flow_session[:pii_from_user] && flow_session[:pii_from_user][:address1].present?
if IdentityConfig.store.in_person_residential_address_controller_enabled
redirect_to idv_in_person_proofing_address_url
else
redirect_to idv_in_person_step_url(step: :address)
end
redirect_to idv_in_person_proofing_address_url
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/idv/in_person_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class InPersonController < ApplicationController

FLOW_STATE_MACHINE_SETTINGS = {
step_url: :idv_in_person_step_url,
final_url: :idv_in_person_ssn_url,
final_url: :idv_in_person_proofing_address_url,
flow: Idv::Flows::InPersonFlow,
analytics_id: 'In Person Proofing',
}.freeze
Expand Down
76 changes: 76 additions & 0 deletions app/controllers/users/piv_cac_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
module Users
class PivCacController < ApplicationController
include ReauthenticationRequiredConcern

before_action :confirm_two_factor_authenticated
before_action :confirm_recently_authenticated_2fa
before_action :set_form
before_action :validate_configuration_exists
before_action :set_presenter

def edit; end

def update
result = form.submit(name: params.dig(:form, :name))

analytics.piv_cac_update_name_submitted(**result.to_h)

if result.success?
flash[:success] = presenter.rename_success_alert_text
redirect_to account_path
else
flash.now[:error] = result.first_error_message
render :edit
end
end

def destroy
result = form.submit

analytics.piv_cac_delete_submitted(**result.to_h)

if result.success?
create_user_event(:piv_cac_disabled)
revoke_remember_device(current_user)
deliver_push_notification

flash[:success] = presenter.delete_success_alert_text
redirect_to account_path
else
flash[:error] = result.first_error_message
redirect_to edit_piv_cac_path(id: params[:id])
end
end

private

def deliver_push_notification
event = PushNotification::RecoveryInformationChangedEvent.new(user: current_user)
PushNotification::HttpPush.deliver(event)
end

def form
@form ||= form_class.new(user: current_user, configuration_id: params[:id])
end

def presenter
@presenter ||= TwoFactorAuthentication::PivCacEditPresenter.new
end

alias_method :set_form, :form
alias_method :set_presenter, :presenter

def form_class
case action_name
when 'edit', 'update'
TwoFactorAuthentication::PivCacUpdateForm
when 'destroy'
TwoFactorAuthentication::PivCacDeleteForm
end
end

def validate_configuration_exists
render_not_found if form.configuration.blank?
end
end
end
6 changes: 4 additions & 2 deletions app/forms/idv/api_image_upload_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module Idv
class ApiImageUploadForm
include ActiveModel::Model
include ActionView::Helpers::TranslationHelper
include ApplicationHelper

validates_presence_of :front
validates_presence_of :back
Expand Down Expand Up @@ -154,6 +155,7 @@ def extra_attributes

@extra_attributes[:front_image_fingerprint] = front_image_fingerprint
@extra_attributes[:back_image_fingerprint] = back_image_fingerprint
@extra_attributes[:liveness_checking_required] = liveness_checking_required
@extra_attributes
end

Expand Down Expand Up @@ -458,14 +460,14 @@ def store_failed_images(client_response, doc_pii_response)
front_image_fingerprint: failed_front_fingerprint,
back_image_fingerprint: failed_back_fingerprint,
doc_auth_success: client_response.doc_auth_success?,
selfie_success: client_response.selfie_success,
selfie_status: selfie_status_from_response(client_response),
)
elsif doc_pii_response && !doc_pii_response.success?
document_capture_session.store_failed_auth_data(
front_image_fingerprint: extra_attributes[:front_image_fingerprint],
back_image_fingerprint: extra_attributes[:back_image_fingerprint],
doc_auth_success: client_response.doc_auth_success?,
selfie_success: client_response.selfie_success,
selfie_status: selfie_status_from_response(client_response),
)
end
# retrieve updated data from session
Expand Down
57 changes: 57 additions & 0 deletions app/forms/two_factor_authentication/piv_cac_delete_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
module TwoFactorAuthentication
class PivCacDeleteForm
include ActiveModel::Model
include ActionView::Helpers::TranslationHelper

attr_reader :user, :configuration_id

validate :validate_configuration_exists
validate :validate_has_multiple_mfa

def initialize(user:, configuration_id:)
@user = user
@configuration_id = configuration_id
end

def submit
success = valid?

configuration.destroy if success

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

def configuration
@configuration ||= user.piv_cac_configurations.find_by(id: configuration_id)
end

private

def validate_configuration_exists
return if configuration.present?
errors.add(
:configuration_id,
:configuration_not_found,
message: t('errors.manage_authenticator.internal_error'),
)
end

def validate_has_multiple_mfa
return if !configuration || MfaPolicy.new(user).multiple_factors_enabled?
errors.add(
:configuration_id,
:only_method,
message: t('errors.manage_authenticator.remove_only_method_error'),
)
end

def extra_analytics_attributes
{ configuration_id: configuration_id }
end
end
end
70 changes: 70 additions & 0 deletions app/forms/two_factor_authentication/piv_cac_update_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
module TwoFactorAuthentication
class PivCacUpdateForm
include ActiveModel::Model
include ActionView::Helpers::TranslationHelper

attr_reader :user, :configuration_id

validate :validate_configuration_exists
validate :validate_unique_name

def initialize(user:, configuration_id:)
@user = user
@configuration_id = configuration_id
end

def submit(name:)
@name = name

success = valid?
if valid?
configuration.name = name
success = configuration.valid?
errors.merge!(configuration.errors)
configuration.save if success
end

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

def name
return @name if defined?(@name)
@name = configuration&.name
end

def configuration
@configuration ||= user.piv_cac_configurations.find_by(id: configuration_id)
end

private

def validate_configuration_exists
return if configuration.present?

errors.add(
:configuration_id,
:configuration_not_found,
message: t('errors.manage_authenticator.internal_error'),
)
end

def validate_unique_name
return unless user.piv_cac_configurations.where.not(id: configuration_id).find_by(name:)

errors.add(
:name,
:duplicate,
message: t('errors.manage_authenticator.unique_name_error'),
)
end

def extra_analytics_attributes
{ configuration_id: configuration_id }
end
end
end
6 changes: 6 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,10 @@ def cancel_link_text
def desktop_device?
!BrowserCache.parse(request.user_agent).mobile?
end

def selfie_status_from_response(client_response)
return client_response.selfie_status if client_response.respond_to?(:selfie_status)

:not_processed
end
end
Loading