diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e5e0acd3c05..1f4413fa60a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,8 +1,15 @@ version: 2 updates: - - package-ecosystem: 'npm' - directory: '/' + - package-ecosystem: npm + directory: / schedule: - interval: 'daily' + interval: daily allow: - dependency-name: '@18f/identity-design-system' + - dependency-name: libphonenumber-js + - package-ecosystem: bundler + directory: / + schedule: + interval: daily + allow: + - dependency-name: phonelib diff --git a/Gemfile.lock b/Gemfile.lock index a5314280bef..cc016fe2adb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -60,70 +60,70 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.0.7) - actionpack (= 7.0.7) - activesupport (= 7.0.7) + actioncable (7.0.7.2) + actionpack (= 7.0.7.2) + activesupport (= 7.0.7.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.7) - actionpack (= 7.0.7) - activejob (= 7.0.7) - activerecord (= 7.0.7) - activestorage (= 7.0.7) - activesupport (= 7.0.7) + actionmailbox (7.0.7.2) + actionpack (= 7.0.7.2) + activejob (= 7.0.7.2) + activerecord (= 7.0.7.2) + activestorage (= 7.0.7.2) + activesupport (= 7.0.7.2) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.7) - actionpack (= 7.0.7) - actionview (= 7.0.7) - activejob (= 7.0.7) - activesupport (= 7.0.7) + actionmailer (7.0.7.2) + actionpack (= 7.0.7.2) + actionview (= 7.0.7.2) + activejob (= 7.0.7.2) + activesupport (= 7.0.7.2) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.7) - actionview (= 7.0.7) - activesupport (= 7.0.7) + actionpack (7.0.7.2) + actionview (= 7.0.7.2) + activesupport (= 7.0.7.2) rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.7) - actionpack (= 7.0.7) - activerecord (= 7.0.7) - activestorage (= 7.0.7) - activesupport (= 7.0.7) + actiontext (7.0.7.2) + actionpack (= 7.0.7.2) + activerecord (= 7.0.7.2) + activestorage (= 7.0.7.2) + activesupport (= 7.0.7.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.7) - activesupport (= 7.0.7) + actionview (7.0.7.2) + activesupport (= 7.0.7.2) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (7.0.7) - activesupport (= 7.0.7) + activejob (7.0.7.2) + activesupport (= 7.0.7.2) globalid (>= 0.3.6) - activemodel (7.0.7) - activesupport (= 7.0.7) - activerecord (7.0.7) - activemodel (= 7.0.7) - activesupport (= 7.0.7) + activemodel (7.0.7.2) + activesupport (= 7.0.7.2) + activerecord (7.0.7.2) + activemodel (= 7.0.7.2) + activesupport (= 7.0.7.2) activerecord-postgis-adapter (8.0.2) activerecord (~> 7.0.0) rgeo-activerecord (~> 7.0.0) - activestorage (7.0.7) - actionpack (= 7.0.7) - activejob (= 7.0.7) - activerecord (= 7.0.7) - activesupport (= 7.0.7) + activestorage (7.0.7.2) + actionpack (= 7.0.7.2) + activejob (= 7.0.7.2) + activerecord (= 7.0.7.2) + activesupport (= 7.0.7.2) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.7) + activesupport (7.0.7.2) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -443,7 +443,7 @@ GEM pg (1.5.3) pg_query (4.2.3) google-protobuf (>= 3.22.3) - phonelib (0.8.2) + phonelib (0.8.3) pkcs11 (0.3.4) premailer (1.21.0) addressable @@ -492,20 +492,20 @@ GEM rack_session_access (0.2.0) builder (>= 2.0.0) rack (>= 1.0.0) - rails (7.0.7) - actioncable (= 7.0.7) - actionmailbox (= 7.0.7) - actionmailer (= 7.0.7) - actionpack (= 7.0.7) - actiontext (= 7.0.7) - actionview (= 7.0.7) - activejob (= 7.0.7) - activemodel (= 7.0.7) - activerecord (= 7.0.7) - activestorage (= 7.0.7) - activesupport (= 7.0.7) + rails (7.0.7.2) + actioncable (= 7.0.7.2) + actionmailbox (= 7.0.7.2) + actionmailer (= 7.0.7.2) + actionpack (= 7.0.7.2) + actiontext (= 7.0.7.2) + actionview (= 7.0.7.2) + activejob (= 7.0.7.2) + activemodel (= 7.0.7.2) + activerecord (= 7.0.7.2) + activestorage (= 7.0.7.2) + activesupport (= 7.0.7.2) bundler (>= 1.15.0) - railties (= 7.0.7) + railties (= 7.0.7.2) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -520,9 +520,9 @@ GEM rails-i18n (7.0.6) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) - railties (7.0.7) - actionpack (= 7.0.7) - activesupport (= 7.0.7) + railties (7.0.7.2) + actionpack (= 7.0.7.2) + activesupport (= 7.0.7.2) method_source rake (>= 12.2) thor (~> 1.0) diff --git a/app/components/page_footer_component.rb b/app/components/page_footer_component.rb index 3c05b57e5f5..9cb063b3aaf 100644 --- a/app/components/page_footer_component.rb +++ b/app/components/page_footer_component.rb @@ -10,6 +10,6 @@ def call end def css_class - ['margin-top-4 padding-top-2 border-top border-primary-light', *tag_options[:class]] + ['page-footer margin-top-4 padding-top-2 border-top border-primary-light', *tag_options[:class]] end end diff --git a/app/controllers/concerns/forced_reauthentication_concern.rb b/app/controllers/concerns/forced_reauthentication_concern.rb new file mode 100644 index 00000000000..470d16de6c2 --- /dev/null +++ b/app/controllers/concerns/forced_reauthentication_concern.rb @@ -0,0 +1,19 @@ +# This module defines an interface for storing when an issuer has forced re-authentication +# for an active session. A request to force re-authentication that does not result +# in the user needing to re-authenticate due to not being authenticated should be excluded. + +module ForcedReauthenticationConcern + def issuer_forced_reauthentication?(issuer:) + session.dig(:forced_reauthentication_sps, issuer) == true + end + + def set_issuer_forced_reauthentication(issuer:, is_forced_reauthentication:) + if is_forced_reauthentication + session[:forced_reauthentication_sps] ||= {} + session[:forced_reauthentication_sps][issuer] = true + elsif session[:forced_reauthentication_sps] + session[:forced_reauthentication_sps].delete(issuer) + session.delete(:forced_reauthentication_sps) if session[:forced_reauthentication_sps].blank? + end + end +end diff --git a/app/controllers/concerns/idv/ab_test_analytics_concern.rb b/app/controllers/concerns/idv/ab_test_analytics_concern.rb index 874523a3503..aad080e0494 100644 --- a/app/controllers/concerns/idv/ab_test_analytics_concern.rb +++ b/app/controllers/concerns/idv/ab_test_analytics_concern.rb @@ -4,7 +4,12 @@ module AbTestAnalyticsConcern include Idv::GettingStartedAbTestConcern def ab_test_analytics_buckets - acuant_sdk_ab_test_analytics_args. + buckets = {} + if defined?(idv_session) + buckets[:skip_hybrid_handoff] = idv_session&.skip_hybrid_handoff + end + + buckets.merge(acuant_sdk_ab_test_analytics_args). merge(getting_started_ab_test_analytics_bucket) end end diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb index 38593738669..be6c37b50e2 100644 --- a/app/controllers/concerns/idv/verify_info_concern.rb +++ b/app/controllers/concerns/idv/verify_info_concern.rb @@ -30,8 +30,7 @@ def shared_update should_proof_state_id: should_use_aamva?(pii), trace_id: amzn_trace_id, user_id: current_user.id, - threatmetrix_session_id: - idv_session.threatmetrix_session_id || flow_session[:threatmetrix_session_id], + threatmetrix_session_id: idv_session.threatmetrix_session_id, request_ip: request.remote_ip, double_address_verification: capture_secondary_id_enabled, ) @@ -195,8 +194,9 @@ def async_state_done(current_async_state) address_edited: !!(idv_session.address_edited || flow_session['address_edited']), address_line2_present: !pii[:address2].blank?, pii_like_keypaths: [[:errors, :ssn], [:response_body, :first_name], + [:same_address_as_id], [:state_id, :state_id_jurisdiction]], - }.merge(ab_test_analytics_buckets), + }, ) log_idv_verification_submitted_event( success: form_response.success?, @@ -219,7 +219,7 @@ def async_state_done(current_async_state) idv_session.invalidate_verify_info_step! end - analytics.idv_doc_auth_verify_proofing_results(**form_response.to_h) + analytics.idv_doc_auth_verify_proofing_results(**analytics_arguments, **form_response.to_h) end def next_step_url diff --git a/app/controllers/concerns/saml_idp_auth_concern.rb b/app/controllers/concerns/saml_idp_auth_concern.rb index a2db3a10958..c1b426680f1 100644 --- a/app/controllers/concerns/saml_idp_auth_concern.rb +++ b/app/controllers/concerns/saml_idp_auth_concern.rb @@ -1,6 +1,7 @@ module SamlIdpAuthConcern extend ActiveSupport::Concern extend Forwardable + include ForcedReauthenticationConcern included do # rubocop:disable Rails/LexicallyScopedActionFilter @@ -19,9 +20,22 @@ module SamlIdpAuthConcern private def sign_out_if_forceauthn_is_true_and_user_is_signed_in + if !saml_request.force_authn? + set_issuer_forced_reauthentication( + issuer: saml_request_service_provider.issuer, + is_forced_reauthentication: false, + ) + end + return unless user_signed_in? && saml_request.force_authn? - sign_out unless sp_session[:final_auth_request] + if !sp_session[:final_auth_request] + sign_out + set_issuer_forced_reauthentication( + issuer: saml_request_service_provider.issuer, + is_forced_reauthentication: true, + ) + end sp_session[:final_auth_request] = false end diff --git a/app/controllers/frontend_log_controller.rb b/app/controllers/frontend_log_controller.rb index a2708f4810c..813663c3272 100644 --- a/app/controllers/frontend_log_controller.rb +++ b/app/controllers/frontend_log_controller.rb @@ -28,8 +28,8 @@ class FrontendLogController < ApplicationController 'Multi-Factor Authentication: download backup code' => :multi_factor_auth_backup_code_download, 'Show Password button clicked' => :show_password_button_clicked, 'Sign In: IdV requirements accordion clicked' => :sign_in_idv_requirements_accordion_clicked, - 'User prompted before navigation and still on page' => :user_prompted_before_navigation_and_still_on_page, 'User prompted before navigation' => :user_prompted_before_navigation, + 'User prompted before navigation and still on page' => :user_prompted_before_navigation_and_still_on_page, }.transform_values { |method| AnalyticsEvents.instance_method(method) }.freeze # rubocop:enable Layout/LineLength diff --git a/app/controllers/idv/agreement_controller.rb b/app/controllers/idv/agreement_controller.rb index 9248bcb6789..c244177a8f6 100644 --- a/app/controllers/idv/agreement_controller.rb +++ b/app/controllers/idv/agreement_controller.rb @@ -41,6 +41,7 @@ def analytics_arguments { step: 'agreement', analytics_id: 'Doc Auth', + skip_hybrid_handoff: idv_session.skip_hybrid_handoff, irs_reproofing: irs_reproofing?, }.merge(ab_test_analytics_buckets) end diff --git a/app/controllers/idv/document_capture_controller.rb b/app/controllers/idv/document_capture_controller.rb index 8db81b04648..b7ae2db42da 100644 --- a/app/controllers/idv/document_capture_controller.rb +++ b/app/controllers/idv/document_capture_controller.rb @@ -75,7 +75,8 @@ def analytics_arguments analytics_id: 'Doc Auth', irs_reproofing: irs_reproofing?, redo_document_capture: idv_session.redo_document_capture, - }.compact.merge(ab_test_analytics_buckets) + skip_hybrid_handoff: idv_session.skip_hybrid_handoff, + }.merge(ab_test_analytics_buckets) end def handle_stored_result diff --git a/app/controllers/idv/getting_started_controller.rb b/app/controllers/idv/getting_started_controller.rb index d4e547fbef3..f622b68aad5 100644 --- a/app/controllers/idv/getting_started_controller.rb +++ b/app/controllers/idv/getting_started_controller.rb @@ -46,6 +46,7 @@ def analytics_arguments { step: 'getting_started', analytics_id: 'Doc Auth', + skip_hybrid_handoff: idv_session.skip_hybrid_handoff, irs_reproofing: irs_reproofing?, }.merge(ab_test_analytics_buckets) end diff --git a/app/controllers/idv/hybrid_handoff_controller.rb b/app/controllers/idv/hybrid_handoff_controller.rb index 18271841804..d75ec9427a8 100644 --- a/app/controllers/idv/hybrid_handoff_controller.rb +++ b/app/controllers/idv/hybrid_handoff_controller.rb @@ -145,7 +145,8 @@ def analytics_arguments analytics_id: 'Doc Auth', irs_reproofing: irs_reproofing?, redo_document_capture: params[:redo] ? true : nil, - }.compact.merge(ab_test_analytics_buckets) + skip_hybrid_handoff: idv_session.skip_hybrid_handoff, + }.merge(ab_test_analytics_buckets) end def form_response(destination:) diff --git a/app/controllers/idv/link_sent_controller.rb b/app/controllers/idv/link_sent_controller.rb index cdf763a0dfa..f6c345eb50a 100644 --- a/app/controllers/idv/link_sent_controller.rb +++ b/app/controllers/idv/link_sent_controller.rb @@ -6,7 +6,6 @@ class LinkSentController < ApplicationController before_action :confirm_hybrid_handoff_complete before_action :confirm_document_capture_needed - before_action :extend_timeout_using_meta_refresh def show analytics.idv_doc_auth_link_sent_visited(**analytics_arguments) @@ -91,18 +90,5 @@ def document_capture_session_result document_capture_session&.load_doc_auth_async_result end end - - def extend_timeout_using_meta_refresh - max_10min_refreshes = IdentityConfig.store.doc_auth_extend_timeout_by_minutes / 10 - return if max_10min_refreshes <= 0 - meta_refresh_count = flow_session[:meta_refresh_count].to_i - return if meta_refresh_count >= max_10min_refreshes - do_meta_refresh(meta_refresh_count) - end - - def do_meta_refresh(meta_refresh_count) - @meta_refresh = 10 * 60 - flow_session[:meta_refresh_count] = meta_refresh_count + 1 - end end end diff --git a/app/controllers/openid_connect/authorization_controller.rb b/app/controllers/openid_connect/authorization_controller.rb index 203a104b06f..e34174ef6ff 100644 --- a/app/controllers/openid_connect/authorization_controller.rb +++ b/app/controllers/openid_connect/authorization_controller.rb @@ -6,6 +6,7 @@ class AuthorizationController < ApplicationController include SecureHeadersConcern include AuthorizationCountConcern include BillableEventTrackable + include ForcedReauthenticationConcern before_action :build_authorize_form_from_params, only: [:index] before_action :pre_validate_authorize_form, only: [:index] @@ -125,12 +126,22 @@ def pre_validate_authorize_form end def sign_out_if_prompt_param_is_login_and_user_is_signed_in + if @authorize_form.prompt != 'login' + set_issuer_forced_reauthentication( + issuer: @authorize_form.service_provider.issuer, + is_forced_reauthentication: false, + ) + end return unless user_signed_in? && @authorize_form.prompt == 'login' return if session[:oidc_state_for_login_prompt] == @authorize_form.state return if check_sp_handoff_bounced unless sp_session[:request_url] == request.original_url sign_out session[:oidc_state_for_login_prompt] = @authorize_form.state + set_issuer_forced_reauthentication( + issuer: @authorize_form.service_provider.issuer, + is_forced_reauthentication: true, + ) end end diff --git a/app/controllers/users/phone_setup_controller.rb b/app/controllers/users/phone_setup_controller.rb index 9dc28867cfb..6344fd8e60b 100644 --- a/app/controllers/users/phone_setup_controller.rb +++ b/app/controllers/users/phone_setup_controller.rb @@ -5,16 +5,20 @@ class PhoneSetupController < ApplicationController include PhoneConfirmation include MfaSetupConcern include RecaptchaConcern + include ReauthenticationRequiredConcern before_action :authenticate_user before_action :confirm_user_authenticated_for_2fa_setup - before_action :confirm_user_in_account_setup before_action :set_setup_presenter before_action :allow_csp_recaptcha_src, if: :recaptcha_enabled? + before_action :redirect_if_phone_vendor_outage + before_action :confirm_recently_authenticated_2fa + before_action :check_max_phone_numbers_per_account, only: %i[index create] helper_method :in_multi_mfa_selection_flow? def index + user_session[:phone_id] = nil @new_phone_form = NewPhoneForm.new( user: current_user, analytics: analytics, @@ -45,9 +49,13 @@ def recaptcha_enabled? def track_phone_setup_visit mfa_user = MfaContext.new(current_user) - analytics.user_registration_phone_setup_visit( - enabled_mfa_methods_count: mfa_user.enabled_mfa_methods_count, - ) + if in_multi_mfa_selection_flow? + analytics.user_registration_phone_setup_visit( + enabled_mfa_methods_count: mfa_user.enabled_mfa_methods_count, + ) + else + analytics.add_phone_setup_visit + end end def set_setup_presenter @@ -70,6 +78,7 @@ def handle_create_success(phone) phone: @new_phone_form.phone, selected_delivery_method: @new_phone_form.otp_delivery_preference, phone_type: @new_phone_form.phone_info&.type, + selected_default_number: @new_phone_form.otp_make_default_number, ) else flash[:error] = t('errors.messages.phone_duplicate') @@ -77,6 +86,21 @@ def handle_create_success(phone) end end + def check_max_phone_numbers_per_account + max_phones_count = IdentityConfig.store.max_phone_numbers_per_account + return if current_user.phone_configurations.count < max_phones_count + flash[:phone_error] = t('users.phones.error_message') + redirect_path = request.referer.match(account_two_factor_authentication_url) ? + account_two_factor_authentication_url(anchor: 'phones') : + account_url(anchor: 'phones') + redirect_to redirect_path + end + + def redirect_if_phone_vendor_outage + return unless OutageStatus.new.all_phone_vendor_outage? + redirect_to vendor_outage_path(from: :users_phones) + end + def new_phone_form_params params.require(:new_phone_form).permit( :phone, @@ -88,11 +112,5 @@ def new_phone_form_params :recaptcha_mock_score, ) end - - def confirm_user_in_account_setup - return if user_fully_authenticated? && in_multi_mfa_selection_flow? - return unless MfaPolicy.new(current_user).two_factor_enabled? - redirect_to account_path - end end end diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index c8be461e9f5..f533ae24d2b 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -7,6 +7,7 @@ class SessionsController < Devise::SessionsController include RememberDeviceConcern include Ial2ProfileConcern include Api::CsrfTokenConcern + include ForcedReauthenticationConcern rescue_from ActionController::InvalidAuthenticityToken, with: :redirect_to_signin @@ -20,6 +21,9 @@ def new override_csp_for_google_analytics @ial = sp_session_ial + @issuer_forced_reauthentication = issuer_forced_reauthentication?( + issuer: decorated_session.sp_issuer, + ) analytics.sign_in_page_visit( flash: flash[:alert], stored_location: session['user_return_to'], diff --git a/app/controllers/users/two_factor_authentication_setup_controller.rb b/app/controllers/users/two_factor_authentication_setup_controller.rb index 2f5fd7bab98..479d06be738 100644 --- a/app/controllers/users/two_factor_authentication_setup_controller.rb +++ b/app/controllers/users/two_factor_authentication_setup_controller.rb @@ -5,12 +5,13 @@ class TwoFactorAuthenticationSetupController < ApplicationController before_action :authenticate_user before_action :confirm_user_authenticated_for_2fa_setup - before_action :confirm_user_needs_2fa_setup + + delegate :enabled_mfa_methods_count, to: :mfa_context def index two_factor_options_form @presenter = two_factor_options_presenter - analytics.user_registration_2fa_setup_visit + analytics.user_registration_2fa_setup_visit(enabled_mfa_methods_count:) end def create @@ -42,6 +43,10 @@ def two_factor_options_form private + def mfa_context + @mfa_context ||= MfaContext.new(current_user) + end + def submit_form two_factor_options_form.submit(two_factor_options_form_params) end @@ -52,18 +57,19 @@ def two_factor_options_presenter user: current_user, phishing_resistant_required: service_provider_mfa_policy.phishing_resistant_required?, piv_cac_required: service_provider_mfa_policy.piv_cac_required?, + show_skip_additional_mfa_link: show_skip_additional_mfa_link?, + after_mfa_setup_path:, ) end def process_valid_form user_session[:mfa_selections] = @two_factor_options_form.selection - redirect_to confirmation_path(user_session[:mfa_selections].first) - end - def confirm_user_needs_2fa_setup - return unless mfa_policy.two_factor_enabled? - return if service_provider_mfa_policy.user_needs_sp_auth_method_setup? - redirect_to after_mfa_setup_path + if user_session[:mfa_selections].first.present? + redirect_to confirmation_path(user_session[:mfa_selections].first) + else + redirect_to after_mfa_setup_path + end end def two_factor_options_form_params diff --git a/app/forms/webauthn_visit_form.rb b/app/forms/webauthn_visit_form.rb index 74feb3f2248..d299057e0b1 100644 --- a/app/forms/webauthn_visit_form.rb +++ b/app/forms/webauthn_visit_form.rb @@ -25,9 +25,7 @@ def platform_authenticator? end def current_mfa_setup_path - if mfa_user.two_factor_enabled? && in_mfa_selection_flow - second_mfa_setup_path - elsif mfa_user.two_factor_enabled? + if mfa_user.two_factor_enabled? && !in_mfa_selection_flow account_path else authentication_methods_setup_path diff --git a/app/javascript/packages/document-capture/components/location-collection-item.spec.tsx b/app/javascript/packages/document-capture/components/location-collection-item.spec.tsx index 013d181f665..16d54ff419b 100644 --- a/app/javascript/packages/document-capture/components/location-collection-item.spec.tsx +++ b/app/javascript/packages/document-capture/components/location-collection-item.spec.tsx @@ -147,4 +147,44 @@ describe('LocationCollectionItem', () => { expect(sunHours).not.to.exist(); }); }); + + context('when handleSelect callback is not provided', () => { + it('renders the component without the button', () => { + const onClick = sinon.stub(); + const { container } = render( + , + ); + + expect(container.textContent).to.contain('in_person_proofing.body.location.location_button'); + }); + }); + + context('when handleSelect callback is provided', () => { + it('renders the component with the button', () => { + const { container } = render( + , + ); + + expect(container.textContent).to.not.contain( + 'in_person_proofing.body.location.location_button', + ); + }); + }); }); diff --git a/app/javascript/packages/document-capture/components/location-collection-item.tsx b/app/javascript/packages/document-capture/components/location-collection-item.tsx index 3cfc1ac2fd6..5d27d870685 100644 --- a/app/javascript/packages/document-capture/components/location-collection-item.tsx +++ b/app/javascript/packages/document-capture/components/location-collection-item.tsx @@ -4,7 +4,7 @@ import { useI18n } from '@18f/identity-react-i18n'; interface LocationCollectionItemProps { distance?: string; formattedCityStateZip: string; - handleSelect: (event: React.MouseEvent, selection: number) => void; + handleSelect?: (event: React.MouseEvent, selection: number) => void; name?: string; saturdayHours: string; selectId: number; @@ -60,24 +60,28 @@ function LocationCollectionItem({ {`${t('in_person_proofing.body.location.retail_hours_sun')} ${sundayHours}`} )} - handleSelect(event, selectId)} - type="submit" - > - {t('in_person_proofing.body.location.location_button')} - + {handleSelect && ( + handleSelect(event, selectId)} + type="submit" + > + {t('in_person_proofing.body.location.location_button')} + + )}
- { - handleSelect(event, selectId); - }} - type="submit" - > - {t('in_person_proofing.body.location.location_button')} - + {handleSelect && ( + { + handleSelect(event, selectId); + }} + type="submit" + > + {t('in_person_proofing.body.location.location_button')} + + )}
diff --git a/app/javascript/packages/phone-input/package.json b/app/javascript/packages/phone-input/package.json index c8f0d8a0018..e0d1e27b6eb 100644 --- a/app/javascript/packages/phone-input/package.json +++ b/app/javascript/packages/phone-input/package.json @@ -4,6 +4,6 @@ "version": "1.0.0", "dependencies": { "intl-tel-input": "^17.0.19", - "libphonenumber-js": "^1.10.39" + "libphonenumber-js": "^1.10.41" } } diff --git a/app/jobs/in_person/send_proofing_notification_job.rb b/app/jobs/in_person/send_proofing_notification_job.rb index edc07781164..c3d84a404d5 100644 --- a/app/jobs/in_person/send_proofing_notification_job.rb +++ b/app/jobs/in_person/send_proofing_notification_job.rb @@ -2,6 +2,8 @@ module InPerson class SendProofingNotificationJob < ApplicationJob + include LocaleHelper + # @param [Number] enrollment_id primary key of the enrollment def perform(enrollment_id) return unless IdentityConfig.store.in_person_proofing_enabled && @@ -42,7 +44,7 @@ def perform(enrollment_id) rescue StandardError => err analytics(user: enrollment&.user || AnonymousUser.new). idv_in_person_send_proofing_notification_job_exception( - enrollment_code: enrollment&.code, + enrollment_code: enrollment&.enrollment_code, enrollment_id: enrollment_id, exception_class: err.class.to_s, exception_message: err.message, @@ -72,12 +74,14 @@ def handle_telephony_response(enrollment:, phone:, telephony_response:) end def notification_message(enrollment:) - proof_date = enrollment.proofed_at ? I18n.l(enrollment.proofed_at, format: :sms_date) : 'NA' - I18n.t( - 'telephony.confirmation_ipp_enrollment_result.sms', - app_name: APP_NAME, - proof_date: proof_date, - ) + with_user_locale(enrollment.user) do + proof_date = I18n.l(enrollment.proofed_at, format: :sms_date) + I18n.t( + 'telephony.confirmation_ipp_enrollment_result.sms', + app_name: APP_NAME, + proof_date: proof_date, + ) + end end def analytics(user:) diff --git a/app/models/in_person_enrollment.rb b/app/models/in_person_enrollment.rb index dc9c2400dea..169b2f16779 100644 --- a/app/models/in_person_enrollment.rb +++ b/app/models/in_person_enrollment.rb @@ -34,44 +34,64 @@ class InPersonEnrollment < ApplicationRecord before_create(:set_unique_id, unless: :unique_id) before_create(:set_capture_secondary_id) - def self.is_pending_and_established_between(early_benchmark, late_benchmark) - where(status: :pending). - and( - where(enrollment_established_at: late_benchmark...(early_benchmark.end_of_day)), - ). - order(enrollment_established_at: :asc) - end + class << self + def needs_early_email_reminder(early_benchmark, late_benchmark) + is_pending_and_established_between( + early_benchmark, + late_benchmark, + ).where(early_reminder_sent: false) + end - def self.needs_early_email_reminder(early_benchmark, late_benchmark) - self.is_pending_and_established_between( - early_benchmark, - late_benchmark, - ).where(early_reminder_sent: false) - end + def needs_late_email_reminder(early_benchmark, late_benchmark) + is_pending_and_established_between( + early_benchmark, + late_benchmark, + ).where(late_reminder_sent: false) + end - def self.needs_late_email_reminder(early_benchmark, late_benchmark) - self.is_pending_and_established_between( - early_benchmark, - late_benchmark, - ).where(late_reminder_sent: false) - end + # Find enrollments that need a status check via the USPS API + def needs_usps_status_check(check_interval) + where(status: :pending). + and( + where(last_batch_claimed_at: check_interval). + or(where(last_batch_claimed_at: nil)), + ) + end - # Find enrollments that need a status check via the USPS API - def self.needs_usps_status_check(check_interval) - where(status: :pending). - and( - where(last_batch_claimed_at: check_interval). - or(where(last_batch_claimed_at: nil)), - ) - end + def needs_usps_status_check_batch(batch_at) + where(status: :pending). + and( + where(last_batch_claimed_at: batch_at), + ). + order(status_check_attempted_at: :asc) + end + + # Find enrollments that are ready for a status check via the USPS API + def needs_status_check_on_ready_enrollments(check_interval) + needs_usps_status_check(check_interval).where(ready_for_status_check: true) + end - def self.needs_usps_status_check_batch(batch_at) - where(status: :pending). - and( - where(last_batch_claimed_at: batch_at), - ). - order(status_check_attempted_at: :asc) + # Find waiting enrollments that need a status check via the USPS API + def needs_status_check_on_waiting_enrollments(check_interval) + needs_usps_status_check(check_interval).where(ready_for_status_check: false) + end + + # Generates a random 18-digit string, the hex returns a string of length n*2 + def generate_unique_id + SecureRandom.hex(9) + end + + private + + def is_pending_and_established_between(early_benchmark, late_benchmark) + where(status: :pending). + and( + where(enrollment_established_at: late_benchmark...(early_benchmark.end_of_day)), + ). + order(enrollment_established_at: :asc) + end end + # end class methods # Does this enrollment need a status check via the USPS API? def needs_usps_status_check?(check_interval) @@ -81,21 +101,11 @@ def needs_usps_status_check?(check_interval) ) end - # Find enrollments that are ready for a status check via the USPS API - def self.needs_status_check_on_ready_enrollments(check_interval) - needs_usps_status_check(check_interval).where(ready_for_status_check: true) - end - # Does this ready enrollment need a status check via the USPS API? def needs_status_check_on_ready_enrollment?(check_interval) needs_usps_status_check?(check_interval) && ready_for_status_check? end - # Find waiting enrollments that need a status check via the USPS API - def self.needs_status_check_on_waiting_enrollments(check_interval) - needs_usps_status_check(check_interval).where(ready_for_status_check: false) - end - # Does this waiting enrollment need a status check via the USPS API? def needs_status_check_on_waiting_enrollment?(check_interval) needs_usps_status_check?(check_interval) && !ready_for_status_check? @@ -121,16 +131,6 @@ def minutes_since_last_status_update (Time.zone.now - status_updated_at).seconds.in_minutes.round(2) end - # (deprecated) Returns the value to use for the USPS enrollment ID - def usps_unique_id - user.uuid.delete('-').slice(0, 18) - end - - # Generates a random 18-digit string, the hex returns a string of length n*2 - def self.generate_unique_id - SecureRandom.hex(9) - end - def due_date start_date = enrollment_established_at.presence || created_at start_date + IdentityConfig.store.in_person_enrollment_validity_in_days.days @@ -141,18 +141,24 @@ def days_to_due_date (today...due_date).count end - def on_notification_sent_at_updated - if self.notification_sent_at && self.notification_phone_configuration - self.notification_phone_configuration.destroy - end + def eligible_for_notification? + notification_phone_configuration.present? && (passed? || failed?) end - def eligible_for_notification? - self.notification_phone_configuration.present? && (self.passed? || self.failed?) + # (deprecated) Returns the value to use for the USPS enrollment ID + def usps_unique_id + user.uuid.delete('-').slice(0, 18) end private + def on_notification_sent_at_updated + change_will_be_saved = notification_sent_at_change_to_be_saved&.last.present? + if change_will_be_saved && notification_phone_configuration.present? + notification_phone_configuration.destroy + end + end + def on_status_updated if enrollment_will_be_cancelled_or_expired? && notification_phone_configuration.present? notification_phone_configuration.destroy! @@ -165,7 +171,7 @@ def enrollment_will_be_cancelled_or_expired? end def set_unique_id - self.unique_id = self.class.generate_unique_id + self.unique_id = InPersonEnrollment.generate_unique_id end def profile_belongs_to_user diff --git a/app/presenters/navigation_presenter.rb b/app/presenters/navigation_presenter.rb index e9e7409098b..0613f22e587 100644 --- a/app/presenters/navigation_presenter.rb +++ b/app/presenters/navigation_presenter.rb @@ -25,7 +25,7 @@ def navigation_items NavItem.new( I18n.t('account.navigation.two_factor_authentication'), account_two_factor_authentication_path, [ - NavItem.new(I18n.t('account.navigation.add_phone_number'), add_phone_path), + NavItem.new(I18n.t('account.navigation.add_phone_number'), phone_setup_path), NavItem.new( I18n.t('account.navigation.add_authentication_apps'), authenticator_setup_url, diff --git a/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb b/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb index 1fc5f0ba08d..8bf2f003c51 100644 --- a/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb +++ b/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb @@ -73,7 +73,7 @@ def redirect_location_step def troubleshoot_change_phone_or_method_option if unconfirmed_phone - BlockLinkComponent.new(url: add_phone_path).with_content( + BlockLinkComponent.new(url: phone_setup_path).with_content( t('two_factor_authentication.phone_verification.troubleshooting.change_number'), ) else diff --git a/app/presenters/two_factor_options_presenter.rb b/app/presenters/two_factor_options_presenter.rb index d733cf45a5c..37ce24c5408 100644 --- a/app/presenters/two_factor_options_presenter.rb +++ b/app/presenters/two_factor_options_presenter.rb @@ -1,18 +1,24 @@ class TwoFactorOptionsPresenter include ActionView::Helpers::TranslationHelper - attr_reader :user - - def initialize(user_agent:, - user: nil, - phishing_resistant_required: false, - piv_cac_required: false, - show_skip_additional_mfa_link: true) + attr_reader :user, :after_mfa_setup_path + + delegate :two_factor_enabled?, to: :mfa_policy + + def initialize( + user_agent:, + user: nil, + phishing_resistant_required: false, + piv_cac_required: false, + show_skip_additional_mfa_link: true, + after_mfa_setup_path: nil + ) @user_agent = user_agent @user = user @phishing_resistant_required = phishing_resistant_required @piv_cac_required = piv_cac_required @show_skip_additional_mfa_link = show_skip_additional_mfa_link + @after_mfa_setup_path = after_mfa_setup_path end def options @@ -54,6 +60,10 @@ def show_skip_additional_mfa_link? @show_skip_additional_mfa_link end + def skip_path + after_mfa_setup_path if two_factor_enabled? && show_skip_additional_mfa_link? + end + private def piv_cac_option diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 1ee40706f45..1cebaeccaeb 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -918,6 +918,8 @@ def idv_doc_auth_welcome_visited(**extra) # @param [Boolean] gpo_verification_pending Profile is awaiting gpo verificaiton # @param [Boolean] in_person_verification_pending Profile is awaiting in person verificaiton # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @see Reporting::IdentityVerificationReport#query This event is used by the identity verification + # report. Changes here should be reflected there. # Tracks the last step of IDV, indicates the user successfully proofed def idv_final( success:, @@ -1051,6 +1053,8 @@ def idv_gpo_reminder_email_sent(user_id:, **extra) # @param [Integer] attempts Number of attempts to enter a correct code # @param [Boolean] pending_in_person_enrollment # @param [Boolean] fraud_check_failed + # @see Reporting::IdentityVerificationReport#query This event is used by the identity verification + # report. Changes here should be reflected there. # GPO verification submitted def idv_gpo_verification_submitted( success:, @@ -3883,9 +3887,12 @@ def user_registration_2fa_setup( end # Tracks when user visits MFA selection page - def user_registration_2fa_setup_visit + # @param [Integer] enabled_mfa_methods_count Number of MFAs associated with user at time of visit + def user_registration_2fa_setup_visit(enabled_mfa_methods_count:, **extra) track_event( 'User Registration: 2FA Setup visited', + enabled_mfa_methods_count:, + **extra, ) end diff --git a/app/services/encryption/encryptors/pii_encryptor.rb b/app/services/encryption/encryptors/pii_encryptor.rb index 704a8cc618c..af5ec65e9f2 100644 --- a/app/services/encryption/encryptors/pii_encryptor.rb +++ b/app/services/encryption/encryptors/pii_encryptor.rb @@ -70,10 +70,9 @@ def encrypt(plaintext, user_uuid: nil) end def decrypt(ciphertext_pair, user_uuid: nil) - ciphertext_string = ciphertext_pair.single_region_ciphertext - + ciphertext_string = ciphertext_pair.multi_or_single_region_ciphertext ciphertext = Ciphertext.parse_from_string(ciphertext_string) - aes_encrypted_ciphertext = single_region_kms_client.decrypt( + aes_encrypted_ciphertext = multi_region_kms_client.decrypt( ciphertext.encrypted_data, kms_encryption_context(user_uuid: user_uuid) ) aes_encryption_key = scrypt_password_digest(salt: ciphertext.salt, cost: ciphertext.cost) diff --git a/app/services/encryption/password_verifier.rb b/app/services/encryption/password_verifier.rb index b5133bfb400..edcc30a2b3c 100644 --- a/app/services/encryption/password_verifier.rb +++ b/app/services/encryption/password_verifier.rb @@ -69,7 +69,7 @@ def create_digest_pair(password:, user_uuid:) end def verify(password:, digest_pair:, user_uuid:) - digest = digest_pair.single_region_ciphertext + digest = digest_pair.multi_or_single_region_ciphertext password_digest = PasswordDigest.parse_from_string(digest) return verify_uak_digest(password, digest) if stale_digest?(digest) @@ -101,7 +101,7 @@ def verify_password_against_digest(password:, password_digest:, user_uuid:) end def decrypt_digest_with_kms(encrypted_password, user_uuid) - single_region_kms_client.decrypt( + multi_region_kms_client.decrypt( encrypted_password, kms_encryption_context(user_uuid: user_uuid) ) end diff --git a/app/services/encryption/regional_ciphertext_pair.rb b/app/services/encryption/regional_ciphertext_pair.rb index 18bec0cde37..73646f14e79 100644 --- a/app/services/encryption/regional_ciphertext_pair.rb +++ b/app/services/encryption/regional_ciphertext_pair.rb @@ -4,4 +4,12 @@ def to_ary [single_region_ciphertext, multi_region_ciphertext] end + + def multi_or_single_region_ciphertext + if IdentityConfig.store.aws_kms_multi_region_read_enabled + multi_region_ciphertext.presence || single_region_ciphertext + else + single_region_ciphertext + end + end end diff --git a/app/services/idv/steps/threat_metrix_step_helper.rb b/app/services/idv/steps/threat_metrix_step_helper.rb index 24ec29174c9..6817b5cb5d5 100644 --- a/app/services/idv/steps/threat_metrix_step_helper.rb +++ b/app/services/idv/steps/threat_metrix_step_helper.rb @@ -12,12 +12,8 @@ def threatmetrix_view_variables end def generate_threatmetrix_session_id - if !updating_ssn? - idv_session.threatmetrix_session_id = SecureRandom.uuid - # for 50/50 state, to be removed in next deploy - flow_session[:threatmetrix_session_id] = idv_session.threatmetrix_session_id - end - idv_session.threatmetrix_session_id || flow_session[:threatmetrix_session_id] + idv_session.threatmetrix_session_id = SecureRandom.uuid if !updating_ssn? + idv_session.threatmetrix_session_id end # @return [Array] diff --git a/app/services/service_provider_updater.rb b/app/services/service_provider_updater.rb index ab878f9b16d..ccece2020ee 100644 --- a/app/services/service_provider_updater.rb +++ b/app/services/service_provider_updater.rb @@ -3,7 +3,6 @@ class ServiceProviderUpdater SP_PROTECTED_ATTRIBUTES = %i[ created_at id - native updated_at ].to_set.freeze @@ -34,13 +33,12 @@ def update_cache(service_provider) if service_provider['active'] == true create_or_update_service_provider(issuer, service_provider) else - ServiceProvider.where(issuer: issuer, native: false).destroy_all + ServiceProvider.where(issuer: issuer).destroy_all end end def create_or_update_service_provider(issuer, service_provider) sp = ServiceProvider.find_by(issuer: issuer) - return if sp&.native? sync_model(sp, cleaned_service_provider(service_provider)) end diff --git a/app/views/accounts/_phone.html.erb b/app/views/accounts/_phone.html.erb index a9eaf9bb540..0e267f47f2b 100644 --- a/app/views/accounts/_phone.html.erb +++ b/app/views/accounts/_phone.html.erb @@ -25,7 +25,7 @@ <% if current_user.phone_configurations.count < IdentityConfig.store.max_phone_numbers_per_account %> <%= render ButtonComponent.new( - action: ->(**tag_options, &block) { link_to(add_phone_path, **tag_options, &block) }, + action: ->(**tag_options, &block) { link_to(phone_setup_path, **tag_options, &block) }, outline: true, icon: :add, class: 'margin-top-2', diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index 18946fc61b8..196360f07e3 100644 --- a/app/views/devise/sessions/new.html.erb +++ b/app/views/devise/sessions/new.html.erb @@ -17,6 +17,12 @@ <%= render 'shared/sp_alert', section: 'sign_in' %> +<% if @issuer_forced_reauthentication %> +

+ <%= t('account.login.forced_reauthentication_notice_html', sp_name: decorated_session.sp_name) %> +

+<% end %> + <%= simple_form_for( resource, as: resource_name, diff --git a/app/views/idv/link_sent/show.html.erb b/app/views/idv/link_sent/show.html.erb index 3463e51f452..8fb01c7d954 100644 --- a/app/views/idv/link_sent/show.html.erb +++ b/app/views/idv/link_sent/show.html.erb @@ -9,10 +9,6 @@ <% title t('titles.doc_auth.link_sent') %> - -<% if @meta_refresh && !FeatureManagement.doc_capture_polling_enabled? %> - <%= content_for(:meta_refresh) { @meta_refresh.to_s } %> -<% end %> <% if flow_session[:error_message] %> <%= render AlertComponent.new( type: :error, diff --git a/app/views/idv/welcome/_welcome_new.html.erb b/app/views/idv/welcome/_welcome_new.html.erb index 25896b3cb43..af39a487264 100644 --- a/app/views/idv/welcome/_welcome_new.html.erb +++ b/app/views/idv/welcome/_welcome_new.html.erb @@ -16,7 +16,7 @@ category: 'verify-your-identity', article: 'how-to-verify-your-identity', flow: :idv, - step: :getting_started, + step: :welcome_new, location: 'intro_paragraph', ), ), @@ -59,7 +59,7 @@ <% end %> - <%= render 'shared/cancel', link: idv_cancel_path(step: 'getting_started') %> + <%= render 'shared/cancel', link: idv_cancel_path(step: 'welcome_new') %> <% end %> <%= javascript_packs_tag_once('document-capture-welcome') %> diff --git a/app/views/mfa_confirmation/show.html.erb b/app/views/mfa_confirmation/show.html.erb index 5466f763230..d5c403da0ea 100644 --- a/app/views/mfa_confirmation/show.html.erb +++ b/app/views/mfa_confirmation/show.html.erb @@ -10,7 +10,7 @@
<%= link_to( - second_mfa_setup_path, + authentication_methods_setup_path, class: 'usa-button usa-button--wide usa-button--big margin-bottom-3', ) { @content.button } %>
@@ -23,4 +23,4 @@ ) do %> <%= t('mfa.skip') %> <% end %> -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/shared/_cancel_or_back_to_options.html.erb b/app/views/shared/_cancel_or_back_to_options.html.erb index 69d0afac603..ef39ba162e7 100644 --- a/app/views/shared/_cancel_or_back_to_options.html.erb +++ b/app/views/shared/_cancel_or_back_to_options.html.erb @@ -1,7 +1,5 @@ <%= render PageFooterComponent.new do %> - <% if MfaPolicy.new(current_user).two_factor_enabled? && in_multi_mfa_selection_flow? %> - <%= link_to t('two_factor_authentication.choose_another_option'), second_mfa_setup_path %> - <% elsif MfaPolicy.new(current_user).two_factor_enabled? %> + <% if MfaPolicy.new(current_user).two_factor_enabled? && !in_multi_mfa_selection_flow? %> <%= link_to t('links.cancel'), account_path %> <% else %> <%= link_to t('two_factor_authentication.choose_another_option'), authentication_methods_setup_path %> diff --git a/app/views/sign_up/completions/show.html.erb b/app/views/sign_up/completions/show.html.erb index 1fb2b14660f..a4cfdf5ba3f 100644 --- a/app/views/sign_up/completions/show.html.erb +++ b/app/views/sign_up/completions/show.html.erb @@ -18,7 +18,7 @@ <%= render(AlertComponent.new(type: :warning, class: 'margin-bottom-4')) do %> <%= link_to( t('mfa.second_method_warning.link'), - second_mfa_setup_path, + authentication_methods_setup_path, ) %> <%= t('mfa.second_method_warning.text') %> <% end %> @@ -30,4 +30,4 @@ <%= render PageFooterComponent.new do %> <%= link_to t('links.cancel'), return_to_sp_cancel_path %> -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/users/backup_code_setup/confirm_backup_codes.html.erb b/app/views/users/backup_code_setup/confirm_backup_codes.html.erb index 22133481d8b..d2f59eb88fa 100644 --- a/app/views/users/backup_code_setup/confirm_backup_codes.html.erb +++ b/app/views/users/backup_code_setup/confirm_backup_codes.html.erb @@ -27,11 +27,11 @@ big: true, full_width: true, outline: true, - ).with_content(t('two_factor_authentication.backup_codes.new_backup_codes_html')) %> + ).with_content(t('two_factor_authentication.backup_codes.new_backup_codes_html')) %> <%= render PageFooterComponent.new do %> - <%= link_to t('two_factor_authentication.backup_codes.add_another_authentication_option'), second_mfa_setup_path %> -<% end %> + <%= link_to t('two_factor_authentication.backup_codes.add_another_authentication_option'), authentication_methods_setup_path %> +<% end %> diff --git a/app/views/users/phone_setup/index.html.erb b/app/views/users/phone_setup/index.html.erb index 397e40a7f73..613148ac898 100644 --- a/app/views/users/phone_setup/index.html.erb +++ b/app/views/users/phone_setup/index.html.erb @@ -1,8 +1,8 @@ -<% title t('titles.phone_setup') %> +<%= title t('titles.add_info.phone') %> <%= render(VendorOutageAlertComponent.new(vendors: [:sms, :voice])) %> -<%= render PageHeadingComponent.new.with_content(t('titles.phone_setup')) %> +<%= render PageHeadingComponent.new.with_content(t('headings.add_info.phone')) %>

<%= t('two_factor_authentication.phone_info') %> diff --git a/app/views/users/phones/add.html.erb b/app/views/users/phones/add.html.erb index 3560194b531..b91627fabe5 100644 --- a/app/views/users/phones/add.html.erb +++ b/app/views/users/phones/add.html.erb @@ -32,7 +32,7 @@ <%= render CaptchaSubmitButtonComponent.new( form: f, action: PhoneRecaptchaValidator::RECAPTCHA_ACTION, - ).with_content(t('forms.buttons.continue')) %> + ).with_content(t('forms.buttons.send_one_time_code')) %> <% end %>

diff --git a/app/views/users/two_factor_authentication_setup/index.html.erb b/app/views/users/two_factor_authentication_setup/index.html.erb index e8a52619d9e..1506a1d37b3 100644 --- a/app/views/users/two_factor_authentication_setup/index.html.erb +++ b/app/views/users/two_factor_authentication_setup/index.html.erb @@ -14,6 +14,20 @@

<%= @presenter.intro %>

+<% if @presenter.two_factor_enabled? %> +

+ <%= t('headings.account.two_factor') %> +

+ +
    + <% @presenter.options.each do |option| %> + <% if option.mfa_configuration_count > 0 %> + <%= render partial: 'partials/multi_factor_authentication/selected_mfa_option', locals: { option: option } %> + <% end %> + <% end %> +
+<% end %> + <%= simple_form_for @two_factor_options_form, html: { autocomplete: 'off' }, method: :patch, @@ -35,4 +49,12 @@ <%= f.submit t('forms.buttons.continue'), class: 'margin-bottom-1' %> <% end %> -<%= render 'shared/cancel', link: logout_path, link_method: :delete %> +<% if @presenter.skip_path || !@presenter.two_factor_enabled? %> + <%= render PageFooterComponent.new do %> + <% if @presenter.skip_path %> + <%= link_to t('mfa.skip'), @presenter.skip_path %> + <% elsif !@presenter.two_factor_enabled? %> + <%= link_to t('links.cancel_account_creation'), sign_up_cancel_path %> + <% end %> + <% end %> +<% end %> diff --git a/bin/oncall/email-deliveries b/bin/oncall/email-deliveries index 20ef164b29b..0272438f9c4 100755 --- a/bin/oncall/email-deliveries +++ b/bin/oncall/email-deliveries @@ -58,7 +58,7 @@ class EmailDeliveries results = query_data(uuids) table = Terminal::Table.new - table << %w[user_id timestamp message_id events] + table << %w[user_id timestamp message_id email_action events] table << :separator results.each do |result| @@ -66,6 +66,7 @@ class EmailDeliveries result.user_id, result.timestamp, result.message_id, + result.email_action, result.events.join(', '), ] end @@ -77,6 +78,7 @@ class EmailDeliveries :user_id, :timestamp, :message_id, + :email_action, :events, keyword_init: true, ) @@ -90,6 +92,7 @@ class EmailDeliveries @timestamp , properties.user_id AS user_id , properties.event_properties.ses_message_id AS ses_message_id + , properties.event_properties.action AS email_action | filter name = 'Email Sent' | filter properties.user_id IN #{quote(uuids)} | limit 10000 @@ -122,6 +125,7 @@ class EmailDeliveries map do |message_id, events| Result.new( user_id: events_by_message_id[message_id]['user_id'], + email_action: events_by_message_id[message_id]['email_action'], timestamp: events_by_message_id[message_id]['@timestamp'], message_id: message_id, events: events.sort_by { |e| e['@timestamp'] }.map { |e| e['event_type'] }, diff --git a/config/application.yml.default b/config/application.yml.default index 53c502edb8c..71aeeb71819 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -59,6 +59,7 @@ aws_kms_key_id: alias/login-dot-gov-test-keymaker aws_kms_client_contextless_pool_size: 5 aws_kms_client_multi_pool_size: 5 aws_kms_multi_region_key_id: alias/login-dot-gov-keymaker-multi-region +aws_kms_multi_region_read_enabled: false aws_logo_bucket: '' aws_region: 'us-west-2' backup_code_cost: '2000$8$1$' @@ -81,7 +82,6 @@ disable_logout_get_request: true disallow_all_web_crawlers: true disposable_email_services: '[]' doc_auth_attempt_window_in_minutes: 360 -doc_auth_extend_timeout_by_minutes: 40 doc_capture_polling_enabled: true doc_auth_client_glare_threshold: 50 doc_auth_client_sharpness_threshold: 50 diff --git a/config/initializers/job_configurations.rb b/config/initializers/job_configurations.rb index 1845e0dc250..8cf01a96956 100644 --- a/config/initializers/job_configurations.rb +++ b/config/initializers/job_configurations.rb @@ -182,12 +182,6 @@ cron: cron_24h, args: -> { [Time.zone.today] }, }, - # Send reminder letters for old, outstanding GPO verification codes - send_gpo_code_reminders: { - class: 'GpoReminderJob', - cron: cron_24h, - args: -> { [14.days.ago] }, - }, }.compact end # rubocop:enable Metrics/BlockLength diff --git a/config/locales/account/en.yml b/config/locales/account/en.yml index 13d0dd36add..2ce691e82e4 100644 --- a/config/locales/account/en.yml +++ b/config/locales/account/en.yml @@ -71,6 +71,8 @@ en: delete_account: Delete regenerate_personal_key: Reset login: + forced_reauthentication_notice_html: %{sp_name} needs you to + enter your email and password again. piv_cac: Sign in with your government employee ID tab_navigation: Account creation tabs navigation: diff --git a/config/locales/account/es.yml b/config/locales/account/es.yml index 86762885259..c6e124fcde8 100644 --- a/config/locales/account/es.yml +++ b/config/locales/account/es.yml @@ -72,6 +72,8 @@ es: delete_account: Eliminar regenerate_personal_key: Restablecer login: + forced_reauthentication_notice_html: %{sp_name} requiere que + vuelvas a ingresar tu correo electrónico y contraseña. piv_cac: Inicie sesión con su identificación de empleado del gobierno tab_navigation: Pestañas de creación de cuenta navigation: diff --git a/config/locales/account/fr.yml b/config/locales/account/fr.yml index 51ae5a45cc5..c5e969c9181 100644 --- a/config/locales/account/fr.yml +++ b/config/locales/account/fr.yml @@ -77,6 +77,9 @@ fr: delete_account: Effacer regenerate_personal_key: Réinitialiser login: + forced_reauthentication_notice_html: %{sp_name} nécessite que + vous saisissiez à nouveau votre adresse électronique et votre mot de + passe. piv_cac: Connectez-vous avec votre ID d’employé du gouvernement tab_navigation: Onglets de création de compte navigation: diff --git a/config/locales/titles/en.yml b/config/locales/titles/en.yml index f5448999dd4..4be6ec2f24c 100644 --- a/config/locales/titles/en.yml +++ b/config/locales/titles/en.yml @@ -52,7 +52,6 @@ en: change: Change the password for your account forgot: Reset password personal_key: Just in case - phone_setup: Get your one-time code piv_cac_login: add: Add your PIV or CAC new: Use your PIV/CAC to sign in to your account diff --git a/config/locales/titles/es.yml b/config/locales/titles/es.yml index cdc88f7fd44..5349232ffcd 100644 --- a/config/locales/titles/es.yml +++ b/config/locales/titles/es.yml @@ -52,7 +52,6 @@ es: change: Cambie la contraseña de su cuenta forgot: Restablecer la contraseña personal_key: Por si acaso - phone_setup: Obtenga su código único piv_cac_login: add: Agregue su PIV o CAC new: Use su PIV / CAC para iniciar sesión en su cuenta diff --git a/config/locales/titles/fr.yml b/config/locales/titles/fr.yml index 6297821eaa1..ac64385f278 100644 --- a/config/locales/titles/fr.yml +++ b/config/locales/titles/fr.yml @@ -52,7 +52,6 @@ fr: change: Changez le mot de passe de votre compte forgot: Réinitialisez le mot de passe personal_key: Juste au cas - phone_setup: Obtenez votre code à usage unique piv_cac_login: add: Ajoutez votre PIV ou CAC new: Utilisez votre PIV / CAC pour vous connecter à votre compte diff --git a/lib/cleanup/destroyable_records.rb b/lib/cleanup/destroyable_records.rb index fb417a8109b..31228211269 100644 --- a/lib/cleanup/destroyable_records.rb +++ b/lib/cleanup/destroyable_records.rb @@ -29,7 +29,8 @@ def print_data stdout.puts '********' stdout.puts "This provider has #{in_person_enrollments.size} in person enrollments " \ - "that will be destroyed" + "that will be destroyed - Please handle these removals manually. " \ + "For more details check https://cm-jira.usa.gov/browse/LG-10679" stdout.puts "\n" stdout.puts '*******' diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 78f1b56f4d6..9397b94b108 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -146,6 +146,7 @@ def self.build_store(config_map) config.add(:aws_kms_client_multi_pool_size, type: :integer) config.add(:aws_kms_key_id, type: :string) config.add(:aws_kms_multi_region_key_id, type: :string) + config.add(:aws_kms_multi_region_read_enabled, type: :boolean) config.add(:aws_logo_bucket, type: :string) config.add(:aws_region, type: :string) config.add(:backup_code_cost, type: :string) @@ -186,7 +187,6 @@ def self.build_store(config_map) config.add(:doc_auth_error_dpi_threshold, type: :integer) config.add(:doc_auth_error_glare_threshold, type: :integer) config.add(:doc_auth_error_sharpness_threshold, type: :integer) - config.add(:doc_auth_extend_timeout_by_minutes, type: :integer) config.add(:doc_auth_max_attempts, type: :integer) config.add(:doc_auth_max_capture_attempts_before_native_camera, type: :integer) config.add(:doc_auth_max_submission_attempts_before_native_camera, type: :integer) diff --git a/lib/reporting/identity_verification_report.rb b/lib/reporting/identity_verification_report.rb index b78526922a6..2953a0a5fd7 100644 --- a/lib/reporting/identity_verification_report.rb +++ b/lib/reporting/identity_verification_report.rb @@ -59,7 +59,7 @@ def to_csv CSV.generate do |csv| csv << ['Report Timeframe', "#{time_range.begin} to #{time_range.end}"] csv << ['Report Generated', Date.today.to_s] # rubocop:disable Rails/Date - csv << ['Issuer', issuer] + csv << ['Issuer', issuer] if issuer.present? csv << [] csv << ['Metric', '# of Users'] csv << ['Started IdV Verification', idv_doc_auth_image_vendor_submitted] @@ -140,7 +140,7 @@ def fetch_results def query params = { - issuer: quote(issuer), + issuer: issuer && quote(issuer), event_names: quote(Events.all_events), usps_enrollment_status_updated: quote(Events::USPS_ENROLLMENT_STATUS_UPDATED), gpo_verification_submitted: quote(Events::GPO_VERIFICATION_SUBMITTED), @@ -151,13 +151,13 @@ def query fields name , properties.user_id AS user_id - | filter properties.service_provider = %{issuer} + #{issuer.present? ? '| filter properties.service_provider = %{issuer}' : ''} | filter name in %{event_names} | filter (name = %{usps_enrollment_status_updated} and properties.event_properties.passed = 1) or (name != %{usps_enrollment_status_updated}) - | filter (name = %{gpo_verification_submitted} and properties.event_properties.success = 1) + | filter (name = %{gpo_verification_submitted} and properties.event_properties.success = 1 and !properties.event_properties.pending_in_person_enrollment and !properties.event_properties.fraud_check_failed) or (name != %{gpo_verification_submitted}) - | filter (name = %{idv_final_resolution} and isblank(properties.event_properties.deactivation_reason)) + | filter (name = %{idv_final_resolution} and !properties.event_properties.fraud_review_pending and !properties.event_properties.gpo_verification_pending and !properties.event_properties.in_person_verification_pending) or (name != %{idv_final_resolution}) | limit 10000 QUERY @@ -177,7 +177,7 @@ def cloudwatch_client # rubocop:disable Rails/Output if __FILE__ == $PROGRAM_NAME - options = Reporting::CommandLineOptions.new.parse!(ARGV) + options = Reporting::CommandLineOptions.new.parse!(ARGV, require_issuer: false) puts Reporting::IdentityVerificationReport.new(**options).to_csv end diff --git a/lib/tasks/backfill_fraud_pending_reason.rake b/lib/tasks/backfill_fraud_pending_reason.rake deleted file mode 100644 index 7668f62824b..00000000000 --- a/lib/tasks/backfill_fraud_pending_reason.rake +++ /dev/null @@ -1,78 +0,0 @@ -namespace :profiles do - desc 'If a profile is in review or rejected, store the reason it was marked for fraud' - - ## - # Usage: - # - # Print pending updates - # bundle exec rake profiles:backfill_fraud_pending_reason - # - # Commit updates - # bundle exec rake profiles:backfill_fraud_pending_reason UPDATE_PROFILES=true - # - task backfill_fraud_pending_reason: :environment do |_task, _args| - ActiveRecord::Base.connection.execute('SET statement_timeout = 60000') - - update_profiles = ENV['UPDATE_PROFILES'] == 'true' - - profiles = Profile.where( - fraud_pending_reason: nil, - ).where( - 'fraud_review_pending_at IS NOT NULL OR fraud_rejection_at IS NOT NULL', - ) - - profiles.each do |profile| - proofing_component_status = profile.proofing_components&.[]('threatmetrix_review_status') - fraud_pending_reason = case proofing_component_status - when 'review' - 'threatmetrix_review' - when 'reject' - 'threatmetrix_reject' - else - 'threatmetrix_review' - end - - warn "#{profile.id},#{fraud_pending_reason},#{proofing_component_status}" - profile.update!(fraud_pending_reason: fraud_pending_reason) if update_profiles - end - end - - ## - # Usage: - # - # Rollback the above: - # - # export BACKFILL_OUTPUT='' - # bundle exec rake profiles:rollback_backfill_fraud_pending_reason - # - task rollback_backfill_fraud_pending_reason: :environment do |_task, _args| - ActiveRecord::Base.connection.execute('SET statement_timeout = 60000') - - profile_ids = ENV['BACKFILL_OUTPUT'].split("\n").map do |profile_row| - profile_row.split(',').first - end - - warn "Updating #{profile_ids.count} records" - Profile.where(id: profile_ids).update!(fraud_pending_reason: nil) - end - - ## - # Usage: - # bundle exec rake profiles:validate_backfill_fraud_pending_reason - # - task validate_backfill_fraud_pending_reason: :environment do |_task, _args| - ActiveRecord::Base.connection.execute('SET statement_timeout = 60000') - - profiles = Profile.where( - fraud_pending_reason: nil, - ).where( - 'fraud_review_pending_at IS NOT NULL OR fraud_rejection_at IS NOT NULL', - ) - - if profiles.empty? - warn 'fraud_pending_reason backfill was successful' - else - warn "fraud_pending_reason backfill left #{profile.count} rows" - end - end -end diff --git a/lib/tasks/backfill_fraud_review_pending_at.rake b/lib/tasks/backfill_fraud_review_pending_at.rake deleted file mode 100644 index 9b250fbd83c..00000000000 --- a/lib/tasks/backfill_fraud_review_pending_at.rake +++ /dev/null @@ -1,75 +0,0 @@ -namespace :profiles do - desc 'If a profile is in GPO and fraud pending state, move it out of fraud pending state' - - ## - # Usage: - # - # Print pending updates - # bundle exec rake profiles:backfill_fraud_review_pending_at - # - # Commit updates - # bundle exec rake profiles:backfill_fraud_review_pending_at UPDATE_PROFILES=true - # - task backfill_fraud_review_pending_at: :environment do |_task, _args| - ActiveRecord::Base.connection.execute('SET statement_timeout = 60000') - - update_profiles = ENV['UPDATE_PROFILES'] == 'true' - - profiles = Profile.where( - 'fraud_review_pending_at IS NOT NULL OR fraud_rejection_at IS NOT NULL', - ).where.not( - gpo_verification_pending_at: nil, - ) - - profiles.each do |profile| - if profile.fraud_pending_reason.blank? - warn "Profile ##{profile.id} does not have a fraud pending reason!" - break - end - - warn "#{profile.id},#{profile.fraud_review_pending_at},#{profile.fraud_rejection_at}" - profile.update!(fraud_review_pending_at: nil, fraud_rejection_at: nil) if update_profiles - end - end - - ## - # Usage: - # - # Rollback the above: - # - # export BACKFILL_OUTPUT='' - # bundle exec rake profiles:rollback_backfill_fraud_review_pending_at - # - task rollback_backfill_fraud_review_pending_at: :environment do |_task, _args| - ActiveRecord::Base.connection.execute('SET statement_timeout = 60000') - - profile_data = ENV['BACKFILL_OUTPUT'].split("\n").map do |profile_row| - profile_row.split(',') - end - - warn "Updating #{profile_data.count} records" - profile_data.each do |profile_datum| - profile_id, fraud_review_pending_at, fraud_rejection_at = profile_datum - Profile.where(id: profile_id).update!( - fraud_review_pending_at: fraud_review_pending_at, - fraud_rejection_at: fraud_rejection_at, - ) - end - end - - ## - # Usage: - # bundle exec rake profiles:validate_backfill_fraud_review_pending_at - # - task validate_backfill_fraud_review_pending_at: :environment do |_task, _args| - ActiveRecord::Base.connection.execute('SET statement_timeout = 60000') - - profiles = Profile.where( - 'fraud_review_pending_at IS NOT NULL OR fraud_rejection_at IS NOT NULL', - ).where.not( - gpo_verification_pending_at: nil, - ) - - warn "fraud_pending_reason backfill left #{profiles.count} rows" - end -end diff --git a/lib/tasks/multi_region_kms.rake b/lib/tasks/multi_region_kms.rake new file mode 100644 index 00000000000..879667128d8 --- /dev/null +++ b/lib/tasks/multi_region_kms.rake @@ -0,0 +1,92 @@ +namespace :multi_region_kms do + desc 'Confirm that the multi-region KMS inner-layers are the same for both ciphertexts' + task check_inner_layer: :environment do |_task, _args| + ActiveRecord::Base.connection.execute('SET statement_timeout = 60000') + + sample_password_users = + User.where.not(encrypted_password_digest_multi_region: nil).limit(10000).all + sample_personal_key_users = + User.where.not(encrypted_recovery_code_digest_multi_region: nil).limit(10000).all + sample_profiles = Profile.where.not(encrypted_pii_multi_region: nil).limit(10000).all + + kms_client = Encryption::KmsClient.new + + mismatched_records = [] + + sample_password_users.each do |user| + kms_context = { + 'context' => 'password-digest', + 'user_uuid' => user.uuid, + } + + mr_inner_layer = kms_client.decrypt( + JSON.parse(user.encrypted_password_digest_multi_region)['encrypted_password'], kms_context + ) + sr_inner_layer = kms_client.decrypt( + JSON.parse(user.encrypted_password_digest)['encrypted_password'], kms_context + ) + + if mr_inner_layer != sr_inner_layer + warn "Mismatch identified: User##{user.id}" + mismatched_records.push(user) + end + end + + sample_personal_key_users.each do |user| + kms_context = { + 'context' => 'password-digest', + 'user_uuid' => user.uuid, + } + + mr_inner_layer = kms_client.decrypt( + JSON.parse(user.encrypted_recovery_code_digest_multi_region)['encrypted_password'], + kms_context, + ) + sr_inner_layer = kms_client.decrypt( + JSON.parse(user.encrypted_recovery_code_digest)['encrypted_password'], kms_context + ) + + if mr_inner_layer != sr_inner_layer + warn "Mismatch identified: User##{user.id}" + mismatched_records.push(user) + end + end + + sample_profiles.each do |profile| + kms_context = { + 'context' => 'pii-encryption', + 'user_uuid' => profile.user.uuid, + } + + mr_pii_inner_layer = kms_client.decrypt( + Base64.decode64(JSON.parse(profile.encrypted_pii_multi_region)['encrypted_data']), + kms_context, + ) + sr_pii_inner_layer = kms_client.decrypt( + Base64.decode64(JSON.parse(profile.encrypted_pii)['encrypted_data']), + kms_context, + ) + mr_pii_recovery_inner_layer = kms_client.decrypt( + Base64.decode64(JSON.parse(profile.encrypted_pii_recovery_multi_region)['encrypted_data']), + kms_context, + ) + sr_pii_recovery_inner_layer = kms_client.decrypt( + Base64.decode64(JSON.parse(profile.encrypted_pii_recovery)['encrypted_data']), + kms_context, + ) + + mistmatch_detected = mr_pii_inner_layer != sr_pii_inner_layer || + mr_pii_recovery_inner_layer != sr_pii_recovery_inner_layer + + if mistmatch_detected + warn "Mismatch identified: Profile##{profile.id}" + mismatched_records.push(profile) + end + end + + warn "Sampled #{sample_password_users.size} passwords" + warn "Sampled #{sample_personal_key_users.size} personal keys" + warn "Sampled #{sample_profiles.size} encrypted PII records" + warn "#{mismatched_records.size} mismatched records detected" + end +end diff --git a/spec/bin/oncall/email-deliveries_spec.rb b/spec/bin/oncall/email-deliveries_spec.rb index 2b176c1b91c..3bee52b0643 100644 --- a/spec/bin/oncall/email-deliveries_spec.rb +++ b/spec/bin/oncall/email-deliveries_spec.rb @@ -58,8 +58,8 @@ # rubocop:disable Layout/LineLength let(:events_log) do [ - { '@timestamp' => '2023-01-01 00:00:01', 'user_id' => 'abc123', 'ses_message_id' => 'message-1' }, - { '@timestamp' => '2023-01-01 00:00:02', 'user_id' => 'def456', 'ses_message_id' => 'message-2' }, + { '@timestamp' => '2023-01-01 00:00:01', 'user_id' => 'abc123', 'email_action' => 'forgot_password', 'ses_message_id' => 'message-1' }, + { '@timestamp' => '2023-01-01 00:00:02', 'user_id' => 'def456', 'email_action' => 'forgot_password', 'ses_message_id' => 'message-2' }, ] end @@ -80,9 +80,9 @@ expect(table).to eq( [ - ['user_id', 'timestamp', 'message_id', 'events'], - ['abc123', '2023-01-01 00:00:01', 'message-1', 'Send, Delivery'], - ['def456', '2023-01-01 00:00:02', 'message-2', 'Send, Bounce'], + ['user_id', 'timestamp', 'message_id', 'email_action', 'events'], + ['abc123', '2023-01-01 00:00:01', 'message-1', 'forgot_password', 'Send, Delivery'], + ['def456', '2023-01-01 00:00:02', 'message-2', 'forgot_password', 'Send, Bounce'], ], ) end diff --git a/spec/components/page_footer_component_spec.rb b/spec/components/page_footer_component_spec.rb index 69e3a7ee0bb..43738a57cfc 100644 --- a/spec/components/page_footer_component_spec.rb +++ b/spec/components/page_footer_component_spec.rb @@ -6,6 +6,7 @@ rendered = render_inline PageFooterComponent.new.with_content(content) expect(rendered).to have_content(content) + expect(rendered).to have_css('.page-footer') end context 'tag options' do @@ -20,7 +21,7 @@ it 'appends custom class' do rendered = render_inline PageFooterComponent.new(class: 'custom-class') - expect(rendered).to have_css('.custom-class') + expect(rendered).to have_css('.page-footer.custom-class') end end end diff --git a/spec/controllers/concerns/forced_reauthentication_concern_spec.rb b/spec/controllers/concerns/forced_reauthentication_concern_spec.rb new file mode 100644 index 00000000000..f2a5cf4dd30 --- /dev/null +++ b/spec/controllers/concerns/forced_reauthentication_concern_spec.rb @@ -0,0 +1,50 @@ +require 'rails_helper' + +RSpec.describe ForcedReauthenticationConcern do + let(:test_class) do + Class.new do + include ForcedReauthenticationConcern + + attr_reader :session + + def initialize(session = {}) + @session = session + end + end + end + let(:instance) { test_class.new } + + describe '#issuer_forced_reauthentication?' do + it 'returns true if issuer has forced reauthentication' do + instance.set_issuer_forced_reauthentication( + issuer: 'test_issuer', + is_forced_reauthentication: true, + ) + expect(instance.issuer_forced_reauthentication?(issuer: 'test_issuer')).to eq true + end + + it 'returns false if issuer has not forced reauthentication' do + expect(instance.issuer_forced_reauthentication?(issuer: 'test_issuer')).to eq false + end + + it 'returns false if forced reauthentication is set to false for an issuer' do + instance.set_issuer_forced_reauthentication( + issuer: 'test_issuer', + is_forced_reauthentication: false, + ) + expect(instance.issuer_forced_reauthentication?(issuer: 'test_issuer')).to eq false + end + + it 'returns false if issuer sets forced reauthentication to true and then false' do + instance.set_issuer_forced_reauthentication( + issuer: 'test_issuer', + is_forced_reauthentication: true, + ) + instance.set_issuer_forced_reauthentication( + issuer: 'test_issuer', + is_forced_reauthentication: false, + ) + expect(instance.issuer_forced_reauthentication?(issuer: 'test_issuer')).to eq false + end + end +end diff --git a/spec/controllers/concerns/idv/ab_test_analytics_concern_spec.rb b/spec/controllers/concerns/idv/ab_test_analytics_concern_spec.rb index 5181f0a8470..7a8c8b9cd34 100644 --- a/spec/controllers/concerns/idv/ab_test_analytics_concern_spec.rb +++ b/spec/controllers/concerns/idv/ab_test_analytics_concern_spec.rb @@ -8,6 +8,9 @@ class StepController < ApplicationController end let(:user) { create(:user) } + let(:idv_session) do + Idv::Session.new(user_session: subject.user_session, current_user: user, service_provider: nil) + end describe '#ab_test_analytics_buckets' do controller Idv::StepController do @@ -24,12 +27,29 @@ class StepController < ApplicationController and_return(getting_started_args) end - it 'includes acuant_sdk_ab_test_analytics_args' do - expect(controller.ab_test_analytics_buckets).to include(acuant_sdk_args) + context 'idv_session is available' do + before do + sign_in(user) + expect(subject).to receive(:idv_session).and_return(idv_session) + end + it 'includes acuant_sdk_ab_test_analytics_args' do + expect(controller.ab_test_analytics_buckets).to include(acuant_sdk_args) + end + + it 'includes getting_started_ab_test_analytics_bucket' do + expect(controller.ab_test_analytics_buckets).to include(getting_started_args) + end + + it 'includes skip_hybrid_handoff' do + idv_session.skip_hybrid_handoff = :shh_value + expect(controller.ab_test_analytics_buckets).to include({ skip_hybrid_handoff: :shh_value }) + end end - it 'includes getting_started_ab_test_analytics_bucket' do - expect(controller.ab_test_analytics_buckets).to include(getting_started_args) + context 'idv_session is not available' do + it 'still works' do + expect(controller.ab_test_analytics_buckets).to include(acuant_sdk_args) + end end end end diff --git a/spec/controllers/frontend_log_controller_spec.rb b/spec/controllers/frontend_log_controller_spec.rb index 1e84d535bc9..8eda1f642f5 100644 --- a/spec/controllers/frontend_log_controller_spec.rb +++ b/spec/controllers/frontend_log_controller_spec.rb @@ -195,5 +195,12 @@ expect(request.session_options[:skip]).to eql(true) end end + + context 'with all events' do + it 'sorts keys alphabetically' do + expect(described_class::EVENT_MAP.keys). + to eq(described_class::EVENT_MAP.keys.sort_by(&:downcase)) + end + end end end diff --git a/spec/controllers/idv/agreement_controller_spec.rb b/spec/controllers/idv/agreement_controller_spec.rb index dc252a1d32b..788ea74716a 100644 --- a/spec/controllers/idv/agreement_controller_spec.rb +++ b/spec/controllers/idv/agreement_controller_spec.rb @@ -39,6 +39,7 @@ { step: 'agreement', analytics_id: 'Doc Auth', + skip_hybrid_handoff: nil, irs_reproofing: false, }.merge(ab_test_args) end @@ -93,6 +94,7 @@ errors: {}, step: 'agreement', analytics_id: 'Doc Auth', + skip_hybrid_handoff: nil, irs_reproofing: false, }.merge(ab_test_args) end @@ -134,7 +136,9 @@ put :update, params: params end.to change { subject.idv_session.flow_path - }.from(nil).to('standard') + }.from(nil).to('standard').and change { + subject.idv_session.skip_hybrid_handoff + }.from(nil).to(true) end it 'redirects to hybrid handoff' do diff --git a/spec/controllers/idv/document_capture_controller_spec.rb b/spec/controllers/idv/document_capture_controller_spec.rb index 248b7dc1184..1c1bde51257 100644 --- a/spec/controllers/idv/document_capture_controller_spec.rb +++ b/spec/controllers/idv/document_capture_controller_spec.rb @@ -56,6 +56,8 @@ { analytics_id: 'Doc Auth', flow_path: 'standard', + redo_document_capture: nil, + skip_hybrid_handoff: nil, irs_reproofing: false, step: 'document_capture', }.merge(ab_test_args) @@ -147,6 +149,8 @@ errors: {}, analytics_id: 'Doc Auth', flow_path: 'standard', + redo_document_capture: nil, + skip_hybrid_handoff: nil, irs_reproofing: false, step: 'document_capture', }.merge(ab_test_args) diff --git a/spec/controllers/idv/getting_started_controller_spec.rb b/spec/controllers/idv/getting_started_controller_spec.rb index 17cbb438c31..d47e4204838 100644 --- a/spec/controllers/idv/getting_started_controller_spec.rb +++ b/spec/controllers/idv/getting_started_controller_spec.rb @@ -38,6 +38,7 @@ { step: 'getting_started', analytics_id: 'Doc Auth', + skip_hybrid_handoff: nil, irs_reproofing: false, }.merge(ab_test_args) end @@ -100,6 +101,7 @@ errors: {}, step: 'getting_started', analytics_id: 'Doc Auth', + skip_hybrid_handoff: nil, irs_reproofing: false, }.merge(ab_test_args) end @@ -161,7 +163,9 @@ put :update, params: params end.to change { subject.idv_session.flow_path - }.from(nil).to('standard') + }.from(nil).to('standard').and change { + subject.idv_session.skip_hybrid_handoff + }.from(nil).to(true) end it 'redirects to hybrid handoff' do diff --git a/spec/controllers/idv/hybrid_handoff_controller_spec.rb b/spec/controllers/idv/hybrid_handoff_controller_spec.rb index 361d1642996..59f63c2fd25 100644 --- a/spec/controllers/idv/hybrid_handoff_controller_spec.rb +++ b/spec/controllers/idv/hybrid_handoff_controller_spec.rb @@ -54,6 +54,8 @@ { step: 'hybrid_handoff', analytics_id: 'Doc Auth', + redo_document_capture: nil, + skip_hybrid_handoff: nil, irs_reproofing: false, }.merge(ab_test_args) end @@ -200,6 +202,8 @@ flow_path: 'hybrid', step: 'hybrid_handoff', analytics_id: 'Doc Auth', + redo_document_capture: nil, + skip_hybrid_handoff: nil, irs_reproofing: false, telephony_response: { errors: {}, @@ -250,6 +254,8 @@ flow_path: 'standard', step: 'hybrid_handoff', analytics_id: 'Doc Auth', + redo_document_capture: nil, + skip_hybrid_handoff: nil, irs_reproofing: false, }.merge(ab_test_args) end diff --git a/spec/controllers/idv/in_person/ssn_controller_spec.rb b/spec/controllers/idv/in_person/ssn_controller_spec.rb index df614b291e9..ae89bc25a9a 100644 --- a/spec/controllers/idv/in_person/ssn_controller_spec.rb +++ b/spec/controllers/idv/in_person/ssn_controller_spec.rb @@ -79,14 +79,8 @@ ) end - it 'adds a session id to flow session' do - get :show - expect(flow_session[:threatmetrix_session_id]).to_not eq(nil) - end - it 'adds a threatmetrix session id to idv_session' do - get :show - expect(subject.idv_session.threatmetrix_session_id).to_not eq(nil) + expect { get :show }.to change { subject.idv_session.threatmetrix_session_id }.from(nil) end context 'with an ssn in session' do @@ -179,14 +173,6 @@ end it 'does not change threatmetrix_session_id when updating ssn' do - flow_session[:pii_from_user][:ssn] = ssn - put :update, params: params - session_id = flow_session[:threatmetrix_session_id] - subject.threatmetrix_view_variables - expect(flow_session[:threatmetrix_session_id]).to eq(session_id) - end - - it 'does not change idv_session threatmetrix_session_id when updating ssn' do flow_session[:pii_from_user][:ssn] = ssn put :update, params: params session_id = subject.idv_session.threatmetrix_session_id diff --git a/spec/controllers/idv/in_person/verify_info_controller_spec.rb b/spec/controllers/idv/in_person/verify_info_controller_spec.rb index db010800968..3cd9a7773dd 100644 --- a/spec/controllers/idv/in_person/verify_info_controller_spec.rb +++ b/spec/controllers/idv/in_person/verify_info_controller_spec.rb @@ -56,7 +56,6 @@ before do stub_analytics stub_attempts_tracker - allow(@analytics).to receive(:track_event) end describe '#show' do @@ -67,8 +66,6 @@ flow_path: 'standard', irs_reproofing: false, step: 'verify', - same_address_as_id: true, - pii_like_keypaths: [[:same_address_as_id], [:state_id, :state_id_jurisdiction]], }.merge(ab_test_args) end @@ -81,7 +78,46 @@ it 'sends analytics_visited event' do get :show - expect(@analytics).to have_received(:track_event).with(analytics_name, analytics_args) + expect(@analytics).to have_logged_event( + 'IdV: doc auth verify visited', + hash_including(**analytics_args, same_address_as_id: true), + ) + end + + context 'when done' do + let(:review_status) { 'review' } + let(:async_state) { instance_double(ProofingSessionAsyncResult) } + let(:adjudicated_result) do + { + context: { + stages: { + threatmetrix: { + transaction_id: 1, + review_status: review_status, + response_body: { + tmx_summary_reason_code: ['Identity_Negative_History'], + }, + }, + }, + }, + errors: {}, + exception: nil, + success: true, + threatmetrix_review_status: review_status, + } + end + it 'logs proofing results with analytics_id' do + allow(controller).to receive(:load_async_state).and_return(async_state) + allow(async_state).to receive(:done?).and_return(true) + allow(async_state).to receive(:result).and_return(adjudicated_result) + + get :show + + expect(@analytics).to have_logged_event( + 'IdV: doc auth verify proofing results', + hash_including(**analytics_args, success: true), + ) + end end end diff --git a/spec/controllers/idv/ssn_controller_spec.rb b/spec/controllers/idv/ssn_controller_spec.rb index 61b109a0b9f..14cb43f5726 100644 --- a/spec/controllers/idv/ssn_controller_spec.rb +++ b/spec/controllers/idv/ssn_controller_spec.rb @@ -88,6 +88,10 @@ ) end + it 'adds a threatmetrix session id to idv_session' do + expect { get :show }.to change { subject.idv_session.threatmetrix_session_id }.from(nil) + end + context 'with an ssn in session' do let(:referer) { idv_document_capture_url } before do @@ -216,27 +220,7 @@ end end - it 'adds a threatmetrix session id to flow session' do - put :update, params: params - subject.threatmetrix_view_variables - expect(flow_session[:threatmetrix_session_id]).to_not eq(nil) - end - - it 'does not change flow_session threatmetrix_session_id when updating ssn' do - flow_session['pii_from_doc'][:ssn] = ssn - put :update, params: params - session_id = flow_session[:threatmetrix_session_id] - subject.threatmetrix_view_variables - expect(flow_session[:threatmetrix_session_id]).to eq(session_id) - end - - it 'adds a threatmetrix session id to idv_session' do - put :update, params: params - subject.threatmetrix_view_variables - expect(subject.idv_session.threatmetrix_session_id).to_not eq(nil) - end - - it 'does not change idv_session threatmetrix_session_id when updating ssn' do + it 'does not change threatmetrix_session_id when updating ssn' do flow_session['pii_from_doc'][:ssn] = ssn put :update, params: params session_id = subject.idv_session.threatmetrix_session_id diff --git a/spec/controllers/idv/verify_info_controller_spec.rb b/spec/controllers/idv/verify_info_controller_spec.rb index b830d7d04f2..a1f41d940d9 100644 --- a/spec/controllers/idv/verify_info_controller_spec.rb +++ b/spec/controllers/idv/verify_info_controller_spec.rb @@ -321,12 +321,12 @@ expect(response).to redirect_to idv_phone_url end - it 'logs an event' do + it 'logs an event with analytics_id set' do put :show expect(@analytics).to have_logged_event( 'IdV: doc auth verify proofing results', - hash_including(success: true), + hash_including(**analytics_args, success: true, analytics_id: 'Doc Auth'), ) end diff --git a/spec/controllers/users/phone_setup_controller_spec.rb b/spec/controllers/users/phone_setup_controller_spec.rb index f4706ef42b3..f65feba8996 100644 --- a/spec/controllers/users/phone_setup_controller_spec.rb +++ b/spec/controllers/users/phone_setup_controller_spec.rb @@ -17,11 +17,14 @@ end context 'when signed in' do - it 'renders the index view' do + let(:user) { build(:user, otp_delivery_preference: 'voice') } + before do stub_analytics - user = build(:user, otp_delivery_preference: 'voice') stub_sign_in_before_2fa(user) + subject.user_session[:mfa_selections] = ['voice'] + end + it 'renders the index view' do expect(@analytics).to receive(:track_event). with('User Registration: phone setup visited', { enabled_mfa_methods_count: 0 }) @@ -37,18 +40,6 @@ end end - context 'when fully registered and signed in' do - it 'redirects to account page' do - stub_analytics - user = build(:user, :with_phone) - stub_sign_in(user) - - get :index - - expect(response).to redirect_to(account_path) - end - end - context 'when fully registered and partially signed in' do it 'redirects to 2FA page' do stub_analytics @@ -152,7 +143,7 @@ expect(response).to redirect_to( otp_send_path( otp_delivery_selection_form: { otp_delivery_preference: 'voice', - otp_make_default_number: nil }, + otp_make_default_number: false }, ), ) @@ -192,7 +183,7 @@ expect(response).to redirect_to( otp_send_path( otp_delivery_selection_form: { otp_delivery_preference: 'sms', - otp_make_default_number: nil }, + otp_make_default_number: false }, ), ) @@ -231,7 +222,7 @@ expect(response).to redirect_to( otp_send_path( otp_delivery_selection_form: { otp_delivery_preference: 'sms', - otp_make_default_number: nil }, + otp_make_default_number: false }, ), ) diff --git a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb index 825c14cddd7..f1234ffd6b6 100644 --- a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb +++ b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb @@ -6,10 +6,12 @@ stub_sign_in_before_2fa stub_analytics - expect(@analytics).to receive(:track_event). - with('User Registration: 2FA Setup visited') - get :index + + expect(@analytics).to have_logged_event( + 'User Registration: 2FA Setup visited', + enabled_mfa_methods_count: 0, + ) end context 'when signed out' do @@ -21,13 +23,17 @@ end context 'when fully authenticated and MFA enabled' do - it 'loads the account page' do - user = build(:user, :fully_registered) + it 'logs the visit event with mfa method count' do + user = build(:user, :with_phone) stub_sign_in(user) + stub_analytics get :index - expect(response).to redirect_to(account_url) + expect(@analytics).to have_logged_event( + 'User Registration: 2FA Setup visited', + enabled_mfa_methods_count: 1, + ) end end diff --git a/spec/features/accessibility/user_pages_spec.rb b/spec/features/accessibility/user_pages_spec.rb index d86cf271a55..5b33372668a 100644 --- a/spec/features/accessibility/user_pages_spec.rb +++ b/spec/features/accessibility/user_pages_spec.rb @@ -138,7 +138,7 @@ scenario 'add phone page' do sign_in_and_2fa_user - visit add_phone_path + visit phone_setup_path expect_page_to_have_no_accessibility_violations(page) end diff --git a/spec/features/event_disavowal_spec.rb b/spec/features/event_disavowal_spec.rb index e837a4c5c13..661bedc7130 100644 --- a/spec/features/event_disavowal_spec.rb +++ b/spec/features/event_disavowal_spec.rb @@ -43,13 +43,13 @@ scenario 'disavowing a phone being added' do sign_in_and_2fa_user(user) - visit add_phone_path + visit phone_setup_path fill_in 'new_phone_form[phone]', with: '202-555-3434' choose 'new_phone_form_otp_delivery_preference_sms' check 'new_phone_form_otp_make_default_number' - click_button t('forms.buttons.continue') + click_button t('forms.buttons.send_one_time_code') submit_prefilled_otp_code(user, 'sms') diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index 469187584a4..b7cf54dcdf1 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -38,28 +38,28 @@ let(:happy_path_events) do { 'IdV: intro visited' => {}, - 'IdV: doc auth welcome visited' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default }, - 'IdV: doc auth welcome submitted' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default }, - 'IdV: doc auth agreement visited' => { step: 'agreement', analytics_id: 'Doc Auth', irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default }, + 'IdV: doc auth welcome visited' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil }, + 'IdV: doc auth welcome submitted' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil }, + 'IdV: doc auth agreement visited' => { step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default }, 'IdV: consent checkbox toggled' => { checked: true }, - 'IdV: doc auth agreement submitted' => { success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default }, - 'IdV: doc auth hybrid handoff visited' => { step: 'hybrid_handoff', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false }, - 'IdV: doc auth hybrid handoff submitted' => { success: true, errors: {}, destination: :document_capture, flow_path: 'standard', step: 'hybrid_handoff', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false }, - 'IdV: doc auth document_capture visited' => { flow_path: 'standard', step: 'document_capture', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false }, + 'IdV: doc auth agreement submitted' => { success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default }, + 'IdV: doc auth hybrid handoff visited' => { step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, + 'IdV: doc auth hybrid handoff submitted' => { success: true, errors: {}, destination: :document_capture, flow_path: 'standard', step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, + 'IdV: doc auth document_capture visited' => { flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'Frontend: IdV: front image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO' }, 'Frontend: IdV: back image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO' }, 'IdV: doc auth image upload form submitted' => { success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String) }, 'IdV: doc auth image upload vendor pii validation' => { success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String) }, - 'IdV: doc auth document_capture submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'document_capture', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false }, - 'IdV: doc auth ssn visited' => { flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false }, - 'IdV: doc auth ssn submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false }, - 'IdV: doc auth verify visited' => { flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false }, - 'IdV: doc auth verify submitted' => { flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false }, - 'IdV: doc auth verify proofing results' => { success: true, errors: {}, address_edited: false, address_line2_present: false, ssn_is_unique: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, + 'IdV: doc auth document_capture submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, + 'IdV: doc auth ssn visited' => { flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, + 'IdV: doc auth ssn submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, + 'IdV: doc auth verify visited' => { flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, + 'IdV: doc auth verify submitted' => { flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, + 'IdV: doc auth verify proofing results' => { success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, irs_reproofing: false, skip_hybrid_handoff: nil, proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', double_address_verification: false, resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired' }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } }, - 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, + 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, - 'IdV: phone confirmation form' => { success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, otp_delivery_preference: 'sms', + 'IdV: phone confirmation form' => { success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, otp_delivery_preference: 'sms', proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, 'IdV: phone confirmation vendor' => { success: true, errors: {}, vendor: { exception: nil, vendor_name: 'AddressMock', transaction_id: 'address-mock-transaction-id-123', timed_out: false, reference: '' }, new_phone_added: false, area_code: '202', country_code: 'US', phone_fingerprint: anything, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, @@ -68,11 +68,11 @@ 'IdV: phone confirmation otp visited' => { proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: phone confirmation otp submitted' => { success: true, code_expired: false, code_matches: true, second_factor_attempts_count: 0, second_factor_locked_at: nil, errors: {}, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, - 'IdV: review info visited' => { address_verification_method: 'phone', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, + 'IdV: review info visited' => { address_verification_method: 'phone', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, - 'IdV: review complete' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, + 'IdV: review complete' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, - 'IdV: final resolution' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, + 'IdV: final resolution' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: personal key visited' => { address_verification_method: 'phone', proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, @@ -86,35 +86,35 @@ let(:gpo_path_events) do { 'IdV: intro visited' => {}, - 'IdV: doc auth welcome visited' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default }, - 'IdV: doc auth welcome submitted' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default }, - 'IdV: doc auth agreement visited' => { step: 'agreement', analytics_id: 'Doc Auth', irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default }, - 'IdV: doc auth agreement submitted' => { success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default }, - 'IdV: doc auth hybrid handoff visited' => { step: 'hybrid_handoff', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false }, - 'IdV: doc auth hybrid handoff submitted' => { success: true, errors: {}, destination: :document_capture, flow_path: 'standard', step: 'hybrid_handoff', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false }, - 'IdV: doc auth document_capture visited' => { flow_path: 'standard', step: 'document_capture', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false }, + 'IdV: doc auth welcome visited' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil }, + 'IdV: doc auth welcome submitted' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil }, + 'IdV: doc auth agreement visited' => { step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default }, + 'IdV: doc auth agreement submitted' => { success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default }, + 'IdV: doc auth hybrid handoff visited' => { step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, + 'IdV: doc auth hybrid handoff submitted' => { success: true, errors: {}, destination: :document_capture, flow_path: 'standard', redo_document_capture: nil, step: 'hybrid_handoff', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, + 'IdV: doc auth document_capture visited' => { flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'Frontend: IdV: front image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO' }, 'Frontend: IdV: back image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO' }, 'IdV: doc auth image upload form submitted' => { success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String) }, 'IdV: doc auth image upload vendor pii validation' => { success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String) }, - 'IdV: doc auth document_capture submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'document_capture', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false }, - 'IdV: doc auth ssn visited' => { flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false }, - 'IdV: doc auth ssn submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false }, - 'IdV: doc auth verify visited' => { flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false }, - 'IdV: doc auth verify submitted' => { flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false }, - 'IdV: doc auth verify proofing results' => { success: true, errors: {}, address_edited: false, address_line2_present: false, ssn_is_unique: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, - proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', double_address_verification: false, should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired' }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } }, - 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, + 'IdV: doc auth document_capture submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, + 'IdV: doc auth ssn visited' => { flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, + 'IdV: doc auth ssn submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, + 'IdV: doc auth verify visited' => { flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, + 'IdV: doc auth verify submitted' => { flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, + 'IdV: doc auth verify proofing results' => { success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, irs_reproofing: false, skip_hybrid_handoff: nil, + proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', double_address_verification: false, resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired' }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } }, + 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, - 'IdV: USPS address letter requested' => { resend: false, phone_step_attempts: 0, first_letter_requested_at: nil, hours_since_first_letter: 0, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, + 'IdV: USPS address letter requested' => { resend: false, phone_step_attempts: 0, first_letter_requested_at: nil, hours_since_first_letter: 0, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, - 'IdV: review info visited' => { address_verification_method: 'gpo', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, + 'IdV: review info visited' => { address_verification_method: 'gpo', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } }, - 'IdV: USPS address letter enqueued' => { enqueued_at: Time.zone.now.utc, resend: false, phone_step_attempts: 0, first_letter_requested_at: Time.zone.now.utc, hours_since_first_letter: 0, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, + 'IdV: USPS address letter enqueued' => { enqueued_at: Time.zone.now.utc, resend: false, phone_step_attempts: 0, first_letter_requested_at: Time.zone.now.utc, hours_since_first_letter: 0, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } }, - 'IdV: review complete' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: true, in_person_verification_pending: false, deactivation_reason: nil, + 'IdV: review complete' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: true, in_person_verification_pending: false, deactivation_reason: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } }, - 'IdV: final resolution' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: true, in_person_verification_pending: false, deactivation_reason: nil, + 'IdV: final resolution' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: true, in_person_verification_pending: false, deactivation_reason: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } }, 'IdV: come back later visited' => { proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } }, } @@ -122,13 +122,13 @@ let(:in_person_path_events) do { - 'IdV: doc auth welcome visited' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default }, - 'IdV: doc auth welcome submitted' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default }, - 'IdV: doc auth agreement visited' => { step: 'agreement', analytics_id: 'Doc Auth', irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default }, - 'IdV: doc auth agreement submitted' => { success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default }, - 'IdV: doc auth hybrid handoff visited' => { step: 'hybrid_handoff', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false }, - 'IdV: doc auth hybrid handoff submitted' => { success: true, errors: {}, destination: :document_capture, flow_path: 'standard', step: 'hybrid_handoff', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false }, - 'IdV: doc auth document_capture visited' => { flow_path: 'standard', step: 'document_capture', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false }, + 'IdV: doc auth welcome visited' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil }, + 'IdV: doc auth welcome submitted' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil }, + 'IdV: doc auth agreement visited' => { step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default }, + 'IdV: doc auth agreement submitted' => { success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default }, + 'IdV: doc auth hybrid handoff visited' => { step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, + 'IdV: doc auth hybrid handoff submitted' => { success: true, errors: {}, destination: :document_capture, flow_path: 'standard', redo_document_capture: nil, step: 'hybrid_handoff', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, + 'IdV: doc auth document_capture visited' => { flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'Frontend: IdV: front image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO' }, 'Frontend: IdV: back image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO' }, 'IdV: doc auth image upload form submitted' => { success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String) }, @@ -142,13 +142,13 @@ 'IdV: in person proofing state_id submitted' => { success: true, flow_path: 'standard', step: 'state_id', step_count: 1, analytics_id: 'In Person Proofing', irs_reproofing: false, errors: {}, same_address_as_id: nil }, 'IdV: in person proofing address visited' => { step: 'address', flow_path: 'standard', step_count: 1, analytics_id: 'In Person Proofing', irs_reproofing: false }, 'IdV: in person proofing address submitted' => { success: true, step: 'address', flow_path: 'standard', step_count: 1, analytics_id: 'In Person Proofing', irs_reproofing: false, errors: {}, same_address_as_id: true }, - 'IdV: doc auth ssn visited' => { analytics_id: 'In Person Proofing', step: 'ssn', flow_path: 'standard', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, acuant_sdk_upgrade_ab_test_bucket: :default, same_address_as_id: true }, - 'IdV: doc auth ssn submitted' => { analytics_id: 'In Person Proofing', success: true, step: 'ssn', flow_path: 'standard', irs_reproofing: false, errors: {}, getting_started_ab_test_bucket: :welcome_default, acuant_sdk_upgrade_ab_test_bucket: :default, same_address_as_id: true }, - 'IdV: doc auth verify visited' => { analytics_id: 'In Person Proofing', step: 'verify', flow_path: 'standard', irs_reproofing: false, same_address_as_id: true, getting_started_ab_test_bucket: :welcome_default, acuant_sdk_upgrade_ab_test_bucket: :default }, - 'IdV: doc auth verify submitted' => { analytics_id: 'In Person Proofing', step: 'verify', flow_path: 'standard', irs_reproofing: false, same_address_as_id: true, getting_started_ab_test_bucket: :welcome_default, acuant_sdk_upgrade_ab_test_bucket: :default }, - 'IdV: doc auth verify proofing results' => { success: true, errors: {}, address_edited: false, address_line2_present: false, ssn_is_unique: true, getting_started_ab_test_bucket: :welcome_default, acuant_sdk_upgrade_ab_test_bucket: :default, + 'IdV: doc auth ssn visited' => { analytics_id: 'In Person Proofing', step: 'ssn', flow_path: 'standard', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, same_address_as_id: true }, + 'IdV: doc auth ssn submitted' => { analytics_id: 'In Person Proofing', success: true, step: 'ssn', flow_path: 'standard', irs_reproofing: false, errors: {}, getting_started_ab_test_bucket: :welcome_default, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, same_address_as_id: true }, + 'IdV: doc auth verify visited' => { analytics_id: 'In Person Proofing', step: 'verify', flow_path: 'standard', irs_reproofing: false, same_address_as_id: true, getting_started_ab_test_bucket: :welcome_default, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil }, + 'IdV: doc auth verify submitted' => { analytics_id: 'In Person Proofing', step: 'verify', flow_path: 'standard', irs_reproofing: false, same_address_as_id: true, getting_started_ab_test_bucket: :welcome_default, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil }, + 'IdV: doc auth verify proofing results' => { success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'In Person Proofing', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, irs_reproofing: false, skip_hybrid_handoff: nil, proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', double_address_verification: false, resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired' }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } }, - 'IdV: phone confirmation form' => { success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, otp_delivery_preference: 'sms', + 'IdV: phone confirmation form' => { success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, otp_delivery_preference: 'sms', proofing_components: { document_check: 'usps', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', source_check: 'aamva' } }, 'IdV: phone confirmation vendor' => { success: true, errors: {}, vendor: { exception: nil, vendor_name: 'AddressMock', transaction_id: 'address-mock-transaction-id-123', timed_out: false, reference: '' }, new_phone_added: false, area_code: '202', country_code: 'US', phone_fingerprint: anything, proofing_components: { address_check: 'lexis_nexis_address', document_check: 'usps', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', source_check: 'aamva' } }, @@ -157,11 +157,11 @@ 'IdV: phone confirmation otp visited' => { proofing_components: { address_check: 'lexis_nexis_address', document_check: 'usps', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', source_check: 'aamva' } }, 'IdV: phone confirmation otp submitted' => { success: true, code_expired: false, code_matches: true, second_factor_attempts_count: 0, second_factor_locked_at: nil, errors: {}, proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, - 'IdV: review info visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, address_verification_method: 'phone', + 'IdV: review info visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, address_verification_method: 'phone', proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, - 'IdV: review complete' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: true, deactivation_reason: 'in_person_verification_pending', + 'IdV: review complete' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: true, deactivation_reason: 'in_person_verification_pending', proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, - 'IdV: final resolution' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: true, deactivation_reason: 'in_person_verification_pending', + 'IdV: final resolution' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: true, deactivation_reason: 'in_person_verification_pending', proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: personal key visited' => { proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' }, address_verification_method: 'phone' }, 'IdV: personal key acknowledgment toggled' => { checked: true, diff --git a/spec/features/idv/doc_auth/verify_info_step_spec.rb b/spec/features/idv/doc_auth/verify_info_step_spec.rb index 287bd42bb9f..4aac0cabf08 100644 --- a/spec/features/idv/doc_auth/verify_info_step_spec.rb +++ b/spec/features/idv/doc_auth/verify_info_step_spec.rb @@ -51,7 +51,11 @@ expect(fake_analytics).to have_logged_event( 'IdV: doc auth verify proofing results', - hash_including(address_edited: true, address_line2_present: true), + hash_including( + address_edited: true, + address_line2_present: true, + analytics_id: 'Doc Auth', + ), ) end diff --git a/spec/features/idv/steps/in_person/verify_info_spec.rb b/spec/features/idv/steps/in_person/verify_info_spec.rb index 9c2f31642fe..e0872eef1cc 100644 --- a/spec/features/idv/steps/in_person/verify_info_spec.rb +++ b/spec/features/idv/steps/in_person/verify_info_spec.rb @@ -5,14 +5,16 @@ include IdvStepHelper include InPersonHelper + let(:user) { user_with_2fa } + let(:fake_analytics) { FakeAnalytics.new(user: user) } + before do allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) + allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics) end it 'provides back buttons for address, state ID, and SSN that discard changes', allow_browser_log: true do - user = user_with_2fa - sign_in_and_2fa_user(user) begin_in_person_proofing(user) complete_prepare_step(user) @@ -72,8 +74,6 @@ it 'returns the user to the verify info page when updates are made', allow_browser_log: true do - user = user_with_2fa - sign_in_and_2fa_user(user) begin_in_person_proofing(user) complete_prepare_step(user) @@ -135,7 +135,6 @@ it 'does not proceed to the next page if resolution fails', allow_browser_log: true do - user = user_with_2fa sign_in_and_2fa_user begin_in_person_proofing(user) @@ -155,7 +154,6 @@ it 'proceeds to the next page if resolution passes', allow_browser_log: true do - user = user_with_2fa sign_in_and_2fa_user begin_in_person_proofing(user) complete_prepare_step(user) @@ -166,5 +164,9 @@ click_idv_continue expect(page).to have_content(t('titles.idv.phone')) + expect(fake_analytics).to have_logged_event( + 'IdV: doc auth verify proofing results', + hash_including(analytics_id: 'In Person Proofing'), + ) end end diff --git a/spec/features/multi_factor_authentication/mfa_cta_spec.rb b/spec/features/multi_factor_authentication/mfa_cta_spec.rb index 3dadce29904..b37dd57847f 100644 --- a/spec/features/multi_factor_authentication/mfa_cta_spec.rb +++ b/spec/features/multi_factor_authentication/mfa_cta_spec.rb @@ -68,7 +68,7 @@ expect(page).to have_current_path(confirm_backup_codes_path) acknowledge_backup_code_confirmation click_link(t('mfa.second_method_warning.link')) - expect(page).to have_current_path(second_mfa_setup_path) + expect(page).to have_current_path(authentication_methods_setup_path) end end end diff --git a/spec/features/openid_connect/openid_connect_spec.rb b/spec/features/openid_connect/openid_connect_spec.rb index f6f6048aec3..ffe97872183 100644 --- a/spec/features/openid_connect/openid_connect_spec.rb +++ b/spec/features/openid_connect/openid_connect_spec.rb @@ -167,6 +167,78 @@ expect(current_url).to start_with('http://localhost:7654/auth/result') expect(page.get_rack_session.keys).to include('sp') end + + context 'when using prompt=login' do + it 'does not show reauthentication notice if user was not actively authenticated' do + service_provider = ServiceProvider.find_by(issuer: OidcAuthHelper::OIDC_IAL1_ISSUER) + + visit_idp_from_ial1_oidc_sp(prompt: 'login') + expect(page).to_not have_content( + strip_tags( + t( + 'account.login.forced_reauthentication_notice_html', + sp_name: service_provider.friendly_name, + ), + ), + ) + end + + it 'does show reauthentication notice if user was actively authenticated' do + service_provider = ServiceProvider.find_by(issuer: OidcAuthHelper::OIDC_IAL1_ISSUER) + user = user_with_2fa + sign_in_user(user) + + visit_idp_from_ial1_oidc_sp(prompt: 'login') + + expect(page).to have_content( + strip_tags( + t( + 'account.login.forced_reauthentication_notice_html', + sp_name: service_provider.friendly_name, + ), + ), + ) + + visit_idp_from_ial1_oidc_sp(prompt: 'login') + + expect(page).to have_content( + strip_tags( + t( + 'account.login.forced_reauthentication_notice_html', + sp_name: service_provider.friendly_name, + ), + ), + ) + end + + it 'does not show reauth notice if most recent request in session was not prompt=login' do + service_provider = ServiceProvider.find_by(issuer: OidcAuthHelper::OIDC_IAL1_ISSUER) + user = user_with_2fa + sign_in_user(user) + + visit_idp_from_ial1_oidc_sp(prompt: 'login') + + expect(page).to have_content( + strip_tags( + t( + 'account.login.forced_reauthentication_notice_html', + sp_name: service_provider.friendly_name, + ), + ), + ) + + visit_idp_from_ial1_oidc_sp(prompt: 'select_account') + + expect(page).to_not have_content( + strip_tags( + t( + 'account.login.forced_reauthentication_notice_html', + sp_name: service_provider.friendly_name, + ), + ), + ) + end + end end context 'when accepting id_token_hint in logout' do diff --git a/spec/features/phone/add_phone_spec.rb b/spec/features/phone/add_phone_spec.rb index a6315d6fb30..ea3895417e0 100644 --- a/spec/features/phone/add_phone_spec.rb +++ b/spec/features/phone/add_phone_spec.rb @@ -11,7 +11,7 @@ click_on t('account.navigation.add_phone_number') end fill_in :new_phone_form_phone, with: phone - click_continue + click_send_one_time_code fill_in_code_with_last_phone_otp click_submit_default @@ -30,7 +30,7 @@ click_on t('account.navigation.add_phone_number') end fill_in :new_phone_form_phone, with: phone - click_continue + click_send_one_time_code fill_in_code_with_last_phone_otp click_submit_default @@ -51,7 +51,7 @@ hidden_select = page.find('[name="new_phone_form[international_code]"]', visible: :hidden) # Required field should prompt as required on submit - click_continue + click_send_one_time_code focused_input = page.find(':focus') expect(focused_input).to match_css('.phone-input__number.usa-input--error') expect(hidden_select.value).to eq('US') @@ -66,7 +66,7 @@ # Invalid number should prompt as invalid on submit fill_in :new_phone_form_phone, with: 'abcd1234' - click_continue + click_send_one_time_code focused_input = page.find(':focus') expect(focused_input).to match_css('.phone-input__number.usa-input--error') expect(hidden_select.value).to eq('US') @@ -102,7 +102,7 @@ expect(page).to_not have_content(t('two_factor_authentication.otp_delivery_preference.title')) expect(hidden_select.value).to eq('LK') fill_in :new_phone_form_phone, with: '+94 071 234 5678' - click_continue + click_send_one_time_code expect(page.find(':focus')).to match_css('.phone-input__number') # Switching to supported country should re-show delivery options, but prompt as invalid number @@ -113,7 +113,7 @@ expect(page).to have_content(t('two_factor_authentication.otp_delivery_preference.title')) expect(page).to have_css('.usa-error-message', text: '', visible: false) expect(hidden_select.value).to eq('US') - click_continue + click_send_one_time_code expect(page.find(':focus')).to match_css('.phone-input__number') expect(page).to have_content(t('errors.messages.invalid_phone_number.us')) @@ -121,7 +121,7 @@ input = fill_in :new_phone_form_phone, with: '+81543543643' expect(input.value).to eq('+81 543543643') expect(hidden_select.value).to eq('JP') - click_continue + click_send_one_time_code expect(page).to have_content(t('components.one_time_code_input.label')) end @@ -157,7 +157,7 @@ phone = phone_configuration.phone.sub(/^\+1\s*/, '').gsub(/\D/, '') fill_in :new_phone_form_phone, with: phone - click_continue + click_send_one_time_code expect(page).to have_content(I18n.t('errors.messages.phone_duplicate')) @@ -179,7 +179,7 @@ click_on t('account.navigation.add_phone_number') end fill_in :new_phone_form_phone, with: telephony_gem_voip_number - click_continue + click_send_one_time_code expect(page).to have_content(t('errors.messages.voip_check_error')) end @@ -218,7 +218,7 @@ # Failing international should display spam protection screen fill_in t('two_factor_authentication.phone_label'), with: '3065550100' fill_in t('components.captcha_submit_button.mock_score_label'), with: '0.5' - click_continue + click_send_one_time_code expect(page).to have_content(t('titles.spam_protection'), wait: 5) click_continue expect(page).to have_content(t('two_factor_authentication.header_text')) @@ -243,7 +243,7 @@ # Passing international should display OTP confirmation fill_in t('two_factor_authentication.phone_label'), with: '3065550100' fill_in t('components.captcha_submit_button.mock_score_label'), with: '0.7' - click_continue + click_send_one_time_code expect(page).to have_content(t('two_factor_authentication.header_text'), wait: 25) visit account_path within('.sidenav') { click_on t('account.navigation.add_phone_number') } @@ -251,7 +251,7 @@ # Failing domestic should display OTP confirmation fill_in t('two_factor_authentication.phone_label'), with: '5135550100' fill_in t('components.captcha_submit_button.mock_score_label'), with: '0.5' - click_continue + click_send_one_time_code expect(page).to have_content(t('two_factor_authentication.header_text'), wait: 5) visit account_path within('.sidenav') { click_on t('account.navigation.add_phone_number') } @@ -259,7 +259,7 @@ # Passing domestic should display OTP confirmation fill_in t('two_factor_authentication.phone_label'), with: '5135550100' fill_in t('components.captcha_submit_button.mock_score_label'), with: '0.7' - click_continue + click_send_one_time_code expect(page).to have_content(t('two_factor_authentication.header_text'), wait: 5) end @@ -272,7 +272,7 @@ click_on t('account.navigation.add_phone_number') end fill_in :new_phone_form_phone, with: phone - click_continue + click_send_one_time_code click_link t('links.cancel') expect(page).to have_current_path(account_path) @@ -290,9 +290,9 @@ end fill_in :new_phone_form_phone, with: phone - click_continue + click_send_one_time_code click_link t('two_factor_authentication.phone_verification.troubleshooting.change_number') - expect(page).to have_current_path(add_phone_path) + expect(page).to have_current_path(phone_setup_path) end end diff --git a/spec/features/phone/confirmation_spec.rb b/spec/features/phone/confirmation_spec.rb index cf3396e32e0..04478724e63 100644 --- a/spec/features/phone/confirmation_spec.rb +++ b/spec/features/phone/confirmation_spec.rb @@ -69,7 +69,7 @@ def visit_otp_confirmation(delivery_method) end fill_in :new_phone_form_phone, with: phone select_phone_delivery_option(delivery_method) - click_continue + click_send_one_time_code end def expect_successful_otp_confirmation(delivery_method) diff --git a/spec/features/phone/default_phone_selection_spec.rb b/spec/features/phone/default_phone_selection_spec.rb index 7239d270390..40340bda191 100644 --- a/spec/features/phone/default_phone_selection_spec.rb +++ b/spec/features/phone/default_phone_selection_spec.rb @@ -33,7 +33,7 @@ enter_phone_number('202-555-3434') check 'new_phone_form_otp_make_default_number' - click_button t('forms.buttons.continue') + click_button t('forms.buttons.send_one_time_code') expect(page).to have_content t( 'instructions.mfa.sms.number_message_html', @@ -61,7 +61,7 @@ new_phone = '202-555-3111' sign_in_visit_add_phone_path(user, phone_config2) fill_in :new_phone_form_phone, with: new_phone - click_continue + click_send_one_time_code fill_in_code_with_last_phone_otp click_submit_default @@ -104,7 +104,7 @@ enter_phone_number('202-555-3434') choose 'new_phone_form_otp_delivery_preference_voice' check 'new_phone_form_otp_make_default_number' - click_button t('forms.buttons.continue') + click_button t('forms.buttons.send_one_time_code') expect(page).to have_content t( 'instructions.mfa.voice.number_message_html', @@ -147,7 +147,7 @@ def sign_in_visit_manage_phone_path(user, phone_config2) def sign_in_visit_add_phone_path(user, phone_config2) sign_in_and_2fa_user(user) - visit add_phone_path(id: phone_config2.id) + visit phone_setup_path(id: phone_config2.id) expect(page).to have_content t('two_factor_authentication.otp_make_default_number.label') end end diff --git a/spec/features/phone/rate_limitting_spec.rb b/spec/features/phone/rate_limitting_spec.rb index 35b83abc602..a89b085ee98 100644 --- a/spec/features/phone/rate_limitting_spec.rb +++ b/spec/features/phone/rate_limitting_spec.rb @@ -30,7 +30,7 @@ def visit_otp_confirmation(delivery_method) end fill_in :new_phone_form_phone, with: phone select_phone_delivery_option(delivery_method) - click_continue + click_send_one_time_code end end end diff --git a/spec/features/remember_device/webauthn_spec.rb b/spec/features/remember_device/webauthn_spec.rb index 7dd1aef60df..3f286587970 100644 --- a/spec/features/remember_device/webauthn_spec.rb +++ b/spec/features/remember_device/webauthn_spec.rb @@ -123,14 +123,14 @@ def remember_device_and_sign_out_user click_link t('mfa.add') - expect(page).to have_current_path(second_mfa_setup_path) + expect(page).to have_current_path(authentication_methods_setup_path) click_2fa_option('phone') click_continue expect(page). - to have_content t('titles.phone_setup') + to have_content t('headings.add_info.phone') expect(current_path).to eq phone_setup_path diff --git a/spec/features/saml/saml_spec.rb b/spec/features/saml/saml_spec.rb index 03809ff85b8..984c6c60a59 100644 --- a/spec/features/saml/saml_spec.rb +++ b/spec/features/saml/saml_spec.rb @@ -257,6 +257,7 @@ expect(sp_return_logs.count).to eq(1) expect(sp_return_logs.first.ial).to eq(2) end + context 'when ForceAuthn = true in SAMLRequest' do let(:saml_request_overrides) do { @@ -276,15 +277,24 @@ scenario 'enforces reauthentication if already signed in' do # start with an active user session + service_provider = ServiceProvider.find_by(issuer: sp1_issuer) sign_in_live_with_2fa(user) # visit from SP with force_authn: true visit_saml_authn_request_url(overrides: saml_request_overrides) expect(page).to have_content( - 'is using Login.gov to allow you to sign in to your account safely and securely.', + t('headings.create_account_with_sp.sp_text', app_name: APP_NAME), ) expect(page).to have_button('Sign in') - + # visit from SP with force_authn: true + expect(page).to have_content( + strip_tags( + t( + 'account.login.forced_reauthentication_notice_html', + sp_name: service_provider.friendly_name, + ), + ), + ) # sign in again fill_in_credentials_and_submit(user.email, user.password) fill_in_code_with_last_phone_otp @@ -309,12 +319,22 @@ end scenario 'enforces reauthentication if already signed in from the same SP' do + service_provider = ServiceProvider.find_by(issuer: sp1_issuer) # first visit from Test SP visit_saml_authn_request_url(overrides: saml_request_overrides) expect(page).to have_content( 'Test SP is using Login.gov to allow you to sign in' \ ' to your account safely and securely.', ) + # does not show reauth notice if user was not logged in + expect(page).to_not have_content( + strip_tags( + t( + 'account.login.forced_reauthentication_notice_html', + sp_name: service_provider.friendly_name, + ), + ), + ) expect(page).to have_button('Sign in') # Log in with Test SP as the SP session fill_in_credentials_and_submit(user.email, user.password) @@ -336,6 +356,14 @@ 'Test SP is using Login.gov to allow you to sign in' \ ' to your account safely and securely.', ) + expect(page).to have_content( + strip_tags( + t( + 'account.login.forced_reauthentication_notice_html', + sp_name: service_provider.friendly_name, + ), + ), + ) expect(page).to have_button('Sign in') # log in for second time @@ -367,15 +395,24 @@ end scenario 'enforces reauthentication when ForceAuthn = true in SAMLRequest' do + service_provider = ServiceProvider.find_by(issuer: SamlAuthHelper::SP_ISSUER) # start with an active user session sign_in_live_with_2fa(user) # visit from SP with force_authn: true visit_saml_authn_request_url(overrides: { force_authn: true }) expect(page).to have_content( - 'is using Login.gov to allow you to sign in to your account safely and securely.', + t('headings.create_account_with_sp.sp_text', app_name: APP_NAME), ) expect(page).to have_button('Sign in') + expect(page).to have_content( + strip_tags( + t( + 'account.login.forced_reauthentication_notice_html', + sp_name: service_provider.friendly_name, + ), + ), + ) # sign in again fill_in_credentials_and_submit(user.email, user.password) @@ -391,6 +428,42 @@ xmldoc.status_code.attribute('Value').value, ).to eq 'urn:oasis:names:tc:SAML:2.0:status:Success' end + + scenario 'does not show reauth notice if most recent request in session was not ForceAuthn' do + service_provider = ServiceProvider.find_by(issuer: SamlAuthHelper::SP_ISSUER) + # start with an active user session + sign_in_live_with_2fa(user) + + # visit from SP with force_authn: true + visit_saml_authn_request_url(overrides: { force_authn: true }) + expect(page).to have_content( + t('headings.create_account_with_sp.sp_text', app_name: APP_NAME), + ) + expect(page).to have_button('Sign in') + expect(page).to have_content( + strip_tags( + t( + 'account.login.forced_reauthentication_notice_html', + sp_name: service_provider.friendly_name, + ), + ), + ) + + visit_saml_authn_request_url + + expect(page).to have_content( + t('headings.create_account_with_sp.sp_text', app_name: APP_NAME), + ) + expect(page).to have_button('Sign in') + expect(page).to_not have_content( + strip_tags( + t( + 'account.login.forced_reauthentication_notice_html', + sp_name: service_provider.friendly_name, + ), + ), + ) + end end end diff --git a/spec/features/two_factor_authentication/change_factor_spec.rb b/spec/features/two_factor_authentication/change_factor_spec.rb index d45ce065333..be135fae9e9 100644 --- a/spec/features/two_factor_authentication/change_factor_spec.rb +++ b/spec/features/two_factor_authentication/change_factor_spec.rb @@ -84,17 +84,17 @@ visit webauthn_setup_path expect(current_path).to eq login_two_factor_options_path - visit add_phone_path + visit phone_setup_path expect(current_path).to eq login_two_factor_options_path find("label[for='two_factor_options_form_selection_sms']").click click_on t('forms.buttons.continue') fill_in_code_with_last_phone_otp click_submit_default - expect(current_path).to eq add_phone_path + expect(current_path).to eq phone_setup_path - visit add_phone_path - expect(current_path).to eq add_phone_path + visit phone_setup_path + expect(current_path).to eq phone_setup_path end end diff --git a/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb b/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb index 39e566c8d4e..1abaa311ddd 100644 --- a/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb +++ b/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb @@ -15,7 +15,7 @@ click_continue expect(page). - to have_content t('titles.phone_setup') + to have_content t('headings.add_info.phone') expect(current_path).to eq phone_setup_path @@ -48,7 +48,7 @@ click_continue expect(page). - to have_content t('titles.phone_setup') + to have_content t('headings.add_info.phone') expect(current_path).to eq phone_setup_path @@ -62,7 +62,7 @@ click_link t('two_factor_authentication.choose_another_option') - expect(page).to have_current_path(second_mfa_setup_path) + expect(page).to have_current_path(authentication_methods_setup_path) select_2fa_option('auth_app') fill_in t('forms.totp_setup.totp_step_1'), with: 'App' @@ -132,7 +132,7 @@ click_continue expect(page). - to have_content t('titles.phone_setup') + to have_content t('headings.add_info.phone') click_continue @@ -168,7 +168,7 @@ expect(page).to have_current_path(auth_method_confirmation_path) click_link t('mfa.add') - expect(page).to have_current_path(second_mfa_setup_path) + expect(page).to have_current_path(authentication_methods_setup_path) click_link t('mfa.skip') expect(page).to have_current_path(account_path) @@ -189,7 +189,7 @@ expect(page).to have_current_path(auth_method_confirmation_path) click_link t('mfa.add') - expect(page).to have_current_path(second_mfa_setup_path) + expect(page).to have_current_path(authentication_methods_setup_path) click_continue expect(page).to have_current_path(account_path) @@ -218,10 +218,10 @@ expect(page).to_not have_button(t('mfa.skip')) click_link t('mfa.add') - expect(page).to have_current_path(second_mfa_setup_path) + expect(page).to have_current_path(authentication_methods_setup_path) click_continue - expect(page).to have_current_path(second_mfa_setup_path) + expect(page).to have_current_path(authentication_methods_setup_path) expect(page).to have_content( t('errors.two_factor_auth_setup.must_select_additional_option'), ) diff --git a/spec/features/two_factor_authentication/sign_in_spec.rb b/spec/features/two_factor_authentication/sign_in_spec.rb index 75141f4bea8..560f65eb5aa 100644 --- a/spec/features/two_factor_authentication/sign_in_spec.rb +++ b/spec/features/two_factor_authentication/sign_in_spec.rb @@ -14,7 +14,7 @@ click_continue expect(page). - to have_content t('titles.phone_setup') + to have_content t('headings.add_info.phone') send_one_time_code_without_entering_phone_number diff --git a/spec/features/users/sign_up_spec.rb b/spec/features/users/sign_up_spec.rb index dec3ac13da7..7cc68677adf 100644 --- a/spec/features/users/sign_up_spec.rb +++ b/spec/features/users/sign_up_spec.rb @@ -197,8 +197,8 @@ def clipboard_text set_up_2fa_with_backup_codes skip_second_mfa_prompt - visit add_phone_path - expect(page).to have_current_path add_phone_path + visit phone_setup_path + expect(page).to have_current_path phone_setup_path end end @@ -419,8 +419,8 @@ def clipboard_text acknowledge_backup_code_confirmation expect(page).to have_current_path account_path - visit add_phone_path - expect(page).to have_current_path add_phone_path + visit phone_setup_path + expect(page).to have_current_path phone_setup_path end describe 'visiting the homepage by clicking the logo image' do diff --git a/spec/forms/webauthn_visit_form_spec.rb b/spec/forms/webauthn_visit_form_spec.rb index bc394a3e0ae..45b4771358d 100644 --- a/spec/forms/webauthn_visit_form_spec.rb +++ b/spec/forms/webauthn_visit_form_spec.rb @@ -168,7 +168,7 @@ context 'with two_factor_enabled and in_mfa_selection_flow' do let(:user) { create(:user, :with_phone) } - it { is_expected.to eq(second_mfa_setup_path) } + it { is_expected.to eq(authentication_methods_setup_path) } end context 'with two_factor_enabled' do diff --git a/spec/jobs/in_person/send_proofing_notification_job_spec.rb b/spec/jobs/in_person/send_proofing_notification_job_spec.rb index 0c111830409..011cd2092b2 100644 --- a/spec/jobs/in_person/send_proofing_notification_job_spec.rb +++ b/spec/jobs/in_person/send_proofing_notification_job_spec.rb @@ -7,18 +7,23 @@ let(:passed_enrollment_without_notification) { create(:in_person_enrollment, :passed) } let(:passed_enrollment) do - enrollment = create(:in_person_enrollment, :passed, :with_notification_phone_configuration) - enrollment.proofed_at = Time.zone.now - 3.days - enrollment + create( + :in_person_enrollment, + :passed, + :with_notification_phone_configuration, + proofed_at: Time.zone.now - 3.days, + ) end let(:failing_enrollment) do - enrollment = create(:in_person_enrollment, :failed, :with_notification_phone_configuration) - enrollment.proofed_at = Time.zone.now - 3.days - enrollment + create( + :in_person_enrollment, + :failed, + :with_notification_phone_configuration, + proofed_at: Time.zone.now - 3.days, + ) end let(:expired_enrollment) do - enrollment = create(:in_person_enrollment, :expired, :with_notification_phone_configuration) - enrollment + create(:in_person_enrollment, :expired, :with_notification_phone_configuration) end let(:sms_success_response) do Telephony::Response.new( @@ -40,6 +45,7 @@ error: Telephony::DailyLimitReachedError.new, ) end + before do ActiveJob::Base.queue_adapter = :test allow(job).to receive(:analytics).and_return(analytics) @@ -62,6 +68,7 @@ expect(analytics).not_to have_logged_event('SendProofingNotificationJob: job completed') end end + context 'job disabled' do let(:in_person_proofing_enabled) { true } let(:in_person_send_proofing_notifications_enabled) { false } @@ -73,9 +80,11 @@ expect(analytics).not_to have_logged_event('SendProofingNotificationJob: job completed') end end + context 'ipp and job enabled' do let(:in_person_proofing_enabled) { true } let(:in_person_send_proofing_notifications_enabled) { true } + context 'enrollment does not exist' do it 'returns without doing anything' do bad_id = (InPersonEnrollment.all.pluck(:id).max || 0) + 1 @@ -84,6 +93,7 @@ expect(analytics).to have_logged_event('SendProofingNotificationJob: job skipped') end end + context 'enrollment has an unsupported status' do it 'returns without doing anything' do job.perform(expired_enrollment.id) @@ -91,6 +101,7 @@ expect(analytics).to have_logged_event('SendProofingNotificationJob: job skipped') end end + context 'without notification phone notification' do it 'returns without doing anything' do job.perform(passed_enrollment_without_notification.id) @@ -98,6 +109,7 @@ expect(analytics).to have_logged_event('SendProofingNotificationJob: job skipped') end end + context 'with notification phone configuration' do it 'sends notification successfully when enrollment is successful and enrollment updated' do allow(Telephony).to receive(:send_notification).and_return(sms_success_response) @@ -116,6 +128,7 @@ expect(passed_enrollment.reload.notification_phone_configuration).to be_nil end end + it 'sends notification successfully when enrollment failed' do allow(Telephony).to receive(:send_notification).and_return(sms_success_response) @@ -131,7 +144,30 @@ expect(failing_enrollment.reload.notification_phone_configuration).to be_nil end end + + it 'sends a message that respects the user email locale preference' do + allow(Telephony).to receive(:send_notification).and_return(sms_success_response) + + passed_enrollment.user.update!(email_language: 'fr') + passed_enrollment.update!(proofed_at: Time.zone.now) + proofed_date = Time.zone.now.strftime('%m/%d/%Y') + phone_number = passed_enrollment.notification_phone_configuration.formatted_phone + + expect(Telephony). + to( + receive(:send_notification). + with( + to: phone_number, + message: "Login.gov: Vous avez tenté de vérifier votre identité dans un bureau " \ + "de poste le #{proofed_date}. Vérifiez votre e-mail pour votre résultat.", + country_code: Phonelib.parse(phone_number).country, + ), + ) + + job.perform(passed_enrollment.id) + end end + context 'when failed to send notification' do it 'logs sms send failure when number is opt out and enrollment not updated' do allow(Telephony).to receive(:send_notification).and_return(sms_opt_out_response) @@ -142,6 +178,7 @@ ) expect(passed_enrollment.reload.notification_sent_at).to be_nil end + it 'logs sms send failure for delivery failure' do allow(Telephony).to receive(:send_notification).and_return(sms_failure_response) @@ -152,7 +189,8 @@ expect(passed_enrollment.reload.notification_sent_at).to be_nil end end - context 'when an exception is raised' do + + context 'when an exception is raised trying to find the enrollment' do it 'logs the exception details' do allow(InPersonEnrollment). to receive(:find_by). @@ -169,6 +207,28 @@ ) end end + + context 'when an exception is raised trying to send the notification' do + let(:exception_message) { 'SMS unsupported' } + + it 'logs the exception details' do + allow(Telephony). + to( + receive(:send_notification). + and_raise(Telephony::SmsUnsupportedError.new(exception_message)), + ) + + job.perform(passed_enrollment.id) + + expect(analytics).to have_logged_event( + 'SendProofingNotificationJob: exception raised', + enrollment_code: passed_enrollment.enrollment_code, + enrollment_id: passed_enrollment.id, + exception_class: 'Telephony::SmsUnsupportedError', + exception_message: exception_message, + ) + end + end end end end diff --git a/spec/lib/reporting/identity_verification_report_spec.rb b/spec/lib/reporting/identity_verification_report_spec.rb index 68c9769b83e..be70d91edea 100644 --- a/spec/lib/reporting/identity_verification_report_spec.rb +++ b/spec/lib/reporting/identity_verification_report_spec.rb @@ -77,6 +77,26 @@ end end + describe '#query' do + context 'with an issuer' do + it 'includes an issuer filter' do + result = subject.query + + expect(result).to include('| filter properties.service_provider = "my:example:issuer"') + end + end + + context 'without an issuer' do + let(:issuer) { nil } + + it 'does not include an issuer filter' do + result = subject.query + + expect(result).to_not include('filter properties.service_provider') + end + end + end + describe '#cloudwatch_client' do let(:opts) { {} } let(:subject) { described_class.new(issuer:, time_range:, **opts) } diff --git a/spec/models/in_person_enrollment_spec.rb b/spec/models/in_person_enrollment_spec.rb index 372604202c0..35848e6d5ab 100644 --- a/spec/models/in_person_enrollment_spec.rb +++ b/spec/models/in_person_enrollment_spec.rb @@ -4,6 +4,8 @@ describe 'Associations' do it { is_expected.to belong_to :user } it { is_expected.to belong_to :profile } + it { is_expected.to belong_to :service_provider } + it { is_expected.to have_one(:notification_phone_configuration).dependent(:destroy) } end describe 'Status' do @@ -66,7 +68,63 @@ end end - describe 'Triggers' do + describe 'Callbacks' do + describe 'when status is updated' do + it 'sets status_updated_at' do + enrollment = create(:in_person_enrollment, :establishing) + freeze_time do + current_time = Time.zone.now + expect(enrollment.status_updated_at).to be_nil + enrollment.update(status: InPersonEnrollment::STATUS_CANCELLED) + expect(enrollment.status_updated_at).to eq(current_time) + end + end + + describe 'enrollment expires or is canceled' do + it 'deletes the notification phone number' do + statuses = [InPersonEnrollment::STATUS_CANCELLED, InPersonEnrollment::STATUS_EXPIRED] + statuses.each do |status| + enrollment = create( + :in_person_enrollment, :pending, :with_notification_phone_configuration + ) + config_id = enrollment.notification_phone_configuration.id + expect(NotificationPhoneConfiguration.find_by({ id: config_id })).to_not be_nil + + enrollment.update(status: status) + enrollment.reload + + expect(enrollment.notification_phone_configuration).to be_nil + expect(NotificationPhoneConfiguration.find_by({ id: config_id })).to be_nil + end + end + end + end + + describe 'when notification_sent_at is updated' do + context 'enrollment has a notification phone configuration' do + let!(:enrollment) do + create(:in_person_enrollment, :passed, :with_notification_phone_configuration) + end + + it 'destroys the notification phone configuration' do + expect(enrollment.notification_phone_configuration).to_not be_nil + + enrollment.update(notification_sent_at: Time.zone.now) + + expect(enrollment.reload.notification_phone_configuration).to be_nil + end + end + + context 'enrollment does not have a notification phone configuration' do + let!(:enrollment) { create(:in_person_enrollment, :passed) } + + it 'does not raise an error' do + expect(enrollment.notification_phone_configuration).to be_nil + expect { enrollment.update!(notification_sent_at: Time.zone.now) }.to_not raise_error + end + end + end + it 'generates a unique ID if one is not provided' do user = create(:user) profile = create(:profile, gpo_verification_pending_at: 1.day.ago, user: user) @@ -86,34 +144,62 @@ expect(enrollment.unique_id).to eq('1234') end + + describe 'setting capture_secondary_id_enabled on creation' do + let(:capture_enabled) { nil } + + before do + allow(IdentityConfig.store). + to( + receive(:in_person_capture_secondary_id_enabled). + and_return(capture_enabled), + ) + end + + context 'feature flag is enabled' do + let(:capture_enabled) { true } + it 'sets capture_secondary_id_enabled to true on the enrollment' do + enrollment = create(:in_person_enrollment, :pending) + expect(enrollment.capture_secondary_id_enabled).to eq(true) + end + end + + context 'feature flag is not enabled' do + let(:capture_enabled) { false } + it 'does not set capture_secondary_id_enabled to true on the enrollment' do + enrollment = create(:in_person_enrollment, :pending) + expect(enrollment.capture_secondary_id_enabled).to eq(false) + end + end + end end - describe 'email_reminders' do + describe 'enrollments that need email reminders' do let(:early_benchmark) { Time.zone.now - 19.days } let(:late_benchmark) { Time.zone.now - 26.days } let(:final_benchmark) { Time.zone.now - 29.days } - let!(:passed_enrollment) { create(:in_person_enrollment, :passed) } - let!(:failing_enrollment) { create(:in_person_enrollment, :failed) } - let!(:expired_enrollment) { create(:in_person_enrollment, :expired) } - # send on days 11-5 - let!(:pending_enrollment_needing_early_reminder) do + # early reminder is sent on days 11-5 + let!(:enrollments_needing_early_reminder) do [ create(:in_person_enrollment, :pending, enrollment_established_at: Time.zone.now - 19.days), create(:in_person_enrollment, :pending, enrollment_established_at: Time.zone.now - 25.days), ] end - # send on days 4 - 2 - let!(:pending_enrollment_needing_late_reminder) do + # late reminder is sent on days 4 - 2 + let!(:enrollments_needing_late_reminder) do [ create(:in_person_enrollment, :pending, enrollment_established_at: Time.zone.now - 26.days), create(:in_person_enrollment, :pending, enrollment_established_at: Time.zone.now - 28.days), ] end - let!(:pending_enrollment) do + let!(:enrollments_needing_no_reminder) do [ + create(:in_person_enrollment, :passed), + create(:in_person_enrollment, :failed), + create(:in_person_enrollment, :expired), create(:in_person_enrollment, :pending, enrollment_established_at: Time.zone.now), create(:in_person_enrollment, :pending, created_at: Time.zone.now), ] @@ -122,8 +208,7 @@ it 'returns pending enrollments that need early reminder' do expect(InPersonEnrollment.count).to eq(9) results = InPersonEnrollment.needs_early_email_reminder(early_benchmark, late_benchmark) - expect(results.length).to eq pending_enrollment_needing_early_reminder.length - expect(results.pluck(:id)).to match_array pending_enrollment_needing_early_reminder.pluck(:id) + expect(results.pluck(:id)).to match_array enrollments_needing_early_reminder.pluck(:id) results.each do |result| expect(result.pending?).to be_truthy expect(result.early_reminder_sent?).to be_falsey @@ -133,8 +218,7 @@ it 'returns pending enrollments that need late reminder' do expect(InPersonEnrollment.count).to eq(9) results = InPersonEnrollment.needs_late_email_reminder(late_benchmark, final_benchmark) - expect(results.length).to eq(2) - expect(results.pluck(:id)).to match_array pending_enrollment_needing_late_reminder.pluck(:id) + expect(results.pluck(:id)).to match_array enrollments_needing_late_reminder.pluck(:id) results.each do |result| expect(result.pending?).to be_truthy expect(result.late_reminder_sent?).to be_falsey @@ -142,40 +226,41 @@ end end - describe 'needs_usps_status_check' do + describe 'enrollments that need a status check' do let(:check_interval) { ...1.hour.ago } let!(:passed_enrollment) { create(:in_person_enrollment, :passed) } - let!(:failing_enrollment) { create(:in_person_enrollment, :failed) } + let!(:failed_enrollment) { create(:in_person_enrollment, :failed) } let!(:expired_enrollment) { create(:in_person_enrollment, :expired) } let!(:checked_pending_enrollment) do create(:in_person_enrollment, :pending, last_batch_claimed_at: Time.zone.now) end - let!(:needy_enrollments) do - [ - create(:in_person_enrollment, :pending), - create(:in_person_enrollment, :pending), - create(:in_person_enrollment, :pending), - create(:in_person_enrollment, :pending), - ] - end + let!(:needy_enrollments) { create_list(:in_person_enrollment, 4, :pending) } - it 'returns only pending enrollments' do + it 'needs_usps_status_check returns only needy enrollments' do expect(InPersonEnrollment.count).to eq(8) results = InPersonEnrollment.needs_usps_status_check(check_interval) - expect(results.length).to eq needy_enrollments.length expect(results.pluck(:id)).to match_array needy_enrollments.pluck(:id) - results.each do |result| - expect(result.pending?).to be_truthy + results.each { |result| expect(result.pending?).to eq(true) } + end + + it 'needs_usps_status_check_batch returns only matching enrollments' do + freeze_time do + batch_at = Time.zone.now + needy_enrollments.first(2).each do |enrollment| + enrollment.update(last_batch_claimed_at: batch_at) + end + results = InPersonEnrollment.needs_usps_status_check_batch(batch_at) + expect(results.pluck(:id)).to match_array needy_enrollments.first(2).pluck(:id) end end it 'indicates whether an enrollment needs a status check' do - expect(passed_enrollment.needs_usps_status_check?(check_interval)).to be_falsey - expect(failing_enrollment.needs_usps_status_check?(check_interval)).to be_falsey - expect(expired_enrollment.needs_usps_status_check?(check_interval)).to be_falsey - expect(checked_pending_enrollment.needs_usps_status_check?(check_interval)).to be_falsey + expect(passed_enrollment.needs_usps_status_check?(check_interval)).to eq(false) + expect(failed_enrollment.needs_usps_status_check?(check_interval)).to eq(false) + expect(expired_enrollment.needs_usps_status_check?(check_interval)).to eq(false) + expect(checked_pending_enrollment.needs_usps_status_check?(check_interval)).to eq(false) needy_enrollments.each do |enrollment| - expect(enrollment.needs_usps_status_check?(check_interval)).to be_truthy + expect(enrollment.needs_usps_status_check?(check_interval)).to eq(true) end end end @@ -185,7 +270,7 @@ let!(:passed_enrollment) do create(:in_person_enrollment, :passed, ready_for_status_check: true) end - let!(:failing_enrollment) do + let!(:failed_enrollment) do create(:in_person_enrollment, :failed, ready_for_status_check: true) end let!(:expired_enrollment) do @@ -193,198 +278,171 @@ end let!(:checked_pending_enrollment) do create( - :in_person_enrollment, :pending, last_batch_claimed_at: Time.zone.now, - ready_for_status_check: true + :in_person_enrollment, + :pending, + last_batch_claimed_at: Time.zone.now, + ready_for_status_check: true, ) end let!(:ready_enrollments) do - [ - create(:in_person_enrollment, :pending, ready_for_status_check: true), - create(:in_person_enrollment, :pending, ready_for_status_check: true), - create(:in_person_enrollment, :pending, ready_for_status_check: true), - create(:in_person_enrollment, :pending, ready_for_status_check: true), - ] + create_list(:in_person_enrollment, 4, :pending, ready_for_status_check: true) end let!(:needy_enrollments) do - [ - create(:in_person_enrollment, :pending, ready_for_status_check: false), - create(:in_person_enrollment, :pending, ready_for_status_check: false), - create(:in_person_enrollment, :pending, ready_for_status_check: false), - create(:in_person_enrollment, :pending, ready_for_status_check: false), - ] + create_list(:in_person_enrollment, 4, :pending, ready_for_status_check: false) end - it 'returns only pending enrollments that are ready for status check' do + it 'needs_status_check_on_ready_enrollments returns only ready pending enrollments' do expect(InPersonEnrollment.count).to eq(12) ready_results = InPersonEnrollment.needs_status_check_on_ready_enrollments(check_interval) - expect(ready_results.length).to eq ready_enrollments.length expect(ready_results.pluck(:id)).to match_array ready_enrollments.pluck(:id) expect(ready_results.pluck(:id)).not_to match_array needy_enrollments.pluck(:id) - ready_results.each do |result| - expect(result.pending?).to be_truthy - end + ready_results.each { |result| expect(result.pending?).to eq(true) } end - it 'indicates whether a ready enrollment needs a status check' do - expect(passed_enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to( - be(false), - ) - expect(failing_enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to( - be(false), - ) - expect(expired_enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to( - be(false), - ) - expect(checked_pending_enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to( - be(false), - ) - needy_enrollments.each do |enrollment| - expect(enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to( - be(false), - ) + it 'needs_status_check_on_ready_enrollment? tells whether an enrollment needs a status check' do + other_enrollments = [ + passed_enrollment, + failed_enrollment, + expired_enrollment, + checked_pending_enrollment, + ] + + (other_enrollments + needy_enrollments).each do |enrollment| + expect(enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to eq(false) end + ready_enrollments.each do |enrollment| - expect(enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to( - be(true), - ) + expect(enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to eq(true) end end - it 'returns only pending enrollments that are not ready for status check' do + it 'needs_status_check_on_waiting_enrollments returns only not ready pending enrollments' do expect(InPersonEnrollment.count).to eq(12) waiting_results = InPersonEnrollment.needs_status_check_on_waiting_enrollments(check_interval) - expect(waiting_results.length).to eq needy_enrollments.length expect(waiting_results.pluck(:id)).to match_array needy_enrollments.pluck(:id) expect(waiting_results.pluck(:id)).not_to match_array ready_enrollments.pluck(:id) - waiting_results.each do |result| - expect(result.pending?).to be_truthy - end + waiting_results.each { |result| expect(result.pending?).to eq(true) } end it 'indicates whether a waiting enrollment needs a status check' do - expect(passed_enrollment.needs_status_check_on_waiting_enrollment?(check_interval)).to( - be(false), - ) - expect( - failing_enrollment.needs_status_check_on_waiting_enrollment?(check_interval), - ).to( - be(false), - ) - expect( - expired_enrollment.needs_status_check_on_waiting_enrollment?(check_interval), - ).to( - be(false), - ) - expect( - checked_pending_enrollment.needs_status_check_on_waiting_enrollment?(check_interval), - ).to( - be(false), - ) - needy_enrollments.each do |enrollment| - expect(enrollment.needs_status_check_on_waiting_enrollment?(check_interval)).to be(true) + other_enrollments = [ + passed_enrollment, + failed_enrollment, + expired_enrollment, + checked_pending_enrollment, + ] + + (other_enrollments + ready_enrollments).each do |enrollment| + expect(enrollment.needs_status_check_on_waiting_enrollment?(check_interval)).to eq(false) end - ready_enrollments.each do |enrollment| - expect(enrollment.needs_status_check_on_waiting_enrollment?(check_interval)).to be(false) + + needy_enrollments.each do |enrollment| + expect(enrollment.needs_status_check_on_waiting_enrollment?(check_interval)).to eq(true) end end end describe 'minutes_since_established' do - let(:enrollment) do - create( - :in_person_enrollment, :passed, enrollment_established_at: Time.zone.now - 2.hours - ) - end - it 'returns number of minutes since enrollment was established' do freeze_time do + enrollment = create( + :in_person_enrollment, + :passed, + enrollment_established_at: Time.zone.now - 2.hours, + ) expect(enrollment.minutes_since_established).to eq 120 end end it 'returns nil if enrollment has not been established' do - enrollment.status = InPersonEnrollment::STATUS_ESTABLISHING - enrollment.enrollment_established_at = nil + enrollment = create(:in_person_enrollment, :establishing, enrollment_established_at: nil) - expect(enrollment.minutes_since_established).to eq(nil) + expect(enrollment.minutes_since_established).to be_nil end end describe 'minutes_since_last_status_check' do - let(:enrollment) do - create( - :in_person_enrollment, :passed, status_check_attempted_at: Time.zone.now - 2.hours - ) - end - it 'returns number of minutes since last status check' do - expect(enrollment.minutes_since_last_status_check).to be_within(0.01).of(120) + freeze_time do + enrollment = create( + :in_person_enrollment, + status_check_attempted_at: Time.zone.now - 2.hours, + ) + expect(enrollment.minutes_since_last_status_check).to eq 120 + end end it 'returns nil if enrollment has not been status-checked' do - enrollment.status_check_attempted_at = nil + enrollment = create(:in_person_enrollment, status_check_attempted_at: nil) - expect(enrollment.minutes_since_last_status_check).to eq(nil) + expect(enrollment.minutes_since_last_status_check).to be_nil end end describe 'minutes_since_last_status_check_completed' do - let(:enrollment) do - create( - :in_person_enrollment, :passed, status_check_completed_at: Time.zone.now - 2.hours - ) - end - it 'returns number of minutes since last status check was completed' do - expect(enrollment.minutes_since_last_status_check_completed).to be_within(0.01).of(120) + freeze_time do + enrollment = create( + :in_person_enrollment, + status_check_completed_at: Time.zone.now - 2.hours, + ) + expect(enrollment.minutes_since_last_status_check_completed).to eq 120 + end end it 'returns nil if enrollment has not completed a status check' do - enrollment.status_check_completed_at = nil + enrollment = create(:in_person_enrollment, status_check_completed_at: nil) - expect(enrollment.minutes_since_last_status_check_completed).to eq(nil) + expect(enrollment.minutes_since_last_status_check_completed).to be_nil end end - describe 'minutes_since_status_updated' do - let(:enrollment) do - enrollment = create(:in_person_enrollment, :passed) - enrollment.status_updated_at = (Time.zone.now - 2.hours) - enrollment - end - + describe 'minutes_since_last_status_update' do it 'returns number of minutes since the status was updated' do - expect(enrollment.minutes_since_last_status_update).to be_within(0.01).of(120) + freeze_time do + enrollment = create(:in_person_enrollment, status_updated_at: Time.zone.now - 2.hours) + expect(enrollment.minutes_since_last_status_update).to eq 120 + end end it 'returns nil if enrollment status has not been updated' do - enrollment.status_updated_at = nil + enrollment = create(:in_person_enrollment, status_updated_at: nil) - expect(enrollment.minutes_since_last_status_update).to eq(nil) + expect(enrollment.minutes_since_last_status_update).to be_nil end end - describe 'when notification_sent_at is updated' do - context 'enrollment has a notification phone configuration' do - let!(:enrollment) do - create(:in_person_enrollment, :passed, :with_notification_phone_configuration) - end - - it 'destroys the notification phone configuration' do - expect(enrollment.notification_phone_configuration).to_not be_nil + describe 'due_date and days_to_due_date' do + let(:validity_in_days) { 10 } + let(:days_ago_established_at) { 7 } - enrollment.update(notification_sent_at: Time.zone.now) + before do + allow(IdentityConfig.store). + to( + receive(:in_person_enrollment_validity_in_days). + and_return(validity_in_days), + ) + end - expect(enrollment.reload.notification_phone_configuration).to be_nil + it 'due_date returns the enrollment expiration date based on when it was established' do + freeze_time do + enrollment = create( + :in_person_enrollment, + enrollment_established_at: days_ago_established_at.days.ago, + ) + expect(enrollment.due_date).to( + eq((validity_in_days - days_ago_established_at).days.from_now), + ) end end - context 'enrollment does not have a notification phone configuration' do - let!(:enrollment) { create(:in_person_enrollment, :passed) } - - it 'does not raise an error' do - expect(enrollment.notification_phone_configuration).to be_nil - expect { enrollment.update!(notification_sent_at: Time.zone.now) }.to_not raise_error + it 'days_to_due_date returns the number of days left until the due date' do + freeze_time do + enrollment = create( + :in_person_enrollment, + enrollment_established_at: days_ago_established_at.days.ago, + ) + expect(enrollment.days_to_due_date).to eq(validity_in_days - days_ago_established_at) end end end @@ -409,7 +467,7 @@ create(:in_person_enrollment, :failed) end - it 'returns true when status of passed/failed/expired and notification configuration' do + it 'returns true when status of passed/failed/expired and with notification configuration' do expect(passed_enrollment.eligible_for_notification?).to eq(true) expect(failed_enrollment.eligible_for_notification?).to eq(true) end @@ -421,47 +479,4 @@ expect(failed_enrollment_without_notification.eligible_for_notification?).to eq(false) end end - - describe 'user cancelling their enrollment' do - context 'user has a notification phone number stored' do - it 'deletes the notification phone number' do - enrollment = create(:in_person_enrollment, :passed, :with_notification_phone_configuration) - config_id = enrollment.notification_phone_configuration.id - expect(NotificationPhoneConfiguration.find_by({ id: config_id })).to_not eq(nil) - - enrollment.cancelled! - enrollment.reload - - expect(enrollment.notification_phone_configuration).to eq(nil) - expect(NotificationPhoneConfiguration.find_by({ id: config_id })).to eq(nil) - end - end - - context 'user has a "nil" for their notification phone number' do - it 'does nothing' do - enrollment = create(:in_person_enrollment, :pending, notification_phone_configuration: nil) - - enrollment.cancelled! - enrollment.reload - - expect(enrollment.notification_phone_configuration).to eq(nil) - end - end - end - - describe 'enrollment expires' do - it 'deletes the notification phone number' do - enrollment = create( - :in_person_enrollment, :establishing, :with_notification_phone_configuration - ) - config_id = enrollment.notification_phone_configuration.id - expect(NotificationPhoneConfiguration.find_by({ id: config_id })).to_not eq(nil) - - enrollment.expired! - enrollment.reload - - expect(enrollment.notification_phone_configuration).to eq(nil) - expect(NotificationPhoneConfiguration.find_by({ id: config_id })).to eq(nil) - end - end end diff --git a/spec/models/profile_spec.rb b/spec/models/profile_spec.rb index 9132aecee56..6115623b71f 100644 --- a/spec/models/profile_spec.rb +++ b/spec/models/profile_spec.rb @@ -230,6 +230,31 @@ expect { profile.decrypt_pii(user.password) }.to raise_error(Encryption::EncryptionError) end + + context 'with aws_kms_multi_region_read_enabled enabled' do + before do + allow(IdentityConfig.store).to receive(:aws_kms_multi_region_read_enabled).and_return(true) + end + + it 'decrypts the PII for users with a multi region ciphertext' do + profile.encrypt_pii(pii, user.password) + + expect(profile.encrypted_pii_multi_region).to_not be_nil + + decrypted_pii = profile.decrypt_pii(user.password) + + expect(decrypted_pii).to eq pii + end + + it 'decrypts the PII for users with only a single region ciphertext' do + profile.encrypt_pii(pii, user.password) + profile.update!(encrypted_pii_multi_region: nil) + + decrypted_pii = profile.decrypt_pii(user.password) + + expect(decrypted_pii).to eq pii + end + end end describe '#recover_pii' do @@ -243,6 +268,37 @@ expect(profile.recover_pii(normalized_personal_key)).to eq pii end + + context 'with aws_kms_multi_region_read_enabled enabled' do + before do + allow(IdentityConfig.store).to receive(:aws_kms_multi_region_read_enabled).and_return(true) + end + + it 'decrypts the PII for users with a multi region ciphertext' do + profile.encrypt_pii(pii, user.password) + personal_key = profile.personal_key + + normalized_personal_key = PersonalKeyGenerator.new(user).normalize(personal_key) + + expect(profile.encrypted_pii_recovery_multi_region).to_not be_nil + + decrypted_pii = profile.recover_pii(normalized_personal_key) + + expect(decrypted_pii).to eq pii + end + + it 'decrypts the PII for users with only a single region ciphertext' do + profile.encrypt_pii(pii, user.password) + profile.update!(encrypted_pii_recovery_multi_region: nil) + personal_key = profile.personal_key + + normalized_personal_key = PersonalKeyGenerator.new(user).normalize(personal_key) + + decrypted_pii = profile.recover_pii(normalized_personal_key) + + expect(decrypted_pii).to eq pii + end + end end describe 'allows only one active Profile per user' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index f76e836ada6..2ac84c3e9e9 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -401,6 +401,38 @@ expect(user.valid_password?('test password')).to eq(true) expect(user.valid_password?('wrong password')).to eq(false) end + + context 'aws_kms_multi_region_read_enabled is set to true' do + before do + allow(IdentityConfig.store).to receive(:aws_kms_multi_region_read_enabled).and_return(true) + end + + it 'validates the password for a user with a multi-region digest' do + user = build(:user, password: 'test password') + + expect(user.encrypted_password_digest_multi_region).to_not be_nil + + expect(user.valid_password?('test password')).to eq(true) + expect(user.valid_password?('wrong password')).to eq(false) + end + + it 'validates the password for a user with a only a single-region digest' do + user = build(:user, password: 'test password') + user.encrypted_password_digest_multi_region = nil + + expect(user.valid_password?('test password')).to eq(true) + expect(user.valid_password?('wrong password')).to eq(false) + end + + it 'validates the password for a user with a only a single-region UAK digest' do + user = build(:user) + user.encrypted_password_digest = Encryption::UakPasswordVerifier.digest('test password') + user.encrypted_password_digest_multi_region = nil + + expect(user.valid_password?('test password')).to eq(true) + expect(user.valid_password?('wrong password')).to eq(false) + end + end end describe '#personal_key=' do @@ -430,6 +462,39 @@ expect(user.valid_personal_key?('test personal key')).to eq(true) expect(user.valid_personal_key?('wrong personal key')).to eq(false) end + + context 'aws_kms_multi_region_read_enabled is set to true' do + before do + allow(IdentityConfig.store).to receive(:aws_kms_multi_region_read_enabled).and_return(true) + end + + it 'validates the personal key for a user with a multi-region digest' do + user = build(:user, personal_key: 'test personal key') + + expect(user.encrypted_recovery_code_digest_multi_region).to_not be_nil + + expect(user.valid_personal_key?('test personal key')).to eq(true) + expect(user.valid_personal_key?('wrong personal key')).to eq(false) + end + + it 'validates the personal key for a user with a only a single-region digest' do + user = build(:user, personal_key: 'test personal key') + user.encrypted_recovery_code_digest_multi_region = nil + + expect(user.valid_personal_key?('test personal key')).to eq(true) + expect(user.valid_personal_key?('wrong personal key')).to eq(false) + end + + it 'validates the personal key for a user with a only a single-region UAK digest' do + user = build(:user) + user.encrypted_recovery_code_digest = + Encryption::UakPasswordVerifier.digest('test personal key') + user.encrypted_recovery_code_digest_multi_region = nil + + expect(user.valid_personal_key?('test personal key')).to eq(true) + expect(user.valid_personal_key?('wrong personal key')).to eq(false) + end + end end describe '#authenticatable_salt' do diff --git a/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb b/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb index 76c76dc30c2..7487d03d421 100644 --- a/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb +++ b/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb @@ -75,7 +75,7 @@ it 'should show an option to change phone number' do expect(presenter.troubleshooting_options).to include( an_object_satisfying do |c| - c.url == add_phone_path && + c.url == phone_setup_path && c.content == t( 'two_factor_authentication.phone_verification.troubleshooting.change_number', ) diff --git a/spec/presenters/two_factor_options_presenter_spec.rb b/spec/presenters/two_factor_options_presenter_spec.rb index d7745b3d655..383fdeb3595 100644 --- a/spec/presenters/two_factor_options_presenter_spec.rb +++ b/spec/presenters/two_factor_options_presenter_spec.rb @@ -4,13 +4,16 @@ include Rails.application.routes.url_helpers include RequestHelper + let(:user) { build(:user) } let(:user_agent) do 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 \ (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36' end + let(:after_mfa_setup_path) { account_path } + let(:show_skip_additional_mfa_link) { true } let(:presenter) do - described_class.new(user_agent: user_agent) + described_class.new(user:, user_agent:, after_mfa_setup_path:, show_skip_additional_mfa_link:) end before do @@ -18,6 +21,12 @@ and_return(false) end + describe '#two_factor_enabled?' do + it 'delegates to mfa_policy' do + expect(presenter).to delegate_method(:two_factor_enabled?).to(:mfa_policy) + end + end + describe '#options' do it 'supplies all the options for a user' do expect(presenter.options.map(&:class)).to eq [ @@ -78,6 +87,24 @@ end end + describe '#skip_path' do + subject(:skip_path) { presenter.skip_path } + + it { expect(skip_path).to be_nil } + + context 'with mfa configured' do + let(:user) { build(:user, :with_phone) } + + it { expect(skip_path).to eq(after_mfa_setup_path) } + + context 'with skip link hidden' do + let(:show_skip_additional_mfa_link) { false } + + it { expect(skip_path).to be_nil } + end + end + end + describe '#show_skip_additional_mfa_link?' do it 'returns true' do expect(presenter.show_skip_additional_mfa_link?).to eq(true) diff --git a/spec/services/encryption/encryptors/pii_encryptor_spec.rb b/spec/services/encryption/encryptors/pii_encryptor_spec.rb index f5d657848aa..c2f442b1465 100644 --- a/spec/services/encryption/encryptors/pii_encryptor_spec.rb +++ b/spec/services/encryption/encryptors/pii_encryptor_spec.rb @@ -108,7 +108,7 @@ expect(scrypt_password).to receive(:digest).and_return(scrypt_digest) expect(SCrypt::Password).to receive(:new).and_return(scrypt_password) - kms_client = subject.send(:single_region_kms_client) + kms_client = subject.send(:multi_region_kms_client) expect(kms_client).to receive(:decrypt). with('kms_ciphertext', { 'context' => 'pii-encryption', 'user_uuid' => 'uuid-123-abc' }). and_return('aes_ciphertext') @@ -131,5 +131,60 @@ expect(result).to eq(plaintext) end + + context 'aws_kms_multi_region_read_enabled is set to true' do + before do + allow(IdentityConfig.store).to receive(:aws_kms_multi_region_read_enabled).and_return(true) + end + + it 'uses the multi-region ciphertext if it is available' do + test_ciphertext_pair = Encryption::RegionalCiphertextPair.new( + single_region_ciphertext: subject.encrypt( + 'single-region-text', user_uuid: '123abc' + ).single_region_ciphertext, + multi_region_ciphertext: subject.encrypt( + 'multi-region-text', user_uuid: '123abc' + ).multi_region_ciphertext, + ) + + result = subject.decrypt(test_ciphertext_pair, user_uuid: '123abc') + + expect(result).to eq('multi-region-text') + end + + it 'uses the single region ciphertext if the multi-region ciphertext is nil' do + test_ciphertext_pair = Encryption::RegionalCiphertextPair.new( + single_region_ciphertext: subject.encrypt( + 'single-region-text', user_uuid: '123abc' + ).single_region_ciphertext, + multi_region_ciphertext: nil, + ) + + result = subject.decrypt(test_ciphertext_pair, user_uuid: '123abc') + + expect(result).to eq('single-region-text') + end + end + + context 'aws_kms_multi_region_read_enabled is set to false' do + before do + allow(IdentityConfig.store).to receive(:aws_kms_multi_region_read_enabled).and_return(false) + end + + it 'uses the single-region ciphertext to decrypt' do + test_ciphertext_pair = Encryption::RegionalCiphertextPair.new( + single_region_ciphertext: subject.encrypt( + 'single-region-text', user_uuid: '123abc' + ).single_region_ciphertext, + multi_region_ciphertext: subject.encrypt( + 'multi-region-text', user_uuid: '123abc' + ).multi_region_ciphertext, + ) + + result = subject.decrypt(test_ciphertext_pair, user_uuid: '123abc') + + expect(result).to eq('single-region-text') + end + end end end diff --git a/spec/services/encryption/password_verifier_spec.rb b/spec/services/encryption/password_verifier_spec.rb index 5ddd973b5ea..14715ef8fed 100644 --- a/spec/services/encryption/password_verifier_spec.rb +++ b/spec/services/encryption/password_verifier_spec.rb @@ -129,6 +129,94 @@ expect(bad_match_result).to eq(false) end + + context 'aws_kms_multi_region_read_enabled is set to true' do + before do + allow(IdentityConfig.store).to receive(:aws_kms_multi_region_read_enabled).and_return(true) + end + + it 'uses the multi-region digest if it is available' do + test_digest_pair = Encryption::RegionalCiphertextPair.new( + single_region_ciphertext: subject.create_digest_pair( + password: 'single-region-password', + user_uuid: user_uuid, + ).single_region_ciphertext, + multi_region_ciphertext: subject.create_digest_pair( + password: 'multi-region-password', + user_uuid: user_uuid, + ).multi_region_ciphertext, + ) + + single_region_result = subject.verify( + password: 'single-region-password', + digest_pair: test_digest_pair, + user_uuid: user_uuid, + ) + multi_region_result = subject.verify( + password: 'multi-region-password', + digest_pair: test_digest_pair, + user_uuid: user_uuid, + ) + + expect(single_region_result).to eq(false) + expect(multi_region_result).to eq(true) + end + + it 'uses the single region digest if the multi-region digest is nil' do + test_digest_pair = subject.create_digest_pair( + password: password, + user_uuid: user_uuid, + ) + test_digest_pair.multi_region_ciphertext = nil + + correct_password_result = subject.verify( + password: password, + digest_pair: test_digest_pair, + user_uuid: user_uuid, + ) + incorrect_password_result = subject.verify( + password: 'this is a fake password lol', + digest_pair: test_digest_pair, + user_uuid: user_uuid, + ) + + expect(correct_password_result).to eq(true) + expect(incorrect_password_result).to eq(false) + end + end + + context 'aws_kms_multi_region_read_enabled is set to false' do + before do + allow(IdentityConfig.store).to receive(:aws_kms_multi_region_read_enabled).and_return(false) + end + + it 'uses the single-region digest to verify' do + test_digest_pair = Encryption::RegionalCiphertextPair.new( + single_region_ciphertext: subject.create_digest_pair( + password: 'single-region-password', + user_uuid: user_uuid, + ).single_region_ciphertext, + multi_region_ciphertext: subject.create_digest_pair( + password: 'multi-region-password', + user_uuid: user_uuid, + ).multi_region_ciphertext, + ) + + single_region_result = subject.verify( + password: 'single-region-password', + digest_pair: test_digest_pair, + user_uuid: user_uuid, + ) + multi_region_result = subject.verify( + password: 'multi-region-password', + digest_pair: test_digest_pair, + user_uuid: user_uuid, + ) + + expect(single_region_result).to eq(true) + expect(multi_region_result).to eq(false) + end + end end describe '#stale_digest?' do diff --git a/spec/services/service_provider_updater_spec.rb b/spec/services/service_provider_updater_spec.rb index 157d873f61f..5706e7b8f56 100644 --- a/spec/services/service_provider_updater_spec.rb +++ b/spec/services/service_provider_updater_spec.rb @@ -27,7 +27,6 @@ block_encryption: 'aes256-cbc', certs: [saml_test_sp_cert], active: true, - native: false, approved: true, help_text: { sign_in: { en: 'A new different sign-in help text' }, @@ -105,7 +104,6 @@ expect(sp.id).to_not eq 0 expect(sp.updated_at).to_not eq friendly_sp[:updated_at] expect(sp.created_at).to_not eq friendly_sp[:created_at] - expect(sp.native).to eq false expect(sp.approved).to eq true expect(sp.help_text['sign_in']).to eq friendly_sp[:help_text][:sign_in]. stringify_keys @@ -129,7 +127,6 @@ expect(sp.id).to eq old_id expect(sp.updated_at).to_not eq friendly_sp[:updated_at] expect(sp.created_at).to_not eq friendly_sp[:created_at] - expect(sp.native).to eq false expect(sp.approved).to eq true expect(sp.help_text['sign_in']).to eq friendly_sp[:help_text][:sign_in]. stringify_keys @@ -184,7 +181,7 @@ end end - context 'a non-native service provider is invalid' do + context 'a service provider is invalid' do let(:dashboard_service_providers) do [ { @@ -200,7 +197,6 @@ block_encryption: 'aes256-cbc', certs: [saml_test_sp_cert], active: true, - native: false, approved: true, redirect_uris: [''], }, @@ -294,25 +290,12 @@ let(:sp) { create(:service_provider, issuer: attributes[:issuer]) } before { attributes[:active] = false } - context 'it is not a native service provider' do - it 'destroys the service_provider' do - subject.run(attributes) - - destroyed_sp = ServiceProvider.find_by(issuer: attributes[:issuer]) - - expect(destroyed_sp).to be nil - end - end - - context 'it is a native service provider' do - before { sp.update!(native: true) } - it 'is not destroyed' do - subject.run(attributes) + it 'destroys the service_provider' do + subject.run(attributes) - destroyed_sp = ServiceProvider.find_by(issuer: attributes[:issuer]) + destroyed_sp = ServiceProvider.find_by(issuer: attributes[:issuer]) - expect(destroyed_sp).to eq sp - end + expect(destroyed_sp).to be nil end end end diff --git a/spec/support/saml_auth_helper.rb b/spec/support/saml_auth_helper.rb index 2fa63db8610..d89bc561cd4 100644 --- a/spec/support/saml_auth_helper.rb +++ b/spec/support/saml_auth_helper.rb @@ -3,6 +3,7 @@ ## GET /api/saml/auth helper methods module SamlAuthHelper PATH_YEAR = '2023' + SP_ISSUER = 'http://localhost:3000' def saml_settings(overrides: {}) settings = OneLogin::RubySaml::Settings.new @@ -16,7 +17,7 @@ def saml_settings(overrides: {}) settings.name_identifier_format = Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT # SP + IdP Settings - settings.issuer = 'http://localhost:3000' + settings.issuer = SP_ISSUER settings.security[:authn_requests_signed] = true settings.security[:logout_requests_signed] = true settings.security[:embed_sign] = true diff --git a/spec/views/idv/welcome/show.html.erb_spec.rb b/spec/views/idv/welcome/show.html.erb_spec.rb index 7f257b51322..550c43ffe75 100644 --- a/spec/views/idv/welcome/show.html.erb_spec.rb +++ b/spec/views/idv/welcome/show.html.erb_spec.rb @@ -83,6 +83,16 @@ expect(rendered).to have_content(@title) expect(rendered).to have_content(t('doc_auth.getting_started.instructions.getting_started')) + expect(rendered).to have_link( + t('doc_auth.info.getting_started_learn_more'), + href: help_center_redirect_path( + category: 'verify-your-identity', + article: 'how-to-verify-your-identity', + flow: :idv, + step: :welcome_new, + location: 'intro_paragraph', + ), + ) expect(rendered).not_to have_link( t('doc_auth.instructions.learn_more'), href: policy_redirect_url(flow: :idv, step: :welcome, location: :footer), diff --git a/spec/views/shared/_cancel_or_back_to_options.html.erb_spec.rb b/spec/views/shared/_cancel_or_back_to_options.html.erb_spec.rb new file mode 100644 index 00000000000..446f3959483 --- /dev/null +++ b/spec/views/shared/_cancel_or_back_to_options.html.erb_spec.rb @@ -0,0 +1,39 @@ +require 'rails_helper' + +RSpec.describe 'shared/_cancel_or_back_to_options.html.erb' do + let(:user) { build(:user) } + let(:in_multi_mfa_selection_flow) { false } + + subject(:rendered) { render } + + before do + allow(view).to receive(:current_user).and_return(user) + allow(view).to receive(:in_multi_mfa_selection_flow?).and_return(in_multi_mfa_selection_flow) + end + + it 'renders link to choose another authentication method' do + expect(rendered).to have_link( + t('two_factor_authentication.choose_another_option'), + href: authentication_methods_setup_path, + ) + end + + context 'with mfa configured' do + let(:user) { build(:user, :with_phone) } + + it 'renders link to cancel and return to account' do + expect(rendered).to have_link(t('links.cancel'), href: account_path) + end + + context 'when in multi mfa selection flow' do + let(:in_multi_mfa_selection_flow) { true } + + it 'renders link to choose another authentication method' do + expect(rendered).to have_link( + t('two_factor_authentication.choose_another_option'), + href: authentication_methods_setup_path, + ) + end + end + end +end diff --git a/spec/views/two_factor_authentication/otp_verification/show.html.erb_spec.rb b/spec/views/two_factor_authentication/otp_verification/show.html.erb_spec.rb index e7303fe16a6..e276bd53324 100644 --- a/spec/views/two_factor_authentication/otp_verification/show.html.erb_spec.rb +++ b/spec/views/two_factor_authentication/otp_verification/show.html.erb_spec.rb @@ -229,7 +229,7 @@ render - expect(rendered).to have_link(t('forms.two_factor.try_again'), href: add_phone_path) + expect(rendered).to have_link(t('forms.two_factor.try_again'), href: phone_setup_path) end end @@ -244,7 +244,7 @@ ) render - expect(rendered).to have_link(t('forms.two_factor.try_again'), href: add_phone_path) + expect(rendered).to have_link(t('forms.two_factor.try_again'), href: phone_setup_path) end end @@ -312,7 +312,7 @@ expect(rendered).to have_link( t('two_factor_authentication.phone_verification.troubleshooting.change_number'), - href: add_phone_path, + href: phone_setup_path, ) end end diff --git a/spec/views/users/two_factor_authentication_setup/index.html.erb_spec.rb b/spec/views/users/two_factor_authentication_setup/index.html.erb_spec.rb index b7ecb9d1972..b6714f86533 100644 --- a/spec/views/users/two_factor_authentication_setup/index.html.erb_spec.rb +++ b/spec/views/users/two_factor_authentication_setup/index.html.erb_spec.rb @@ -3,14 +3,64 @@ RSpec.describe 'users/two_factor_authentication_setup/index.html.erb' do include Devise::Test::ControllerHelpers + let(:user) { build(:user) } + let(:user_agent) { '' } + let(:show_skip_additional_mfa_link) { true } + let(:after_mfa_setup_path) { account_path } subject(:rendered) { render } before do - user = build_stubbed(:user) - @presenter = TwoFactorOptionsPresenter.new(user_agent: '', user: user) + @presenter = TwoFactorOptionsPresenter.new( + user_agent:, + user:, + show_skip_additional_mfa_link:, + after_mfa_setup_path:, + ) @two_factor_options_form = TwoFactorLoginOptionsForm.new(user) end + it 'has link to cancel account creation' do + render + + expect(rendered).to have_css('.page-footer') + expect(rendered).to have_link(t('links.cancel_account_creation'), href: sign_up_cancel_path) + end + + it 'does not list currently configured mfa methods' do + render + + expect(rendered).not_to have_content(t('headings.account.two_factor')) + end + + context 'with configured mfa methods' do + let(:user) { build(:user, :with_phone) } + + it 'lists currently configured mfa methods' do + render + + expect(rendered).to have_content(t('headings.account.two_factor')) + end + + it 'has link to skip additional mfa setup' do + render + + expect(rendered).to have_css('.page-footer') + expect(rendered).to have_link(t('mfa.skip'), href: after_mfa_setup_path) + end + + context 'with skip link hidden' do + let(:show_skip_additional_mfa_link) { false } + + it 'does not have footer link' do + render + + expect(rendered).not_to have_css('.page-footer') + expect(rendered).not_to have_link(t('links.cancel_account_creation')) + expect(rendered).not_to have_link(t('mfa.skip')) + end + end + end + context 'all phone vendor outage' do before do allow_any_instance_of(OutageStatus).to receive(:all_vendor_outage?). diff --git a/yarn.lock b/yarn.lock index ae4673c323d..fcd3b3e641b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4457,10 +4457,10 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -libphonenumber-js@^1.10.39: - version "1.10.39" - resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.39.tgz#12dd512621c9ebb13402a694ac81dc78511cd982" - integrity sha512-iPMM/NbSNIrdwbr94rAOos6krB7snhfzEptmk/DJUtTPs+P9gOhZ1YXVPcRgjpp3jJByclfm/Igvz45spfJK7g== +libphonenumber-js@^1.10.41: + version "1.10.41" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.41.tgz#14b6be5894bed3385808a6a088031b5b8a27c105" + integrity sha512-4rmmF4u4vD3eGNuuCGjCPwRwO+fIuu1WWcS7VwbPTiMFkJd8F02v8o5pY5tlYuMR+xOvJ88mtOHpkm0Tnu2LcQ== lightningcss-darwin-arm64@1.16.1: version "1.16.1"