diff --git a/Makefile b/Makefile index f1ee0b813e3..1fbbab9a121 100644 --- a/Makefile +++ b/Makefile @@ -66,11 +66,13 @@ lint: ## Runs all lint tests @echo "--- erb-lint ---" make lint_erb @echo "--- rubocop ---" + mkdir -p tmp ifdef JUNIT_OUTPUT - bundle exec rubocop --parallel --format progress --format junit --out rubocop.xml --display-only-failed + bundle exec rubocop --parallel --format progress --format junit --out rubocop.xml --display-only-failed --color 2> tmp/rubocop.txt else - bundle exec rubocop --parallel + bundle exec rubocop --parallel --color 2> tmp/rubocop.txt endif + awk 'NF {exit 1}' tmp/rubocop.txt || (printf "Error: Unexpected stderr output from Rubocop\n"; cat tmp/rubocop.txt; exit 1) @echo "--- analytics_events ---" make lint_analytics_events make lint_analytics_events_sorted diff --git a/app/assets/stylesheets/email.css.scss b/app/assets/stylesheets/email.css.scss index 45aaa081097..cc6855cbfcf 100644 --- a/app/assets/stylesheets/email.css.scss +++ b/app/assets/stylesheets/email.css.scss @@ -25,10 +25,6 @@ margin-bottom: 30px; } -.mr-tiny { - margin-right: 4px; -} - .s10 { font-size: 10px; line-height: 10px; @@ -75,48 +71,6 @@ h6 { width: 50%; } -.footer { - background: $secondary-color; - - a { - color: $white; - text-decoration: underline; - } - - p { - color: $white; - padding-top: 0; - } - - .columns { - padding-bottom: 0; - } - - .wrapper-inner { - padding: 25px 0; - } - - .container { - background: transparent; - } -} - -.legal { - background: $body-background; - - a { - color: $dark-gray; - } - - .container { - background: $body-background; - } - - .wrapper-inner { - padding: 15px 0; - } -} - .usa-alert { border-left: units($theme-alert-bar-width) solid; diff --git a/app/controllers/account_reset/cancel_controller.rb b/app/controllers/account_reset/cancel_controller.rb index 66df8081df5..f2224c4e2e9 100644 --- a/app/controllers/account_reset/cancel_controller.rb +++ b/app/controllers/account_reset/cancel_controller.rb @@ -6,7 +6,7 @@ def show return render :show unless token result = AccountReset::ValidateCancelToken.new(token).call - analytics.account_reset_cancel_token_validation(**result.to_h) + analytics.account_reset_cancel_token_validation(**result) if result.success? handle_valid_token @@ -18,7 +18,7 @@ def show def create result = AccountReset::Cancel.new(session[:cancel_token]).call - analytics.account_reset_cancel(**result.to_h) + analytics.account_reset_cancel(**result) if result.success? handle_success diff --git a/app/controllers/account_reset/delete_account_controller.rb b/app/controllers/account_reset/delete_account_controller.rb index 4377dc69bcd..d4e80ee579b 100644 --- a/app/controllers/account_reset/delete_account_controller.rb +++ b/app/controllers/account_reset/delete_account_controller.rb @@ -6,7 +6,7 @@ def show render :show and return unless token result = AccountReset::ValidateGrantedToken.new(token, request, analytics).call - analytics.account_reset_granted_token_validation(**result.to_h) + analytics.account_reset_granted_token_validation(**result) if result.success? handle_valid_token diff --git a/app/controllers/account_reset/request_controller.rb b/app/controllers/account_reset/request_controller.rb index 904716a59c2..dc16c0d3c50 100644 --- a/app/controllers/account_reset/request_controller.rb +++ b/app/controllers/account_reset/request_controller.rb @@ -23,7 +23,7 @@ def create def create_account_reset_request response = AccountReset::CreateRequest.new(current_user, sp_session[:issuer]).call - analytics.account_reset_request(**response.to_h, **analytics_attributes) + analytics.account_reset_request(**response, **analytics_attributes) end def confirm_two_factor_enabled diff --git a/app/controllers/accounts/connected_accounts/selected_email_controller.rb b/app/controllers/accounts/connected_accounts/selected_email_controller.rb index 5a7466decda..9add839cf12 100644 --- a/app/controllers/accounts/connected_accounts/selected_email_controller.rb +++ b/app/controllers/accounts/connected_accounts/selected_email_controller.rb @@ -20,7 +20,7 @@ def update result = @select_email_form.submit(form_params) - analytics.sp_select_email_submitted(**result.to_h) + analytics.sp_select_email_submitted(**result) if result.success? flash[:email_updated_identity_id] = identity.id diff --git a/app/controllers/accounts/personal_keys_controller.rb b/app/controllers/accounts/personal_keys_controller.rb index 9888fd45696..3dbbd850ba7 100644 --- a/app/controllers/accounts/personal_keys_controller.rb +++ b/app/controllers/accounts/personal_keys_controller.rb @@ -19,7 +19,7 @@ def create analytics.profile_personal_key_create create_user_event(:new_personal_key) result = send_new_personal_key_notifications - analytics.profile_personal_key_create_notifications(**result.to_h) + analytics.profile_personal_key_create_notifications(**result) flash[:info] = t('account.personal_key.old_key_will_not_work') redirect_to manage_personal_key_url diff --git a/app/controllers/api/internal/two_factor_authentication/auth_app_controller.rb b/app/controllers/api/internal/two_factor_authentication/auth_app_controller.rb index e2c62d05f74..4fb8f491c94 100644 --- a/app/controllers/api/internal/two_factor_authentication/auth_app_controller.rb +++ b/app/controllers/api/internal/two_factor_authentication/auth_app_controller.rb @@ -19,7 +19,7 @@ def update configuration_id: params[:id], ).submit(name: params[:name]) - analytics.auth_app_update_name_submitted(**result.to_h) + analytics.auth_app_update_name_submitted(**result) if result.success? render json: { success: true } @@ -34,7 +34,7 @@ def destroy configuration_id: params[:id], ).submit - analytics.auth_app_delete_submitted(**result.to_h) + analytics.auth_app_delete_submitted(**result) if result.success? create_user_event(:authenticator_disabled) diff --git a/app/controllers/api/internal/two_factor_authentication/piv_cac_controller.rb b/app/controllers/api/internal/two_factor_authentication/piv_cac_controller.rb index 135cd3ab39e..ca0425b1cf7 100644 --- a/app/controllers/api/internal/two_factor_authentication/piv_cac_controller.rb +++ b/app/controllers/api/internal/two_factor_authentication/piv_cac_controller.rb @@ -20,7 +20,7 @@ def update configuration_id: params[:id], ).submit(name: params[:name]) - analytics.piv_cac_update_name_submitted(**result.to_h) + analytics.piv_cac_update_name_submitted(**result) if result.success? render json: { success: true } @@ -35,7 +35,7 @@ def destroy configuration_id: params[:id], ).submit - analytics.piv_cac_delete_submitted(**result.to_h) + analytics.piv_cac_delete_submitted(**result) if result.success? create_user_event(:piv_cac_disabled) diff --git a/app/controllers/api/internal/two_factor_authentication/webauthn_controller.rb b/app/controllers/api/internal/two_factor_authentication/webauthn_controller.rb index a73bb42d332..d34aef733e6 100644 --- a/app/controllers/api/internal/two_factor_authentication/webauthn_controller.rb +++ b/app/controllers/api/internal/two_factor_authentication/webauthn_controller.rb @@ -19,7 +19,7 @@ def update configuration_id: params[:id], ).submit(name: params[:name]) - analytics.webauthn_update_name_submitted(**result.to_h) + analytics.webauthn_update_name_submitted(**result) if result.success? render json: { success: true } @@ -34,7 +34,7 @@ def destroy configuration_id: params[:id], ).submit - analytics.webauthn_delete_submitted(**result.to_h) + analytics.webauthn_delete_submitted(**result) if result.success? create_user_event(:webauthn_key_removed) diff --git a/app/controllers/concerns/idv/document_capture_concern.rb b/app/controllers/concerns/idv/document_capture_concern.rb index c9e5e7af77f..a92a523599c 100644 --- a/app/controllers/concerns/idv/document_capture_concern.rb +++ b/app/controllers/concerns/idv/document_capture_concern.rb @@ -6,6 +6,18 @@ module DocumentCaptureConcern include DocAuthVendorConcern + def handle_stored_result(user: current_user, store_in_session: true) + if stored_result&.success? && selfie_requirement_met? + save_proofing_components(user) + extract_pii_from_doc(user, store_in_session: store_in_session) + flash[:success] = t('doc_auth.headings.capture_complete') + successful_response + else + extra = { stored_result_present: stored_result.present? } + failure(I18n.t('doc_auth.errors.general.network_error'), extra) + end + end + def save_proofing_components(user) return unless user @@ -50,6 +62,24 @@ def selfie_requirement_met? stored_result.selfie_check_performed? end + def redirect_to_correct_vendor(vendor, in_hybrid_mobile) + expected_doc_auth_vendor = doc_auth_vendor + return if vendor == expected_doc_auth_vendor + return if vendor == Idp::Constants::Vendors::LEXIS_NEXIS && + expected_doc_auth_vendor == Idp::Constants::Vendors::MOCK + + correct_path = case expected_doc_auth_vendor + when Idp::Constants::Vendors::SOCURE + in_hybrid_mobile ? idv_hybrid_mobile_socure_document_capture_path + : idv_socure_document_capture_path + when Idp::Constants::Vendors::LEXIS_NEXIS, Idp::Constants::Vendors::MOCK + in_hybrid_mobile ? idv_hybrid_mobile_document_capture_path + : idv_document_capture_path + end + + redirect_to correct_path + end + private def track_document_issuing_state(user, state) @@ -59,5 +89,10 @@ def track_document_issuing_state(user, state) doc_auth_log.state = state doc_auth_log.save! end + + def cancel_establishing_in_person_enrollments(user: current_user) + UspsInPersonProofing::EnrollmentHelper. + cancel_stale_establishing_enrollments_for_user(user) + end end end diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb index e2608ab854e..78add678c47 100644 --- a/app/controllers/concerns/idv/verify_info_concern.rb +++ b/app/controllers/concerns/idv/verify_info_concern.rb @@ -219,7 +219,7 @@ def async_state_done(current_async_state) flash[:success] = t('doc_auth.forms.doc_success') redirect_to next_step_url end - analytics.idv_doc_auth_verify_proofing_results(**analytics_arguments, **form_response.to_h) + analytics.idv_doc_auth_verify_proofing_results(**analytics_arguments, **form_response) end def next_step_url diff --git a/app/controllers/concerns/mfa_setup_concern.rb b/app/controllers/concerns/mfa_setup_concern.rb index cfd884ef743..6f673afde94 100644 --- a/app/controllers/concerns/mfa_setup_concern.rb +++ b/app/controllers/concerns/mfa_setup_concern.rb @@ -91,6 +91,15 @@ def check_if_possible_piv_user end end + def threatmetrix_attrs + { + user_id: current_user.id, + request_ip: request&.remote_ip, + threatmetrix_session_id: session[:threatmetrix_session_id], + email: EmailContext.new(current_user).last_sign_in_email_address.email, + } + end + private def track_user_registration_mfa_setup_complete_event diff --git a/app/controllers/concerns/two_factor_authenticatable_methods.rb b/app/controllers/concerns/two_factor_authenticatable_methods.rb index 9a3502ae4ef..6701a85a985 100644 --- a/app/controllers/concerns/two_factor_authenticatable_methods.rb +++ b/app/controllers/concerns/two_factor_authenticatable_methods.rb @@ -14,7 +14,7 @@ def auth_methods_session def handle_verification_for_authentication_context(result:, auth_method:, extra_analytics: nil) increment_mfa_selection_attempt_count(auth_method) analytics.multi_factor_auth( - **result.to_h, + **result, multi_factor_auth_method: auth_method, enabled_mfa_methods_count: mfa_context.enabled_mfa_methods_count, new_device: new_device?, diff --git a/app/controllers/concerns/unconfirmed_user_concern.rb b/app/controllers/concerns/unconfirmed_user_concern.rb index 95ee8755bab..a027d47f8c4 100644 --- a/app/controllers/concerns/unconfirmed_user_concern.rb +++ b/app/controllers/concerns/unconfirmed_user_concern.rb @@ -30,12 +30,14 @@ def track_user_already_confirmed_event end def stop_if_invalid_token - result = email_confirmation_token_validator.submit - analytics.user_registration_email_confirmation(**result.to_h) - return if result.success? + return if email_confirmation_token_validator_result.success? process_unsuccessful_confirmation end + def email_confirmation_token_validator_result + @email_confirmation_token_validator_result ||= email_confirmation_token_validator.submit + end + def email_confirmation_token_validator @email_confirmation_token_validator ||= begin EmailConfirmationTokenValidator.new(@email_address, current_user) @@ -44,10 +46,6 @@ def email_confirmation_token_validator def process_valid_confirmation_token @confirmation_token = params[:confirmation_token] - @forbidden_passwords = @user.email_addresses.flat_map do |email_address| - ForbiddenPasswords.new(email_address.email).call - end - flash.now[:success] = t('devise.confirmations.confirmed_but_must_set_password') session[:user_confirmation_token] = @confirmation_token end diff --git a/app/controllers/event_disavowal_controller.rb b/app/controllers/event_disavowal_controller.rb index 0c765c96ff6..32001154b46 100644 --- a/app/controllers/event_disavowal_controller.rb +++ b/app/controllers/event_disavowal_controller.rb @@ -10,13 +10,13 @@ def new success: true, extra: EventDisavowal::BuildDisavowedEventAnalyticsAttributes.call(disavowed_event), ) - analytics.event_disavowal(**result.to_h) + analytics.event_disavowal(**result) @forbidden_passwords = forbidden_passwords end def create result = password_reset_from_disavowal_form.submit(password_reset_params) - analytics.event_disavowal_password_reset(**result.to_h) + analytics.event_disavowal_password_reset(**result) if result.success? handle_successful_password_reset else @@ -50,7 +50,7 @@ def validate_disavowed_event return end - analytics.event_disavowal_token_invalid(**result.to_h) + analytics.event_disavowal_token_invalid(**result) flash[:error] = (result.errors[:event] || result.errors.first.last).first redirect_to root_url end diff --git a/app/controllers/idv/by_mail/enter_code_controller.rb b/app/controllers/idv/by_mail/enter_code_controller.rb index 519f5105c8e..88fe02b3782 100644 --- a/app/controllers/idv/by_mail/enter_code_controller.rb +++ b/app/controllers/idv/by_mail/enter_code_controller.rb @@ -53,7 +53,7 @@ def create @gpo_verify_form = build_gpo_verify_form result = @gpo_verify_form.submit(resolved_authn_context_result.enhanced_ipp?) - analytics.idv_verify_by_mail_enter_code_submitted(**result.to_h) + analytics.idv_verify_by_mail_enter_code_submitted(**result) if !result.success? if rate_limiter.limited? diff --git a/app/controllers/idv/document_capture_controller.rb b/app/controllers/idv/document_capture_controller.rb index df9df2552bd..77599c271c7 100644 --- a/app/controllers/idv/document_capture_controller.rb +++ b/app/controllers/idv/document_capture_controller.rb @@ -12,6 +12,7 @@ class DocumentCaptureController < ApplicationController before_action :confirm_step_allowed, unless: -> { allow_direct_ipp? } before_action :override_csp_to_allow_acuant before_action :set_usps_form_presenter + before_action -> { redirect_to_correct_vendor(Idp::Constants::Vendors::LEXIS_NEXIS, false) } def show analytics.idv_doc_auth_document_capture_visited(**analytics_arguments) @@ -19,12 +20,7 @@ def show Funnel::DocAuth::RegisterStep.new(current_user.id, sp_session[:issuer]). call('document_capture', :view, true) - case doc_auth_vendor - when Idp::Constants::Vendors::SOCURE - redirect_to idv_socure_document_capture_url - when Idp::Constants::Vendors::LEXIS_NEXIS, Idp::Constants::Vendors::MOCK - render :show, locals: extra_view_variables - end + render :show, locals: extra_view_variables end def update @@ -93,11 +89,6 @@ def self.step_info private - def cancel_establishing_in_person_enrollments - UspsInPersonProofing::EnrollmentHelper. - cancel_stale_establishing_enrollments_for_user(current_user) - end - def analytics_arguments { flow_path: flow_path, @@ -110,18 +101,6 @@ def analytics_arguments }.merge(ab_test_analytics_buckets) end - def handle_stored_result - if stored_result&.success? && selfie_requirement_met? - save_proofing_components(current_user) - extract_pii_from_doc(current_user, store_in_session: true) - flash[:success] = t('doc_auth.headings.capture_complete') - successful_response - else - extra = { stored_result_present: stored_result.present? } - failure(I18n.t('doc_auth.errors.general.network_error'), extra) - end - end - def allow_direct_ipp? return false unless idv_session.welcome_visited && idv_session.idv_consent_given? diff --git a/app/controllers/idv/hybrid_mobile/document_capture_controller.rb b/app/controllers/idv/hybrid_mobile/document_capture_controller.rb index e6cf6eb809c..023ce16fa46 100644 --- a/app/controllers/idv/hybrid_mobile/document_capture_controller.rb +++ b/app/controllers/idv/hybrid_mobile/document_capture_controller.rb @@ -11,6 +11,7 @@ class DocumentCaptureController < ApplicationController before_action :override_csp_to_allow_acuant before_action :confirm_document_capture_needed, only: :show before_action :set_usps_form_presenter + before_action -> { redirect_to_correct_vendor(Idp::Constants::Vendors::LEXIS_NEXIS, true) } def show analytics.idv_doc_auth_document_capture_visited(**analytics_arguments) @@ -23,7 +24,10 @@ def show def update document_capture_session.confirm_ocr - result = handle_stored_result + result = handle_stored_result( + user: document_capture_user, + store_in_session: false, + ) analytics.idv_doc_auth_document_capture_submitted(**result.to_h.merge(analytics_arguments)) @@ -32,7 +36,6 @@ def update # rate limiting redirect is in ImageUploadResponsePresenter if result.success? - flash[:success] = t('doc_auth.headings.capture_complete') redirect_to idv_hybrid_mobile_capture_complete_url else redirect_to idv_hybrid_mobile_document_capture_url @@ -65,17 +68,6 @@ def analytics_arguments ) end - def handle_stored_result - if stored_result&.success? && selfie_requirement_met? - save_proofing_components(document_capture_user) - extract_pii_from_doc(document_capture_user) - successful_response - else - extra = { stored_result_present: stored_result.present? } - failure(I18n.t('doc_auth.errors.general.network_error'), extra) - end - end - def confirm_document_capture_needed return unless stored_result&.success? return if redo_document_capture_pending? diff --git a/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb b/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb index 93ec7181d37..4cbf8f44fdf 100644 --- a/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb +++ b/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb @@ -11,6 +11,7 @@ class DocumentCaptureController < ApplicationController check_or_render_not_found -> { IdentityConfig.store.socure_enabled } before_action :check_valid_document_capture_session, except: [:update] + before_action -> { redirect_to_correct_vendor(Idp::Constants::Vendors::SOCURE, true) } def show Funnel::DocAuth::RegisterStep.new(document_capture_user.id, sp_session[:issuer]). @@ -38,8 +39,11 @@ def show :data, :docvTransactionToken, ) + document_capture_session.socure_docv_capture_app_url = document_response.dig( + :data, + :url, + ) document_capture_session.save - # useful for analytics @msg = document_response[:msg] @reference_id = document_response[:referenceId] diff --git a/app/controllers/idv/in_person/address_controller.rb b/app/controllers/idv/in_person/address_controller.rb index b289c440ed7..20ed3a563a2 100644 --- a/app/controllers/idv/in_person/address_controller.rb +++ b/app/controllers/idv/in_person/address_controller.rb @@ -25,7 +25,7 @@ def update form_result = form.submit(flow_params) analytics.idv_in_person_proofing_residential_address_submitted( - **analytics_arguments.merge(**form_result.to_h), + **analytics_arguments.merge(**form_result), ) if form_result.success? diff --git a/app/controllers/idv/in_person/state_id_controller.rb b/app/controllers/idv/in_person/state_id_controller.rb index ad9bc723cee..8ea1678d6dc 100644 --- a/app/controllers/idv/in_person/state_id_controller.rb +++ b/app/controllers/idv/in_person/state_id_controller.rb @@ -31,7 +31,7 @@ def update end analytics.idv_in_person_proofing_state_id_submitted( - **analytics_arguments.merge(**form_result.to_h), + **analytics_arguments.merge(**form_result), ) # Accept Date of Birth from both memorable date and input date components formatted_dob = MemorableDateComponent.extract_date_param flow_params&.[](:dob) diff --git a/app/controllers/idv/otp_verification_controller.rb b/app/controllers/idv/otp_verification_controller.rb index 8132aff6da8..68eb539ac1e 100644 --- a/app/controllers/idv/otp_verification_controller.rb +++ b/app/controllers/idv/otp_verification_controller.rb @@ -22,7 +22,7 @@ def show def update clear_future_steps! result = phone_confirmation_otp_verification_form.submit(code: params[:code]) - analytics.idv_phone_confirmation_otp_submitted(**result.to_h, **ab_test_analytics_buckets) + analytics.idv_phone_confirmation_otp_submitted(**result, **ab_test_analytics_buckets) if result.success? idv_session.mark_phone_step_complete! diff --git a/app/controllers/idv/phone_controller.rb b/app/controllers/idv/phone_controller.rb index f6be81bd270..5665976b566 100644 --- a/app/controllers/idv/phone_controller.rb +++ b/app/controllers/idv/phone_controller.rb @@ -55,7 +55,7 @@ def create Funnel::DocAuth::RegisterStep.new(current_user.id, current_sp&.issuer). call(:verify_phone, :update, result.success?) - analytics.idv_phone_confirmation_form_submitted(**result.to_h, **ab_test_analytics_buckets) + analytics.idv_phone_confirmation_form_submitted(**result, **ab_test_analytics_buckets) if result.success? submit_proofing_attempt redirect_to idv_phone_path diff --git a/app/controllers/idv/resend_otp_controller.rb b/app/controllers/idv/resend_otp_controller.rb index d33f42b79da..5e2b5dde4ed 100644 --- a/app/controllers/idv/resend_otp_controller.rb +++ b/app/controllers/idv/resend_otp_controller.rb @@ -13,7 +13,7 @@ class ResendOtpController < ApplicationController def create result = send_phone_confirmation_otp - analytics.idv_phone_confirmation_otp_resent(**result.to_h) + analytics.idv_phone_confirmation_otp_resent(**result) if result.success? redirect_to idv_otp_verification_url else diff --git a/app/controllers/idv/socure/document_capture_controller.rb b/app/controllers/idv/socure/document_capture_controller.rb index 603355614c5..805b92c3851 100644 --- a/app/controllers/idv/socure/document_capture_controller.rb +++ b/app/controllers/idv/socure/document_capture_controller.rb @@ -11,6 +11,7 @@ class DocumentCaptureController < ApplicationController check_or_render_not_found -> { IdentityConfig.store.socure_enabled } before_action :confirm_not_rate_limited before_action :confirm_step_allowed + before_action -> { redirect_to_correct_vendor(Idp::Constants::Vendors::SOCURE, false) } # reconsider and maybe remove these when implementing the real # update handler @@ -27,7 +28,7 @@ def show # document request document_request = DocAuth::Socure::Requests::DocumentRequest.new( document_capture_session_uuid: document_capture_session_uuid, - redirect_url: idv_socure_document_capture_url, + redirect_url: idv_socure_document_capture_update_url, language: I18n.locale, ) @@ -48,6 +49,10 @@ def show :data, :docvTransactionToken, ) + document_capture_session.socure_docv_capture_app_url = document_response.dig( + :data, + :url, + ) document_capture_session.save # useful for analytics @@ -56,7 +61,25 @@ def show end def update - render plain: 'stub to ensure Socure callback exists and the route works' + clear_future_steps! + idv_session.redo_document_capture = nil # done with this redo + # Not used in standard flow, here for data consistency with hybrid flow. + document_capture_session.confirm_ocr + + result = handle_stored_result + # TODO: new analytics event? + analytics.idv_doc_auth_document_capture_submitted(**result.to_h.merge(analytics_arguments)) + + Funnel::DocAuth::RegisterStep.new(current_user.id, sp_session[:issuer]). + call('socure_document_capture', :update, true) + + cancel_establishing_in_person_enrollments + + if result.success? + redirect_to idv_ssn_url + else + redirect_to idv_socure_document_capture_url + end end def self.step_info @@ -65,22 +88,35 @@ def self.step_info controller: self, next_steps: [:ssn, :ipp_ssn], preconditions: ->(idv_session:, user:) { - idv_session.flow_path == 'standard' && ( - # mobile - idv_session.skip_doc_auth_from_handoff || - idv_session.skip_hybrid_handoff || - idv_session.skip_doc_auth || - idv_session.skip_doc_auth_from_how_to_verify || - !idv_session.selfie_check_required || - idv_session.desktop_selfie_test_mode_enabled? - ) - }, + idv_session.flow_path == 'standard' && ( + # mobile + idv_session.skip_doc_auth_from_handoff || + idv_session.skip_hybrid_handoff || + idv_session.skip_doc_auth || + idv_session.skip_doc_auth_from_how_to_verify || + !idv_session.selfie_check_required || + idv_session.desktop_selfie_test_mode_enabled?) + }, undo_step: ->(idv_session:, user:) do idv_session.pii_from_doc = nil idv_session.invalidate_in_person_pii_from_user! end, ) end + + private + + def analytics_arguments + { + flow_path: flow_path, + step: 'socure_document_capture', + analytics_id: 'Doc Auth', + redo_document_capture: idv_session.redo_document_capture, + skip_hybrid_handoff: idv_session.skip_hybrid_handoff, + liveness_checking_required: resolved_authn_context_result.facial_match?, + selfie_check_required: resolved_authn_context_result.facial_match?, + }.merge(ab_test_analytics_buckets) + end end end end diff --git a/app/controllers/openid_connect/user_info_controller.rb b/app/controllers/openid_connect/user_info_controller.rb index 3e8b5f5d6d7..a1df4ba00f3 100644 --- a/app/controllers/openid_connect/user_info_controller.rb +++ b/app/controllers/openid_connect/user_info_controller.rb @@ -18,7 +18,7 @@ def show def authenticate_identity_via_bearer_token verifier = AccessTokenVerifier.new(request.env['HTTP_AUTHORIZATION']) response, identity = verifier.submit - analytics.openid_connect_bearer_token(**response.to_h) + analytics.openid_connect_bearer_token(**response) if response.success? @current_identity = identity diff --git a/app/controllers/risc/security_events_controller.rb b/app/controllers/risc/security_events_controller.rb index 15dc5bb0024..12a037a52ca 100644 --- a/app/controllers/risc/security_events_controller.rb +++ b/app/controllers/risc/security_events_controller.rb @@ -11,7 +11,7 @@ def create form = SecurityEventForm.new(body: request.body.read) result = form.submit - analytics.security_event_received(**result.to_h) + analytics.security_event_received(**result) if result.success? head :accepted diff --git a/app/controllers/sign_up/completions_controller.rb b/app/controllers/sign_up/completions_controller.rb index dd9800dd9e2..76fb9b252d4 100644 --- a/app/controllers/sign_up/completions_controller.rb +++ b/app/controllers/sign_up/completions_controller.rb @@ -102,7 +102,6 @@ def analytics_attributes(page_occurence) if page_occurence.present? && DisposableEmailDomain.disposable?(email_domain) attributes[:disposable_email_domain] = email_domain end - attributes end diff --git a/app/controllers/sign_up/email_confirmations_controller.rb b/app/controllers/sign_up/email_confirmations_controller.rb index 545be42ee5d..972d73c959c 100644 --- a/app/controllers/sign_up/email_confirmations_controller.rb +++ b/app/controllers/sign_up/email_confirmations_controller.rb @@ -7,6 +7,7 @@ class EmailConfirmationsController < ApplicationController before_action :find_user_with_confirmation_token before_action :confirm_user_needs_sign_up_confirmation + before_action :log_validator_result before_action :stop_if_invalid_token before_action :store_sp_metadata_in_session, only: [:create] @@ -19,6 +20,10 @@ def create private + def log_validator_result + analytics.user_registration_email_confirmation(**email_confirmation_token_validator_result) + end + def clear_setup_piv_cac_from_sign_in session.delete(:needs_to_setup_piv_cac_after_sign_in) end diff --git a/app/controllers/sign_up/passwords_controller.rb b/app/controllers/sign_up/passwords_controller.rb index 51312a862f4..cdfc07e9169 100644 --- a/app/controllers/sign_up/passwords_controller.rb +++ b/app/controllers/sign_up/passwords_controller.rb @@ -11,7 +11,9 @@ class PasswordsController < ApplicationController def new password_form # Memoize the password form to use in the view - process_successful_confirmation + process_valid_confirmation_token + flash.now[:success] = t('devise.confirmations.confirmed_but_must_set_password') + @forbidden_passwords = forbidden_passwords end def create @@ -28,21 +30,14 @@ def create private - def process_successful_confirmation - process_valid_confirmation_token - render_page - end - - def render_page - render( - :new, - locals: { confirmation_token: @confirmation_token }, - formats: :html, - ) + def forbidden_passwords + @user.email_addresses.flat_map do |email_address| + ForbiddenPasswords.new(email_address.email).call + end end def track_analytics(result) - analytics.password_creation(**result.to_h) + analytics.password_creation(**result) end def permitted_params diff --git a/app/controllers/sign_up/registrations_controller.rb b/app/controllers/sign_up/registrations_controller.rb index 6d273eda3d7..a0dd702f040 100644 --- a/app/controllers/sign_up/registrations_controller.rb +++ b/app/controllers/sign_up/registrations_controller.rb @@ -24,12 +24,12 @@ def create result = @register_user_email_form.submit(permitted_params.merge(request_id:)) - analytics.user_registration_email(**result.to_h) + analytics.user_registration_email(**result) if result.success? process_successful_creation else - render :new + render :new, locals: threatmetrix_variables end end @@ -54,7 +54,6 @@ def process_successful_creation session[:email] = @register_user_email_form.email session[:terms_accepted] = @register_user_email_form.terms_accepted session[:sign_in_flow] = :create_account - redirect_to sign_up_verify_email_url(resend: resend_confirmation) end diff --git a/app/controllers/sign_up/select_email_controller.rb b/app/controllers/sign_up/select_email_controller.rb index c98f451027e..41bc0258206 100644 --- a/app/controllers/sign_up/select_email_controller.rb +++ b/app/controllers/sign_up/select_email_controller.rb @@ -7,6 +7,7 @@ class SelectEmailController < ApplicationController check_or_render_not_found -> { IdentityConfig.store.feature_select_email_to_share_enabled } before_action :confirm_two_factor_authenticated before_action :verify_needs_completions_screen + before_action :verify_multiple_emails def show @sp_name = current_sp.friendly_name || sp.agency&.name @@ -21,7 +22,7 @@ def create result = @select_email_form.submit(form_params) - analytics.sp_select_email_submitted(**result.to_h, needs_completion_screen_reason:) + analytics.sp_select_email_submitted(**result, needs_completion_screen_reason:) if result.success? user_session[:selected_email_id_for_linked_identity] = form_params[:selected_email_id] @@ -54,6 +55,10 @@ def last_email end end + def verify_multiple_emails + redirect_to sign_up_completed_path if user_emails.count < 2 + end + def verify_needs_completions_screen redirect_to account_url unless needs_completion_screen_reason end diff --git a/app/controllers/socure_webhook_controller.rb b/app/controllers/socure_webhook_controller.rb index 4d1ca45a362..a4ba596cae3 100644 --- a/app/controllers/socure_webhook_controller.rb +++ b/app/controllers/socure_webhook_controller.rb @@ -21,11 +21,23 @@ def create private + def process_webhook_event + case event[:eventType] + when 'DOCUMENTS_UPLOADED' + increment_rate_limiter + fetch_results + end + end + def fetch_results dcs = document_capture_session raise 'DocumentCaptureSession not found' if dcs.blank? - SocureDocvResultsJob.perform_later(document_capture_session_uuid: dcs.uuid) + if IdentityConfig.store.ruby_workers_idv_enabled + SocureDocvResultsJob.perform_later(document_capture_session_uuid: dcs.uuid) + else + SocureDocvResultsJob.perform_now(document_capture_session_uuid: dcs.uuid) + end end def check_token @@ -74,14 +86,6 @@ def log_webhook_receipt ) end - def process_webhook_event - case event[:eventType] - when 'DOCUMENTS_UPLOADED' - increment_rate_limiter - fetch_results - end - end - def increment_rate_limiter if document_capture_session.present? rate_limiter.increment! @@ -90,8 +94,9 @@ def increment_rate_limiter end def document_capture_session + token = event[:docvTransactionToken] || event[:docVTransactionToken] @document_capture_session ||= DocumentCaptureSession.find_by( - socure_docv_transaction_token: event[:docvTransactionToken], + socure_docv_transaction_token: token, ) end @@ -109,7 +114,7 @@ def rate_limiter def socure_params params.permit( event: [:created, :customerUserId, :eventType, :referenceId, - :docvTransactionToken], + :docvTransactionToken, :docVTransactionToken], ) end end diff --git a/app/controllers/two_factor_authentication/options_controller.rb b/app/controllers/two_factor_authentication/options_controller.rb index 9d74854fad7..1871073aa52 100644 --- a/app/controllers/two_factor_authentication/options_controller.rb +++ b/app/controllers/two_factor_authentication/options_controller.rb @@ -34,7 +34,7 @@ def index def create @two_factor_options_form = TwoFactorLoginOptionsForm.new(current_user) result = @two_factor_options_form.submit(two_factor_options_form_params) - analytics.multi_factor_auth_option_list(**result.to_h) + analytics.multi_factor_auth_option_list(**result) if result.success? process_valid_form diff --git a/app/controllers/two_factor_authentication/otp_verification_controller.rb b/app/controllers/two_factor_authentication/otp_verification_controller.rb index dee0a141549..4c11f1908c4 100644 --- a/app/controllers/two_factor_authentication/otp_verification_controller.rb +++ b/app/controllers/two_factor_authentication/otp_verification_controller.rb @@ -90,7 +90,7 @@ def track_mfa_added reason: RecaptchaAnnotator::AnnotationReasons::PASSED_TWO_FACTOR, ), ) - Funnel::Registration::AddMfa.call(current_user.id, 'phone', analytics) + Funnel::Registration::AddMfa.call(current_user.id, 'phone', analytics, threatmetrix_attrs) end def confirm_multiple_factors_enabled diff --git a/app/controllers/two_factor_authentication/personal_key_verification_controller.rb b/app/controllers/two_factor_authentication/personal_key_verification_controller.rb index 88e4ff4b2f3..0a87a1dbb43 100644 --- a/app/controllers/two_factor_authentication/personal_key_verification_controller.rb +++ b/app/controllers/two_factor_authentication/personal_key_verification_controller.rb @@ -61,7 +61,7 @@ def handle_result(result) def alert_user_about_personal_key_sign_in(disavowal_token) response = UserAlerts::AlertUserAboutPersonalKeySignIn.call(current_user, disavowal_token) - analytics.personal_key_alert_about_sign_in(**response.to_h) + analytics.personal_key_alert_about_sign_in(**response) end def remove_personal_key diff --git a/app/controllers/users/auth_app_controller.rb b/app/controllers/users/auth_app_controller.rb index 729ca14469d..34b07987fe5 100644 --- a/app/controllers/users/auth_app_controller.rb +++ b/app/controllers/users/auth_app_controller.rb @@ -14,7 +14,7 @@ def edit; end def update result = form.submit(name: params.dig(:form, :name)) - analytics.auth_app_update_name_submitted(**result.to_h) + analytics.auth_app_update_name_submitted(**result) if result.success? flash[:success] = t('two_factor_authentication.auth_app.renamed') @@ -28,7 +28,7 @@ def update def destroy result = form.submit - analytics.auth_app_delete_submitted(**result.to_h) + analytics.auth_app_delete_submitted(**result) if result.success? flash[:success] = t('two_factor_authentication.auth_app.deleted') diff --git a/app/controllers/users/backup_code_setup_controller.rb b/app/controllers/users/backup_code_setup_controller.rb index f5d8cc9f696..f0ea7cc2ced 100644 --- a/app/controllers/users/backup_code_setup_controller.rb +++ b/app/controllers/users/backup_code_setup_controller.rb @@ -97,7 +97,12 @@ def track_backup_codes_created enabled_mfa_methods_count: mfa_user.enabled_mfa_methods_count, in_account_creation_flow: in_account_creation_flow?, ) - Funnel::Registration::AddMfa.call(current_user.id, 'backup_codes', analytics) + Funnel::Registration::AddMfa.call( + current_user.id, + 'backup_codes', + analytics, + threatmetrix_attrs, + ) end def mfa_user diff --git a/app/controllers/users/edit_phone_controller.rb b/app/controllers/users/edit_phone_controller.rb index 85eb00f8b3b..9642cece9e4 100644 --- a/app/controllers/users/edit_phone_controller.rb +++ b/app/controllers/users/edit_phone_controller.rb @@ -18,7 +18,7 @@ def edit def update @edit_phone_form = EditPhoneForm.new(current_user, phone_configuration) result = @edit_phone_form.submit(edit_phone_params) - analytics.phone_change_submitted(**result.to_h) + analytics.phone_change_submitted(**result) if result.success? redirect_to account_url else diff --git a/app/controllers/users/email_confirmations_controller.rb b/app/controllers/users/email_confirmations_controller.rb index 7e7e87e9f8b..0f6d468ffa3 100644 --- a/app/controllers/users/email_confirmations_controller.rb +++ b/app/controllers/users/email_confirmations_controller.rb @@ -4,7 +4,7 @@ module Users class EmailConfirmationsController < ApplicationController def create result = email_confirmation_token_validator.submit - analytics.add_email_confirmation(**result.to_h) + analytics.add_email_confirmation(**result) if result.success? process_successful_confirmation(email_address) else @@ -45,10 +45,14 @@ def process_successful_confirmation(email_address) store_from_select_email_flow_in_session if current_user flash[:success] = t('devise.confirmations.confirmed') - redirect_to account_url + if params[:request_id] + redirect_to sign_up_select_email_url + else + redirect_to account_url + end else flash[:success] = t('devise.confirmations.confirmed_but_sign_in') - redirect_to root_url + redirect_to root_url(request_id: params[:request_id]) end end diff --git a/app/controllers/users/email_language_controller.rb b/app/controllers/users/email_language_controller.rb index 33eb44d1f57..a7ef4f0369d 100644 --- a/app/controllers/users/email_language_controller.rb +++ b/app/controllers/users/email_language_controller.rb @@ -10,7 +10,7 @@ def show def update form_response = UpdateEmailLanguageForm.new(current_user).submit(update_email_params) - analytics.email_language_updated(**form_response.to_h) + analytics.email_language_updated(**form_response) flash[:success] = I18n.t('account.email_language.updated') if form_response.success? diff --git a/app/controllers/users/emails_controller.rb b/app/controllers/users/emails_controller.rb index b52c7625d8c..0f8681fa729 100644 --- a/app/controllers/users/emails_controller.rb +++ b/app/controllers/users/emails_controller.rb @@ -23,9 +23,9 @@ def add ) result = @add_user_email_form.submit( - current_user, permitted_params + current_user, permitted_params.merge(request_id:) ) - analytics.add_email_request(**result.to_h) + analytics.add_email_request(**result) if result.success? process_successful_creation @@ -42,7 +42,7 @@ def resend if email_address && !email_address.confirmed? analytics.resend_add_email_request(success: true) - SendAddEmailConfirmation.new(current_user).call(email_address) + SendAddEmailConfirmation.new(current_user).call(email_address:, request_id:) flash[:success] = t('notices.resend_confirmation_email.success') redirect_to add_email_verify_email_url else @@ -58,7 +58,7 @@ def confirm_delete def delete result = DeleteUserEmailForm.new(current_user, email_address).submit - analytics.email_deletion_request(**result.to_h) + analytics.email_deletion_request(**result) if result.success? handle_successful_delete else @@ -89,6 +89,10 @@ def authorize_user_to_edit_email render_not_found end + def request_id + sp_session[:request_id] + end + def email_address EmailAddress.find(params[:id]) end @@ -114,7 +118,7 @@ def session_email end def permitted_params - params.require(:user).permit(:email) + params.require(:user).permit(:email, :request_id) end def check_max_emails_per_account diff --git a/app/controllers/users/passwords_controller.rb b/app/controllers/users/passwords_controller.rb index 1667142e54e..8c974d98a3d 100644 --- a/app/controllers/users/passwords_controller.rb +++ b/app/controllers/users/passwords_controller.rb @@ -26,7 +26,7 @@ def update result = @update_user_password_form.submit(user_password_params) - analytics.password_changed(**result.to_h) + analytics.password_changed(**result) if result.success? handle_valid_password diff --git a/app/controllers/users/phone_setup_controller.rb b/app/controllers/users/phone_setup_controller.rb index 1f5af564354..3369ff2e973 100644 --- a/app/controllers/users/phone_setup_controller.rb +++ b/app/controllers/users/phone_setup_controller.rb @@ -33,7 +33,7 @@ def index def create @new_phone_form = NewPhoneForm.new(user: current_user, analytics: analytics) result = @new_phone_form.submit(new_phone_form_params) - analytics.multi_factor_auth_phone_setup(**result.to_h) + analytics.multi_factor_auth_phone_setup(**result) if result.success? handle_create_success(@new_phone_form.phone) diff --git a/app/controllers/users/piv_cac_authentication_setup_controller.rb b/app/controllers/users/piv_cac_authentication_setup_controller.rb index b52d1e3bad9..d6af98552cc 100644 --- a/app/controllers/users/piv_cac_authentication_setup_controller.rb +++ b/app/controllers/users/piv_cac_authentication_setup_controller.rb @@ -112,7 +112,7 @@ def process_valid_submission def track_mfa_method_added analytics.multi_factor_auth_added_piv_cac(**analytics_properties) - Funnel::Registration::AddMfa.call(current_user.id, 'piv_cac', analytics) + Funnel::Registration::AddMfa.call(current_user.id, 'piv_cac', analytics, threatmetrix_attrs) end def process_invalid_submission diff --git a/app/controllers/users/piv_cac_controller.rb b/app/controllers/users/piv_cac_controller.rb index d540809d666..ec87ff792ad 100644 --- a/app/controllers/users/piv_cac_controller.rb +++ b/app/controllers/users/piv_cac_controller.rb @@ -16,7 +16,7 @@ def edit; end def update result = form.submit(name: params.dig(:form, :name)) - analytics.piv_cac_update_name_submitted(**result.to_h) + analytics.piv_cac_update_name_submitted(**result) if result.success? flash[:success] = presenter.rename_success_alert_text @@ -30,7 +30,7 @@ def update def destroy result = form.submit - analytics.piv_cac_delete_submitted(**result.to_h) + analytics.piv_cac_delete_submitted(**result) if result.success? create_user_event(:piv_cac_disabled) diff --git a/app/controllers/users/piv_cac_login_controller.rb b/app/controllers/users/piv_cac_login_controller.rb index 57785762f1f..7e6c31cbe8f 100644 --- a/app/controllers/users/piv_cac_login_controller.rb +++ b/app/controllers/users/piv_cac_login_controller.rb @@ -54,7 +54,7 @@ def process_piv_cac_login else process_invalid_submission end - analytics.piv_cac_login(**result.to_h, new_device: @new_device) + analytics.piv_cac_login(**result, new_device: @new_device) end def piv_cac_login_form diff --git a/app/controllers/users/reset_passwords_controller.rb b/app/controllers/users/reset_passwords_controller.rb index 47fb962aac9..49d1b6374f8 100644 --- a/app/controllers/users/reset_passwords_controller.rb +++ b/app/controllers/users/reset_passwords_controller.rb @@ -17,7 +17,7 @@ def create @password_reset_email_form = PasswordResetEmailForm.new(email) result = @password_reset_email_form.submit - analytics.password_reset_email(**result.to_h) + analytics.password_reset_email(**result) if result.success? handle_valid_email @@ -32,7 +32,7 @@ def edit else result = PasswordResetTokenValidator.new(token_user).submit - analytics.password_reset_token(**result.to_h) + analytics.password_reset_token(**result) if result.success? @reset_password_form = ResetPasswordForm.new(user: build_user) @forbidden_passwords = forbidden_passwords(token_user.email_addresses) @@ -54,7 +54,7 @@ def update result = @reset_password_form.submit(user_params) - analytics.password_reset_password(**result.to_h) + analytics.password_reset_password(**result) if result.success? session.delete(:reset_password_token) diff --git a/app/controllers/users/rules_of_use_controller.rb b/app/controllers/users/rules_of_use_controller.rb index 39b2c2ed6b2..c711c0f8174 100644 --- a/app/controllers/users/rules_of_use_controller.rb +++ b/app/controllers/users/rules_of_use_controller.rb @@ -19,7 +19,7 @@ def create result = @rules_of_use_form.submit(permitted_params) - analytics.rules_of_use_submitted(**result.to_h) + analytics.rules_of_use_submitted(**result) if result.success? process_successful_agreement_to_rules_of_use diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index b77e7351c39..aed6d671f5c 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -208,7 +208,7 @@ def track_authentication_attempt (recaptcha_response.success? || log_captcha_failures_only?) analytics.email_and_password_auth( - **recaptcha_response.to_h, + **recaptcha_response, success: success, user_id: user.uuid, user_locked_out: user_locked_out?(user), diff --git a/app/controllers/users/totp_setup_controller.rb b/app/controllers/users/totp_setup_controller.rb index af09ac734fe..7e50930a4fb 100644 --- a/app/controllers/users/totp_setup_controller.rb +++ b/app/controllers/users/totp_setup_controller.rb @@ -90,7 +90,7 @@ def create_events enabled_mfa_methods_count: mfa_user.enabled_mfa_methods_count, in_account_creation_flow: in_account_creation_flow?, ) - Funnel::Registration::AddMfa.call(current_user.id, 'auth_app', analytics) + Funnel::Registration::AddMfa.call(current_user.id, 'auth_app', analytics, threatmetrix_attrs) end def process_invalid_code diff --git a/app/controllers/users/two_factor_authentication_controller.rb b/app/controllers/users/two_factor_authentication_controller.rb index 57a5444489e..3a63ff6acb5 100644 --- a/app/controllers/users/two_factor_authentication_controller.rb +++ b/app/controllers/users/two_factor_authentication_controller.rb @@ -20,7 +20,7 @@ def show def send_code result = otp_delivery_selection_form.submit(delivery_params) - analytics.otp_delivery_selection(**result.to_h) + analytics.otp_delivery_selection(**result) if result.success? handle_valid_otp_params( result, @@ -80,7 +80,7 @@ def phone_configuration def validate_otp_delivery_preference_and_send_code result = otp_delivery_selection_form.submit(otp_delivery_preference: delivery_preference) - analytics.otp_delivery_selection(**result.to_h) + analytics.otp_delivery_selection(**result) phone_is_confirmed = UserSessionContext.authentication_or_reauthentication_context?(context) phone_capabilities = PhoneNumberCapabilities.new( parsed_phone, diff --git a/app/controllers/users/two_factor_authentication_setup_controller.rb b/app/controllers/users/two_factor_authentication_setup_controller.rb index 304a63afc5d..34b5370f2ce 100644 --- a/app/controllers/users/two_factor_authentication_setup_controller.rb +++ b/app/controllers/users/two_factor_authentication_setup_controller.rb @@ -23,7 +23,7 @@ def index def create result = submit_form - analytics.user_registration_2fa_setup(**result.to_h) + analytics.user_registration_2fa_setup(**result) user_session[:platform_authenticator_available] = params[:platform_authenticator_available] == 'true' diff --git a/app/controllers/users/verify_personal_key_controller.rb b/app/controllers/users/verify_personal_key_controller.rb index 30914e01b6e..baba6de0794 100644 --- a/app/controllers/users/verify_personal_key_controller.rb +++ b/app/controllers/users/verify_personal_key_controller.rb @@ -30,7 +30,7 @@ def create result = personal_key_form.submit analytics.personal_key_reactivation_submitted( - **result.to_h, + **result, pii_like_keypaths: [ [:errors, :personal_key], [:error_details, :personal_key], diff --git a/app/controllers/users/webauthn_controller.rb b/app/controllers/users/webauthn_controller.rb index ddc13846239..c7dee0cf2a8 100644 --- a/app/controllers/users/webauthn_controller.rb +++ b/app/controllers/users/webauthn_controller.rb @@ -15,7 +15,7 @@ def edit; end def update result = form.submit(name: params.dig(:form, :name)) - analytics.webauthn_update_name_submitted(**result.to_h) + analytics.webauthn_update_name_submitted(**result) if result.success? flash[:success] = presenter.rename_success_alert_text @@ -29,7 +29,7 @@ def update def destroy result = form.submit - analytics.webauthn_delete_submitted(**result.to_h) + analytics.webauthn_delete_submitted(**result) if result.success? flash[:success] = presenter.delete_success_alert_text diff --git a/app/controllers/users/webauthn_setup_controller.rb b/app/controllers/users/webauthn_setup_controller.rb index 60bb0fc4952..66a4694a668 100644 --- a/app/controllers/users/webauthn_setup_controller.rb +++ b/app/controllers/users/webauthn_setup_controller.rb @@ -136,13 +136,23 @@ def process_valid_webauthn(form) handle_valid_verification_for_confirmation_context( auth_method: TwoFactorAuthenticatable::AuthMethod::WEBAUTHN_PLATFORM, ) - Funnel::Registration::AddMfa.call(current_user.id, 'webauthn_platform', analytics) + Funnel::Registration::AddMfa.call( + current_user.id, + 'webauthn_platform', + analytics, + threatmetrix_attrs, + ) flash[:success] = t('notices.webauthn_platform_configured') else handle_valid_verification_for_confirmation_context( auth_method: TwoFactorAuthenticatable::AuthMethod::WEBAUTHN, ) - Funnel::Registration::AddMfa.call(current_user.id, 'webauthn', analytics) + Funnel::Registration::AddMfa.call( + current_user.id, + 'webauthn', + analytics, + threatmetrix_attrs, + ) flash[:success] = t('notices.webauthn_configured') end redirect_to next_setup_path || after_mfa_setup_path diff --git a/app/forms/add_user_email_form.rb b/app/forms/add_user_email_form.rb index cc91ff62fa2..f34ad455ba5 100644 --- a/app/forms/add_user_email_form.rb +++ b/app/forms/add_user_email_form.rb @@ -23,7 +23,7 @@ def submit(user, params) @user = user @email = params[:email] @email_address = email_address_record(@email) - + @request_id = params[:request_id] if valid? process_successful_submission else @@ -46,12 +46,13 @@ def email_address_record(email) private attr_writer :email - attr_reader :success, :email_address + attr_reader :success, :email_address, :request_id def process_successful_submission @success = true email_address.save! - SendAddEmailConfirmation.new(user).call(email_address, in_select_email_flow) + SendAddEmailConfirmation.new(user). + call(email_address:, in_select_email_flow:, request_id:) end def extra_analytics_attributes diff --git a/app/forms/idv/api_image_upload_form.rb b/app/forms/idv/api_image_upload_form.rb index fb775b3f9ce..3372422bfc3 100644 --- a/app/forms/idv/api_image_upload_form.rb +++ b/app/forms/idv/api_image_upload_form.rb @@ -83,7 +83,7 @@ def validate_form extra: extra_attributes, ) - analytics.idv_doc_auth_submitted_image_upload_form(**response.to_h) + analytics.idv_doc_auth_submitted_image_upload_form(**response) response end diff --git a/app/javascript/packs/mock-device-profiling.tsx b/app/javascript/packs/mock-device-profiling.tsx index 6e52d9dc0e9..36ee7defec8 100644 --- a/app/javascript/packs/mock-device-profiling.tsx +++ b/app/javascript/packs/mock-device-profiling.tsx @@ -113,15 +113,6 @@ function MockDeviceProfilingOptions() { ); } -document.addEventListener('DOMContentLoaded', () => { - const ssnInput = document.getElementsByName('doc_auth[ssn]')[0]; - - if (ssnInput) { - const passwordToggle = ssnInput.closest('lg-password-toggle'); - - const div = document.createElement('div'); - passwordToggle?.parentElement?.appendChild(div); - - render(, div); - } -}); +const appRoot = document.createElement('div'); +currentScript?.after(appRoot); +render(, appRoot); diff --git a/app/jobs/account_creation_threat_metrix_job.rb b/app/jobs/account_creation_threat_metrix_job.rb new file mode 100644 index 00000000000..4d1dd53fe32 --- /dev/null +++ b/app/jobs/account_creation_threat_metrix_job.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class AccountCreationThreatMetrixJob < ApplicationJob + def perform( + user_id: nil, + threatmetrix_session_id: nil, + request_ip: nil, + email: nil + ) + + device_profiling_result = AccountCreation::DeviceProfiling.new.proof( + request_ip: request_ip, + threatmetrix_session_id: threatmetrix_session_id, + user_email: email, + ) + ensure + user = User.find_by(id: user_id) + analytics(user).account_creation_tmx_result(**device_profiling_result.to_h) + end + + def analytics(user) + Analytics.new(user: user, request: nil, session: {}, sp: nil) + end +end diff --git a/app/jobs/socure_reason_code_download_job.rb b/app/jobs/socure_reason_code_download_job.rb index 7cbf521e4cd..16c4be2a1de 100644 --- a/app/jobs/socure_reason_code_download_job.rb +++ b/app/jobs/socure_reason_code_download_job.rb @@ -11,7 +11,7 @@ def perform return unless IdentityConfig.store.idv_socure_reason_code_download_enabled result = Proofing::Socure::ReasonCodes::Importer.new.synchronize - analytics.idv_socure_reason_code_download(**result.to_h) + analytics.idv_socure_reason_code_download(**result) end def analytics diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 2f46ed4195e..ca8c1f2f0a5 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -199,7 +199,7 @@ def verify_by_mail_letter_requested end end - def add_email(token, from_select_email_flow = nil) + def add_email(token:, request_id:, from_select_email_flow: nil) with_user_locale(user) do presenter = ConfirmationEmailPresenter.new(user, view_context) @first_sentence = presenter.first_sentence @@ -208,6 +208,7 @@ def add_email(token, from_select_email_flow = nil) confirmation_token: token, from_select_email_flow:, locale: locale_url_param, + request_id:, ) mail(to: email_address.email, subject: t('user_mailer.add_email.subject')) end diff --git a/app/presenters/idv/in_person/ready_to_verify_presenter.rb b/app/presenters/idv/in_person/ready_to_verify_presenter.rb index 7759de34057..988c2a00eac 100644 --- a/app/presenters/idv/in_person/ready_to_verify_presenter.rb +++ b/app/presenters/idv/in_person/ready_to_verify_presenter.rb @@ -24,8 +24,10 @@ def days_remaining end def formatted_due_date - enrollment.due_date.in_time_zone(USPS_SERVER_TIMEZONE). - strftime(I18n.t('time.formats.event_date')) + I18n.l( + enrollment.due_date.in_time_zone(USPS_SERVER_TIMEZONE), + format: :event_date, + ) end def selected_location_hours(prefix) diff --git a/app/presenters/idv/in_person/verification_results_email_presenter.rb b/app/presenters/idv/in_person/verification_results_email_presenter.rb index 55a79260aa4..724245d6ee9 100644 --- a/app/presenters/idv/in_person/verification_results_email_presenter.rb +++ b/app/presenters/idv/in_person/verification_results_email_presenter.rb @@ -17,8 +17,9 @@ def initialize(enrollment:, url_options:, visited_location_name:) end def formatted_verified_date - enrollment.status_updated_at.in_time_zone(USPS_SERVER_TIMEZONE).strftime( - I18n.t('time.formats.event_date'), + I18n.l( + enrollment.status_updated_at.in_time_zone(USPS_SERVER_TIMEZONE), + format: :event_date, ) end diff --git a/app/services/account_creation/device_profiling.rb b/app/services/account_creation/device_profiling.rb new file mode 100644 index 00000000000..7cccde45b12 --- /dev/null +++ b/app/services/account_creation/device_profiling.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module AccountCreation + class DeviceProfiling + attr_reader :request_ip, + :threatmetrix_session_id, + :user_email, + :device_profile_result + def proof( + request_ip:, + threatmetrix_session_id:, + user_email: + ) + @request_ip = request_ip + @threatmetrix_session_id = threatmetrix_session_id + @user_email = user_email + + @device_profile_result = device_profile + end + + def device_profile + return threatmetrix_disabled_result unless + FeatureManagement.account_creation_device_profiling_collecting_enabled? + return threatmetrix_id_missing_result if threatmetrix_session_id.blank? + + proofer.proof( + threatmetrix_session_id: threatmetrix_session_id, + email: user_email, + request_ip: request_ip, + ) + end + + def threatmetrix_disabled_result + Proofing::DdpResult.new( + success: true, + client: 'tmx_disabled', + review_status: 'pass', + ) + end + + def threatmetrix_id_missing_result + Proofing::DdpResult.new( + success: false, + client: 'tmx_session_id_missing', + review_status: 'reject', + ) + end + + def proofer + @proofer ||= + if IdentityConfig.store.lexisnexis_threatmetrix_mock_enabled + Proofing::Mock::DdpMockClient.new + else + Proofing::LexisNexis::Ddp::Proofer.new( + api_key: IdentityConfig.store.lexisnexis_threatmetrix_api_key, + org_id: IdentityConfig.store.lexisnexis_threatmetrix_org_id, + base_url: IdentityConfig.store.lexisnexis_threatmetrix_base_url, + ddp_policy: IdentityConfig.store.lexisnexis_threatmetrix_authentication_policy, + ) + end + end + end +end diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 5d6c0b352cd..f9a49854a66 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -11,6 +11,46 @@ # || || module AnalyticsEvents + # @param [Boolean] success Check whether threatmetrix succeeded properly. + # @param [String] transaction_id Vendor-specific transaction ID for the request. + # @param [String, nil] client Client user was directed from when creating account + # @param [array, nil] errors error response from api call + # @param [String, nil] exception Error exception from api call + # @param [Boolean] timed_out set whether api call timed out + # @param [String] review_status TMX decision on the user + # @param [String] account_lex_id LexID associated with the response. + # @param [String] session_id Session ID associated with response + # @param [Hash] response_body total response body for api call + # Result when threatmetrix is completed for account creation and result + def account_creation_tmx_result( + client:, + success:, + errors:, + exception:, + timed_out:, + transaction_id:, + review_status:, + account_lex_id:, + session_id:, + response_body:, + **extra + ) + track_event( + :account_creation_tmx_result, + client:, + success:, + errors:, + exception:, + timed_out:, + transaction_id:, + review_status:, + account_lex_id:, + session_id:, + response_body:, + **extra, + ) + end + # @param [Boolean] success # When a user submits a form to delete their account def account_delete_submitted(success:, **extra) diff --git a/app/services/doc_auth/socure/request.rb b/app/services/doc_auth/socure/request.rb index 64fa97efd05..cb7644c14a3 100644 --- a/app/services/doc_auth/socure/request.rb +++ b/app/services/doc_auth/socure/request.rb @@ -6,9 +6,11 @@ class Request def fetch # return DocAuth::Response with DocAuth::Error if workflow is invalid http_response = send_http_request - return handle_invalid_response(http_response) unless http_response.success? - - handle_http_response(http_response) + if http_response&.success? && http_response.body.present? + handle_http_response(http_response) + else + handle_invalid_response(http_response) + end rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Faraday::SSLError => e handle_connection_error(exception: e) end @@ -33,16 +35,27 @@ def handle_http_response(_response) end def handle_invalid_response(http_response) - begin - if http_response.body.present? - warn(http_response.body) - JSON.parse(http_response.body) - else - {} - end + message = [ + self.class.name, + 'Unexpected HTTP response', + http_response&.status, + ].join(' ') + exception = DocAuth::RequestError.new(message, http_response&.status) + + response_body = begin + http_response&.body.present? ? JSON.parse(http_response.body) : {} rescue JSON::JSONError {} end + handle_connection_error( + exception: exception, + status: response_body.dig('status'), + status_message: response_body.dig('msg'), + ) + end + + def handle_connection_error(exception:, status: nil, status_message: nil) + raise NotImplementedError end def send_http_get_request diff --git a/app/services/doc_auth/socure/requests/document_request.rb b/app/services/doc_auth/socure/requests/document_request.rb index 796b6cb22b2..4a6f5b0920f 100644 --- a/app/services/doc_auth/socure/requests/document_request.rb +++ b/app/services/doc_auth/socure/requests/document_request.rb @@ -27,7 +27,7 @@ def lang(language) def body redirect = { - method: 'POST', + method: 'GET', url: redirect_url, } @@ -47,6 +47,20 @@ def handle_http_response(http_response) JSON.parse(http_response.body, symbolize_names: true) end + def handle_connection_error(exception:, status: nil, status_message: nil) + NewRelic::Agent.notice_error(exception) + { + success: false, + errors: { network: true }, + exception: exception, + extra: { + vendor: 'Socure', + vendor_status: status, + vendor_status_message: status_message, + }.compact, + } + end + def method :post end diff --git a/app/services/doc_auth/socure/requests/docv_result_request.rb b/app/services/doc_auth/socure/requests/docv_result_request.rb index 6d53cc39d7a..7f4bc26d116 100644 --- a/app/services/doc_auth/socure/requests/docv_result_request.rb +++ b/app/services/doc_auth/socure/requests/docv_result_request.rb @@ -27,6 +27,20 @@ def handle_http_response(http_response) ) end + def handle_connection_error(exception:, status: nil, status_message: nil) + NewRelic::Agent.notice_error(exception) + DocAuth::Response.new( + success: false, + errors: { network: true }, + exception: exception, + extra: { + vendor: 'Socure', + vendor_status: status, + vendor_status_message: status_message, + }.compact, + ) + end + def document_capture_session @document_capture_session ||= DocumentCaptureSession.find_by!(uuid: document_capture_session_uuid) diff --git a/app/services/doc_auth/socure/responses/docv_result_response.rb b/app/services/doc_auth/socure/responses/docv_result_response.rb index 23b17f88e9a..aaf4413da27 100644 --- a/app/services/doc_auth/socure/responses/docv_result_response.rb +++ b/app/services/doc_auth/socure/responses/docv_result_response.rb @@ -32,7 +32,7 @@ class DocvResultResponse < DocAuth::Response expiration_date: %w[documentVerification documentData expirationDate], }.freeze - def initialize(http_response: nil, biometric_comparison_required: false) + def initialize(http_response:, biometric_comparison_required: false) @http_response = http_response @biometric_comparison_required = biometric_comparison_required @pii_from_doc = read_pii @@ -110,17 +110,23 @@ def get_data(path) end def parsed_response_body - @parsed_response_body ||= JSON.parse(http_response.body).with_indifferent_access + @parsed_response_body ||= begin + http_response&.body.present? ? JSON.parse( + http_response.body, + ).with_indifferent_access : {} + rescue JSON::JSONError + {} + end end def state_id_type type = get_data(DATA_PATHS[:id_type]) - type.gsub(/\W/, '').underscore + type&.gsub(/\W/, '')&.underscore end def parse_date(date_string) Date.parse(date_string) - rescue ArgumentError + rescue ArgumentError, TypeError message = { event: 'Failure to parse Socure ID+ date', }.to_json diff --git a/app/services/form_response.rb b/app/services/form_response.rb index e62f67c9613..3fdbbe93384 100644 --- a/app/services/form_response.rb +++ b/app/services/form_response.rb @@ -25,6 +25,8 @@ def to_h hash end + alias_method :to_hash, :to_h + def merge(other) self.class.new( success: success? && other.success?, diff --git a/app/services/funnel/registration/add_mfa.rb b/app/services/funnel/registration/add_mfa.rb index 14ac8595e3c..99ef121b994 100644 --- a/app/services/funnel/registration/add_mfa.rb +++ b/app/services/funnel/registration/add_mfa.rb @@ -3,14 +3,22 @@ module Funnel module Registration class AddMfa - def self.call(user_id, mfa_method, analytics) + def self.call(user_id, mfa_method, analytics, threatmetrix_attrs) now = Time.zone.now funnel = RegistrationLog.create_or_find_by(user_id: user_id) return if funnel.registered_at.present? analytics.user_registration_user_fully_registered(mfa_method: mfa_method) + process_threatmetrix_for_user( + threatmetrix_attrs, + ) funnel.update!(registered_at: now) end + + def self.process_threatmetrix_for_user(threatmetrix_attrs) + return unless FeatureManagement.account_creation_device_profiling_collecting_enabled? + AccountCreationThreatMetrixJob.perform_later(**threatmetrix_attrs) + end end end end diff --git a/app/services/proofing/lexis_nexis/config.rb b/app/services/proofing/lexis_nexis/config.rb index e650a58053b..084cd705178 100644 --- a/app/services/proofing/lexis_nexis/config.rb +++ b/app/services/proofing/lexis_nexis/config.rb @@ -15,6 +15,7 @@ module LexisNexis :request_timeout, :org_id, :api_key, + :ddp_policy, keyword_init: true, allowed_members: [ :instant_verify_workflow, diff --git a/app/services/proofing/lexis_nexis/ddp/verification_request.rb b/app/services/proofing/lexis_nexis/ddp/verification_request.rb index a918500fb93..85455809896 100644 --- a/app/services/proofing/lexis_nexis/ddp/verification_request.rb +++ b/app/services/proofing/lexis_nexis/ddp/verification_request.rb @@ -10,29 +10,29 @@ def build_request_body { api_key: config.api_key, org_id: config.org_id, - account_address_street1: applicant[:address1], + account_address_street1: applicant[:address1] || '', account_address_street2: applicant[:address2] || '', - account_address_city: applicant[:city], - account_address_state: applicant[:state], - account_address_country: 'US', - account_address_zip: applicant[:zipcode], + account_address_city: applicant[:city] || '', + account_address_state: applicant[:state] || '', + account_address_country: applicant[:state] ? 'US' : '', + account_address_zip: applicant[:zipcode] || '', account_date_of_birth: applicant[:dob] ? Date.parse(applicant[:dob]).strftime('%Y%m%d') : '', account_email: applicant[:email], - account_first_name: applicant[:first_name], - account_last_name: applicant[:last_name], + account_first_name: applicant[:first_name] || '', + account_last_name: applicant[:last_name] || '', account_telephone: '', # applicant[:phone], decision was made not to send phone - account_drivers_license_number: applicant[:state_id_number]&.gsub(/\W/, ''), - account_drivers_license_type: 'us_dl', - account_drivers_license_issuer: applicant[:state_id_jurisdiction].to_s.strip, + account_drivers_license_number: applicant[:state_id_number]&.gsub(/\W/, '') || '', + account_drivers_license_type: applicant[:state_id_number] ? 'us_dl' : '', + account_drivers_license_issuer: applicant[:state_id_jurisdiction].to_s.strip || '', event_type: 'ACCOUNT_CREATION', - policy: IdentityConfig.store.lexisnexis_threatmetrix_policy, + policy: config.ddp_policy, service_type: 'all', session_id: applicant[:threatmetrix_session_id], - national_id_number: applicant[:ssn].gsub(/\D/, ''), - national_id_type: 'US_SSN', + national_id_number: applicant[:ssn]&.gsub(/\D/, '') || '', + national_id_type: applicant[:ssn] ? 'US_SSN' : '', input_ip_address: applicant[:request_ip], - local_attrib_1: applicant[:uuid_prefix], + local_attrib_1: applicant[:uuid_prefix] || '', }.to_json end diff --git a/app/services/proofing/resolution/plugins/threat_metrix_plugin.rb b/app/services/proofing/resolution/plugins/threat_metrix_plugin.rb index 03fa63dbeab..f16176b9911 100644 --- a/app/services/proofing/resolution/plugins/threat_metrix_plugin.rb +++ b/app/services/proofing/resolution/plugins/threat_metrix_plugin.rb @@ -46,6 +46,7 @@ def proofer api_key: IdentityConfig.store.lexisnexis_threatmetrix_api_key, org_id: IdentityConfig.store.lexisnexis_threatmetrix_org_id, base_url: IdentityConfig.store.lexisnexis_threatmetrix_base_url, + ddp_policy: IdentityConfig.store.lexisnexis_threatmetrix_policy, ) end end diff --git a/app/services/send_add_email_confirmation.rb b/app/services/send_add_email_confirmation.rb index fbeb8d900a8..3fb0641b627 100644 --- a/app/services/send_add_email_confirmation.rb +++ b/app/services/send_add_email_confirmation.rb @@ -7,9 +7,10 @@ def initialize(user) @user = user end - def call(email_address, in_select_email_flow = nil) + def call(email_address:, in_select_email_flow: nil, request_id: nil) @email_address = email_address @in_select_email_flow = in_select_email_flow + @request_id = request_id update_email_address_record send_email end @@ -24,7 +25,7 @@ def confirmation_sent_at email_address.confirmation_sent_at end - attr_reader :email_address, :in_select_email_flow + attr_reader :email_address, :in_select_email_flow, :request_id def update_email_address_record email_address.update!( @@ -59,8 +60,9 @@ def send_email_associated_with_another_account_email def send_confirmation_email UserMailer.with(user: user, email_address: email_address).add_email( - confirmation_token, - in_select_email_flow, + token: confirmation_token, + from_select_email_flow: in_select_email_flow, + request_id:, ).deliver_now_or_later end end diff --git a/app/views/accounts/connected_accounts/selected_email/edit.html.erb b/app/views/accounts/connected_accounts/selected_email/edit.html.erb index f2a57f126e5..3347832195c 100644 --- a/app/views/accounts/connected_accounts/selected_email/edit.html.erb +++ b/app/views/accounts/connected_accounts/selected_email/edit.html.erb @@ -4,7 +4,7 @@ <% c.with_header(id: 'select-email-heading') { t('titles.select_email') } %>

- <%= t('help_text.select_preferred_email', sp: @identity.display_name, app_name: APP_NAME) %> + <%= t('help_text.select_preferred_email_html', sp: @identity.display_name) %>

<%= simple_form_for( @@ -30,7 +30,7 @@ ] end, ) %> - <%= f.submit(t('help_text.requested_attributes.change_email_link'), class: 'margin-top-1') %> + <%= f.submit(t('help_text.requested_attributes.select_email_link'), class: 'margin-top-1') %> <% end %> <%= render ButtonComponent.new( diff --git a/app/views/sign_up/registrations/new.html.erb b/app/views/sign_up/registrations/new.html.erb index c31939994e6..6699b21dac1 100644 --- a/app/views/sign_up/registrations/new.html.erb +++ b/app/views/sign_up/registrations/new.html.erb @@ -17,15 +17,6 @@ <%= render PageHeadingComponent.new.with_content(t('headings.create_account_new_users')) %> -<% if FeatureManagement.account_creation_device_profiling_collecting_enabled? %> - <%= render partial: 'shared/threat_metrix_profiling', - locals: { - threatmetrix_session_id:, - threatmetrix_javascript_urls:, - threatmetrix_iframe_url:, - } %> -<% end %> - <%= simple_form_for(@register_user_email_form, url: sign_up_register_path) do |f| %> <%= render ValidatedFieldComponent.new( form: f, @@ -51,6 +42,15 @@ required: true, ) %> + <% if FeatureManagement.account_creation_device_profiling_collecting_enabled? %> + <%= render partial: 'shared/threat_metrix_profiling', + locals: { + threatmetrix_session_id:, + threatmetrix_javascript_urls:, + threatmetrix_iframe_url:, + } %> + <% end %> + <%= f.submit t('forms.buttons.submit.default'), class: 'display-block margin-y-5' %> <% end %> diff --git a/app/views/sign_up/select_email/show.html.erb b/app/views/sign_up/select_email/show.html.erb index 53b0c04e5de..00a5134e774 100644 --- a/app/views/sign_up/select_email/show.html.erb +++ b/app/views/sign_up/select_email/show.html.erb @@ -4,7 +4,7 @@ <% c.with_header(id: 'select-email-heading') { t('titles.select_email') } %>

- <%= I18n.t('help_text.select_preferred_email', sp: @sp_name, app_name: APP_NAME) %> + <%= I18n.t('help_text.select_preferred_email_html', sp: @sp_name) %>

<%= simple_form_for(@select_email_form, url: sign_up_select_email_path) do |f| %> @@ -26,7 +26,7 @@ ] end, ) %> - <%= f.submit t('help_text.requested_attributes.change_email_link'), class: 'margin-top-1' %> + <%= f.submit t('help_text.requested_attributes.select_email_link'), class: 'margin-top-1' %> <% end %> <%= render ButtonComponent.new( diff --git a/config/application.yml.default b/config/application.yml.default index 8a39f3775fd..74b313ec0a3 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -207,6 +207,7 @@ lexisnexis_request_mode: testing ################################################################### # LexisNexis DDP/ThreatMetrix ##################################### lexisnexis_threatmetrix_api_key: +lexisnexis_threatmetrix_authentication_policy: '1234' lexisnexis_threatmetrix_base_url: lexisnexis_threatmetrix_js_signing_cert: '' lexisnexis_threatmetrix_mock_enabled: true diff --git a/config/locales/en.yml b/config/locales/en.yml index c9377403782..c0a8740407b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -976,11 +976,12 @@ help_text.requested_attributes.full_name: Full name help_text.requested_attributes.ial2_reverified_consent_info_html: 'Because you verified your identity again, we need your permission to share this information with %{sp_html}:' help_text.requested_attributes.intro_html: 'We’ll share this information with %{sp_html}:' help_text.requested_attributes.phone: Phone number +help_text.requested_attributes.select_email_link: Select email help_text.requested_attributes.social_security_number: Social Security number help_text.requested_attributes.verified_at: Updated on help_text.requested_attributes.x509_issuer: PIV/CAC Issuer help_text.requested_attributes.x509_subject: PIV/CAC Identity -help_text.select_preferred_email: You may change which email you share with %{sp} since you have multiple emails associated with your %{app_name} account. +help_text.select_preferred_email_html: Select or add the email you’d like to use to access %{sp}. i18n.language: Language i18n.locale.en: English i18n.locale.es: Español @@ -1690,7 +1691,7 @@ two_factor_authentication.max_generic_login_attempts_reached: For your security, two_factor_authentication.max_otp_login_attempts_reached: For your security, your account is temporarily locked because you have entered the one-time code incorrectly too many times. two_factor_authentication.max_otp_requests_reached: For your security, your account is temporarily locked because you have requested a one-time code too many times. two_factor_authentication.max_personal_key_login_attempts_reached: For your security, your account is temporarily locked because you have entered the personal key incorrectly too many times. -two_factor_authentication.max_piv_cac_login_attempts_reached: For your security, your account is temporarily locked because you have presented your piv/cac credential incorrectly too many times. +two_factor_authentication.max_piv_cac_login_attempts_reached: For your security, your account is temporarily locked because you have presented your PIV/CAC credential incorrectly too many times. two_factor_authentication.mobile_terms_of_service: Mobile terms of service two_factor_authentication.opt_in.error_retry: Sorry, we are having trouble opting you in. Please try again. two_factor_authentication.opt_in.opted_out_html: You’ve opted out of receiving text messages at %{phone_number_html}. You can opt in and receive a security code again to that phone number. diff --git a/config/locales/es.yml b/config/locales/es.yml index f27cbad7794..ac4c70204ad 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -987,11 +987,12 @@ help_text.requested_attributes.full_name: Nombre completo help_text.requested_attributes.ial2_reverified_consent_info_html: 'Como volvió a verificar su identidad, necesitamos su permiso para divulgar esta información a %{sp_html}:' help_text.requested_attributes.intro_html: 'Divulgaremos esta información a %{sp_html}:' help_text.requested_attributes.phone: Número de teléfono +help_text.requested_attributes.select_email_link: Seleccionar correo electrónico help_text.requested_attributes.social_security_number: Número de Seguro Social help_text.requested_attributes.verified_at: Actualizado en help_text.requested_attributes.x509_issuer: Emisor de la tarjeta PIV o CAC help_text.requested_attributes.x509_subject: Identidad de la tarjeta PIV o CAC -help_text.select_preferred_email: Puede cambiar el correo electrónico que comparte con %{sp} ya que tiene varios correos electrónicos asociados a su cuenta de %{app_name}. +help_text.select_preferred_email_html: Seleccione o agregue el correo electrónico que desea utilizar para acceder a %{sp}. i18n.language: Idioma i18n.locale.en: English i18n.locale.es: Español @@ -1206,7 +1207,7 @@ image_description.warning: Señal amarilla de precaución in_person_proofing.body.barcode.cancel_link_text: Cancele su código de barras in_person_proofing.body.barcode.close_window: Ya puede cerrar esta ventana. in_person_proofing.body.barcode.deadline: Debe acudir a cualquier oficina de correos participante antes del %{deadline} -in_person_proofing.body.barcode.deadline_restart: Si vence el plazo, su información no se guardará y tendrá que reiniciar el proceso. +in_person_proofing.body.barcode.deadline_restart: Si acude después del plazo, su información no se guardará y tendrá que reiniciar el proceso. in_person_proofing.body.barcode.eipp_tag: Código de barras piloto mejorado GSA in_person_proofing.body.barcode.eipp_what_to_bring: 'Según el tipo de identificación que tenga, es posible que deba presentar documentos comprobatorios. Lea con atención las opciones siguientes:' in_person_proofing.body.barcode.email_sent: Enviamos el código de barras y la información más abajo al correo electrónico que usó para iniciar sesión @@ -1569,7 +1570,7 @@ step_indicator.status.complete: Completado step_indicator.status.current: Este paso step_indicator.status.not_complete: No completado time.am: a. m. -time.formats.event_date: '%B %-d, %Y' +time.formats.event_date: '%-d de %B de %Y' time.formats.event_time: '%-l:%M %p' time.formats.event_timestamp: '%B %-d, %Y a las %-l:%M %p' time.formats.event_timestamp_js: '%{month} %{day}, %{year} a las %{hour}:%{minute} %{day_period}' diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 2a41643f85c..f419b9ae3d0 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -976,11 +976,12 @@ help_text.requested_attributes.full_name: Nom complet help_text.requested_attributes.ial2_reverified_consent_info_html: 'Étant donné que vous avez revérifié votre identité, nous avons besoin de votre autorisation pour partager ces informations avec %{sp_html} :' help_text.requested_attributes.intro_html: 'Nous partagerons ces informations avec %{sp_html}:' help_text.requested_attributes.phone: Numéro de téléphone +help_text.requested_attributes.select_email_link: Sélectionner l’e-mail help_text.requested_attributes.social_security_number: Numéro de sécurité sociale help_text.requested_attributes.verified_at: Mis à jour le help_text.requested_attributes.x509_issuer: Émetteur PIV/CAC help_text.requested_attributes.x509_subject: Identité PIV/CAC -help_text.select_preferred_email: Vous pouvez modifier l’adresse e-mail que vous partagez avec %{sp} car vous possédez plusieurs adresses e-mail associées à votre compte %{app_name}. +help_text.select_preferred_email_html: Sélectionnez ou ajoutez l’adresse e-mail que vous souhaitez employer pour accéder à %{sp}. i18n.language: Langue i18n.locale.en: English i18n.locale.es: Español diff --git a/config/locales/zh.yml b/config/locales/zh.yml index e40a3f9067c..1530d4741cc 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -138,7 +138,7 @@ account.re_verify.footer: 验证身份来查看你的信息。 account.revoke_consent.link_title: 切断连接 account.revoke_consent.longer_description_html: 你的信息将不再与 %{service_provider_html} 分享。以后要访问 %{service_provider_html},你必须授权同意分享你的信息。授权同意请到 %{service_provider_html} 网站并登录。 account.security.link: 请到帮助中心了解更多信息 -account.security.text: 为了你的安全,你的用户资料已被锁住。 +account.security.text: 出于安全考虑,你的用户资料已被锁住。 account.verified_information.address: 地址 account.verified_information.dob: 生日 account.verified_information.full_name: 姓名 @@ -989,11 +989,12 @@ help_text.requested_attributes.full_name: 姓名 help_text.requested_attributes.ial2_reverified_consent_info_html: '因为你重新验证了身份,我们需要得到你的许可才能与 %{sp_html} 分享该信息。' help_text.requested_attributes.intro_html: 我们会与 %{sp_html} 分享这些信息: help_text.requested_attributes.phone: 电话号码 +help_text.requested_attributes.select_email_link: 选择电邮 help_text.requested_attributes.social_security_number: 社会保障号码 help_text.requested_attributes.verified_at: 更新是在 help_text.requested_attributes.x509_issuer: PIV/CAC 发放方 help_text.requested_attributes.x509_subject: PIV/CAC 身份 -help_text.select_preferred_email: 因为你有多个电邮与 %{app_name} 账户相关,你可以更改与我们 %{sp} 构分享哪个。 +help_text.select_preferred_email_html: 选择或添加你想用来访问%{sp}的电子邮件。 i18n.language: 语言 i18n.locale.en: English i18n.locale.es: Español @@ -1074,7 +1075,7 @@ idv.failure.phone.warning.next_steps_html: 尝试 另一个 你 idv.failure.phone.warning.try_again_button: 尝试另一个号码 idv.failure.phone.warning.you_entered: 你输入了: idv.failure.sessions.exception: 处理你的请求时内部出错。 -idv.failure.sessions.fail_html: 为了你的安全,我们限制你在网上尝试验证个人信息的次数。 %{timeout}后再试。 +idv.failure.sessions.fail_html: 出于安全考虑,我们限制你在网上尝试验证个人信息的次数。 %{timeout}后再试。 idv.failure.sessions.heading: 我们找不到与你个人信息匹配的记录 idv.failure.sessions.warning: 请检查一下你输入的信息,然后再试一下。常见错误包括社会保障号码或邮编不对。 idv.failure.setup.fail_date_html: 请给我们的联系中心在%{date_html} 之前打电话以继续验证你的身份。 @@ -1447,8 +1448,8 @@ notices.piv_cac_configured: 添加您的政府雇员ID notices.privacy.privacy_act_statement: 隐私法声明 notices.privacy.security_and_privacy_practices: 安全实践和隐私法声明 notices.resend_confirmation_email.success: 我们发送了另外一个确认电邮。 -notices.session_cleared: 为了你的安全,如果你在 %{minutes} 分钟内不移动到一个新页面,我们会清除你输入的内容。 -notices.session_timedout: 我们已将你登出。为了你的安全,如果你在 %{minutes} 分钟内不移动到一个新页面,%{app_name} 会结束你此次访问。 +notices.session_cleared: 出于安全考虑,如果你在 %{minutes} 分钟内不移动到一个新页面,我们会清除你输入的内容。 +notices.session_timedout: 我们已将你登出。出于安全考虑,如果你在 %{minutes} 分钟内不移动到一个新页面,%{app_name} 会结束你此次访问。 notices.sign_in.recaptcha.disclosure_statement_html: 该网站由 reCAPTCHA 保护而且谷歌的 %{google_policy_link_html} 和 %{google_tos_link_html} 都适用。 notices.signed_up_and_confirmed.first_paragraph_end: 带有确认你的电邮地址的链接。点击链接去继续把这个电邮添加到你的账户。 notices.signed_up_and_confirmed.first_paragraph_start: 我们已发电邮到 @@ -1458,11 +1459,11 @@ notices.signed_up_but_unconfirmed.first_paragraph_start: 我们已发电邮到 notices.signed_up_but_unconfirmed.resend_confirmation_email: 重新发送确认电邮 notices.timeout_warning.partially_signed_in.continue: 继续登录 notices.timeout_warning.partially_signed_in.live_region_message_html: '%{time_left_in_session_html} 后你会被登出。选择“保持我登录状态”保持登录;选择“把我登出”来登出。' -notices.timeout_warning.partially_signed_in.message_html: 为了你的安全, %{time_left_in_session_html}分钟后我们将取消你的登录。 +notices.timeout_warning.partially_signed_in.message_html: 出于安全考虑, %{time_left_in_session_html}分钟后我们将取消你的登录。 notices.timeout_warning.partially_signed_in.sign_out: 取消登录 notices.timeout_warning.signed_in.continue: 保持我登录状态 notices.timeout_warning.signed_in.live_region_message_html: '%{time_left_in_session_html} 后你会被登出。选择“保持我登录状态”保持登录;选择“把我登出”来登出。' -notices.timeout_warning.signed_in.message_html: 为了你的安全,我们会在 %{time_left_in_session_html} 后将你登出,除非你告诉我们不要这样做。 +notices.timeout_warning.signed_in.message_html: 出于安全考虑,我们会在 %{time_left_in_session_html} 后将你登出,除非你告诉我们不要这样做。 notices.timeout_warning.signed_in.sign_out: 把我登出 notices.totp_configured: 你账户添加了一个身份证实应用程序。 notices.use_diff_email.link: 使用一个不同的电邮地址 @@ -1570,7 +1571,7 @@ step_indicator.status.complete: 完成了 step_indicator.status.current: 目前步骤 step_indicator.status.not_complete: 未完成 time.am: 上午 -time.formats.event_date: '%B %-d, %Y' +time.formats.event_date: '%Y年%B%-d日' time.formats.event_time: '%-l:%M %p' time.formats.event_timestamp: '%B %-d, %Y at %-l:%M %p' time.formats.event_timestamp_js: '%{year}年%{month}月%{day}日, %{hour}:%{minute} %{day_period}' @@ -1698,12 +1699,12 @@ two_factor_authentication.login_options.webauthn: 安全密钥 two_factor_authentication.login_options.webauthn_info: 使用你的安全密钥来访问账户 two_factor_authentication.login_options.webauthn_platform: 人脸或触摸解锁 two_factor_authentication.login_options.webauthn_platform_info: 不用一次性代码,而是用你的面孔或指纹来访问你的账户。 -two_factor_authentication.max_backup_code_login_attempts_reached: 为了你的安全,你的账户暂时被锁住,因为你错误输入备用代码太多次。 -two_factor_authentication.max_generic_login_attempts_reached: 为了你的安全,你的账户暂时被锁住。 -two_factor_authentication.max_otp_login_attempts_reached: 为了你的安全,你的账户暂时被锁住,因为你错误输入一次性代码太多次。 -two_factor_authentication.max_otp_requests_reached: 为了你的安全,你的账户暂时被锁住,因为你要求一次性代码太多次。 -two_factor_authentication.max_personal_key_login_attempts_reached: 为了你的安全,你的账户暂时被锁住,因为你错误输入个人密钥太多次。 -two_factor_authentication.max_piv_cac_login_attempts_reached: 为了你的安全,你的账户暂时被锁住,因为你错误提供 piv/cac 凭据太多次。 +two_factor_authentication.max_backup_code_login_attempts_reached: 出于安全考虑,你的账户暂时被锁住,因为你错误输入备用代码太多次。 +two_factor_authentication.max_generic_login_attempts_reached: 出于安全考虑,你的账户暂时被锁住。 +two_factor_authentication.max_otp_login_attempts_reached: 出于安全考虑,你的账户暂时被锁住,因为你错误输入一次性代码太多次。 +two_factor_authentication.max_otp_requests_reached: 出于安全考虑,你的账户暂时被锁住,因为你要求一次性代码太多次。 +two_factor_authentication.max_personal_key_login_attempts_reached: 出于安全考虑,你的账户暂时被锁住,因为你错误输入个人密钥太多次。 +two_factor_authentication.max_piv_cac_login_attempts_reached: 出于安全考虑,你的账户暂时被锁住,因为你错误提供 PIV/CAC 凭据太多次。 two_factor_authentication.mobile_terms_of_service: 移动服务条款 two_factor_authentication.opt_in.error_retry: 抱歉,我们让你加入有困难。请再试一次。 two_factor_authentication.opt_in.opted_out_html: 你选择不在 %{phone_number_html} 接受短信。你可以选择加入并再在那个电话号码接受安全代码。 diff --git a/config/routes.rb b/config/routes.rb index 33df2761750..e9f42f70ac7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -360,7 +360,7 @@ get '/document_capture' => 'document_capture#show' put '/document_capture' => 'document_capture#update' get '/socure/document_capture' => 'socure/document_capture#show' - post '/socure/document_capture' => 'socure/document_capture#update' + get '/socure/document_capture_update' => 'socure/document_capture#update', as: :socure_document_capture_update # This route is included in SMS messages sent to users who start the IdV hybrid flow. It # should be kept short, and should not include underscores ("_"). get '/documents' => 'hybrid_mobile/entry#show', as: :hybrid_mobile_entry diff --git a/docs/backend.md b/docs/backend.md index 33494c7ecce..b476f2fd9e0 100644 --- a/docs/backend.md +++ b/docs/backend.md @@ -129,7 +129,7 @@ class MyController < ApplicationController form = MyForm.new(params) result = form.submit - analytics.my_event(**result.to_h) + analytics.my_event(**result) if result.success? do_something(form.sensitive_value_here) diff --git a/lib/feature_management.rb b/lib/feature_management.rb index 2808fcc95d7..4d5d4aaef2a 100644 --- a/lib/feature_management.rb +++ b/lib/feature_management.rb @@ -125,7 +125,7 @@ def self.recaptcha_enterprise? IdentityConfig.store.recaptcha_enterprise_project_id.present? end - # Whether we collect device profiling information as part of the account creation process + # Whether we collect device profiling as part of the account creation process def self.account_creation_device_profiling_collecting_enabled? case IdentityConfig.store.account_creation_device_profiling when :enabled, :collect_only then true diff --git a/lib/identity_config.rb b/lib/identity_config.rb index dbdc4874fa7..b859c70528b 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -15,7 +15,7 @@ def self.store # identity-hostdata transforms these configs to the described type # rubocop:disable Metrics/BlockLength - # rubocop:disable Metrics/LineLength + # rubocop:disable Layout/LineLength BUILDER = proc do |config| # ______________________________________ # / Adding something new in here? Please \ @@ -208,6 +208,7 @@ def self.store config.add(:in_person_stop_expiring_enrollments, type: :boolean) config.add(:invalid_gpo_confirmation_zipcode, type: :string) config.add(:lexisnexis_account_id, type: :string) + config.add(:lexisnexis_threatmetrix_authentication_policy, type: :string) config.add(:lexisnexis_base_url, type: :string) config.add(:lexisnexis_hmac_auth_enabled, type: :boolean) config.add(:lexisnexis_hmac_key_id, type: :string) @@ -471,6 +472,6 @@ def self.store config.add(:vtm_url) config.add(:weekly_auth_funnel_report_config, type: :json) end.freeze - # rubocop:enable Metrics/LineLength + # rubocop:enable Layout/LineLength # rubocop:enable Metrics/BlockLength end diff --git a/lib/tasks/backfill_in_person_pending_at.rake b/lib/tasks/backfill_in_person_pending_at.rake deleted file mode 100644 index 840bbea85db..00000000000 --- a/lib/tasks/backfill_in_person_pending_at.rake +++ /dev/null @@ -1,78 +0,0 @@ -# frozen_string_literal: true - -namespace :profiles do - desc 'Backfill the in_person_verification_pending_at value column.' - - ## - # Usage: - # - # Print pending updates - # bundle exec rake profiles:backfill_in_person_verification_pending_at - # - # Commit updates - # bundle exec rake profiles:backfill_in_person_verification_pending_at UPDATE_PROFILES=true - # - task backfill_in_person_verification_pending_at: :environment do |_task, _args| - ActiveRecord::Base.connection.execute('SET statement_timeout = 60000') - - update_profiles = ENV['UPDATE_PROFILES'] == 'true' - - profiles = Profile.where( - deactivation_reason: 'in_person_verification_pending', - in_person_verification_pending_at: nil, - ) - - profiles.each do |profile| - timestamp = profile.updated_at || profile.created_at - - warn "#{profile.id},#{profile.deactivation_reason},#{timestamp}" - if update_profiles - profile.update!( - in_person_verification_pending_at: timestamp, - deactivation_reason: nil, - ) - end - end - end - - ## - # Usage: - # - # Rollback the above: - # - # export BACKFILL_OUTPUT='' - # bundle exec rake profiles:rollback_backfill_in_person_verification_pending_at - # - task rollback_backfill_in_person_verification_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, deactivation_reason, _timestamp = profile_datum - Profile.where(id: profile_id).update!( - in_person_verification_pending_at: nil, - deactivation_reason: deactivation_reason, - ) - warn profile_id - end - end - - ## - # Usage: - # bundle exec rake profiles:validate_backfill_in_person_verification_pending_at - # - task validate_backfill_in_person_verification_pending_at: :environment do |_task, _args| - ActiveRecord::Base.connection.execute('SET statement_timeout = 60000') - - profiles = Profile.where( - deactivation_reason: 'in_person_verification_pending', - in_person_verification_pending_at: nil, - ) - - warn "backfill_in_person_verification_pending_at left #{profiles.count} rows" - end -end diff --git a/lib/tasks/backfill_sponsor_id.rake b/lib/tasks/backfill_sponsor_id.rake deleted file mode 100644 index f3463f13440..00000000000 --- a/lib/tasks/backfill_sponsor_id.rake +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -namespace :in_person_enrollments do - desc 'Backfill the sponsor_id column.' - - ## - # Usage: - # - # bundle exec rake in_person_enrollments:backfill_sponsor_id - # - task backfill_sponsor_id: :environment do |_task, _args| - with_timeout do - ipp_sponsor_id = IdentityConfig.store.usps_ipp_sponsor_id - enrollments_without_sponsor_id = InPersonEnrollment.where(sponsor_id: nil) - enrollments_without_sponsor_id_count = enrollments_without_sponsor_id.count - - warn("Found #{enrollments_without_sponsor_id_count} in_person_enrollments needing backfill") - - tally = 0 - enrollments_without_sponsor_id.in_batches(of: batch_size) do |batch| - tally += batch.update_all(sponsor_id: ipp_sponsor_id) # rubocop:disable Rails/SkipsModelValidations - warn("set sponsor_id for #{tally} in_person_enrollments") - end - warn("COMPLETE: Updated #{tally} in_person_enrollments") - - enrollments_without_sponsor_id = InPersonEnrollment.where(sponsor_id: nil) - enrollments_without_sponsor_id_count = enrollments_without_sponsor_id.count - warn("#{enrollments_without_sponsor_id_count} enrollments without a sponsor id") - end - end - - def batch_size - ENV['BATCH_SIZE'] ? ENV['BATCH_SIZE'].to_i : 1000 - end - - def with_timeout - timeout_in_seconds ||= if ENV['STATEMENT_TIMEOUT_IN_SECONDS'] - ENV['STATEMENT_TIMEOUT_IN_SECONDS'].to_i.seconds - else - 60.seconds - end - ActiveRecord::Base.transaction do - quoted_timeout = ActiveRecord::Base.connection.quote(timeout_in_seconds.in_milliseconds) - ActiveRecord::Base.connection.execute("SET statement_timeout = #{quoted_timeout}") - yield - end - end -end diff --git a/spec/controllers/idv/document_capture_controller_spec.rb b/spec/controllers/idv/document_capture_controller_spec.rb index 13e7f5b8712..7dca6ffd863 100644 --- a/spec/controllers/idv/document_capture_controller_spec.rb +++ b/spec/controllers/idv/document_capture_controller_spec.rb @@ -116,12 +116,14 @@ } end + let(:idv_vendor) { Idp::Constants::Vendors::LEXIS_NEXIS } + before do allow(IdentityConfig.store).to receive(:doc_auth_vendor).and_return( - Idp::Constants::Vendors::LEXIS_NEXIS, + idv_vendor, ) allow(IdentityConfig.store).to receive(:doc_auth_vendor_default).and_return( - Idp::Constants::Vendors::LEXIS_NEXIS, + idv_vendor, ) end @@ -130,6 +132,16 @@ expect(assigns(:presenter)).to be_kind_of(Idv::InPerson::UspsFormPresenter) end + context 'when we try to use this controller but we should be using the Socure version' do + let(:idv_vendor) { Idp::Constants::Vendors::SOCURE } + + it 'redirects to the Socure controller' do + get :show + + expect(response).to redirect_to idv_socure_document_capture_url + end + end + it 'renders the show template' do expect(subject).to receive(:render).with( :show, diff --git a/spec/controllers/idv/hybrid_mobile/document_capture_controller_spec.rb b/spec/controllers/idv/hybrid_mobile/document_capture_controller_spec.rb index 3f58041aa49..d3123a4335e 100644 --- a/spec/controllers/idv/hybrid_mobile/document_capture_controller_spec.rb +++ b/spec/controllers/idv/hybrid_mobile/document_capture_controller_spec.rb @@ -15,12 +15,16 @@ let(:document_capture_session_requested_at) { Time.zone.now } let(:document_capture_session_result_captured_at) { Time.zone.now + 1.second } let(:document_capture_session_result_success) { true } + let(:idv_vendor) { Idp::Constants::Vendors::MOCK } before do stub_analytics session[:doc_capture_user_id] = user&.id session[:document_capture_session_uuid] = document_capture_session_uuid + + allow(IdentityConfig.store).to receive(:doc_auth_vendor).and_return(idv_vendor) + allow(IdentityConfig.store).to receive(:doc_auth_vendor_default).and_return(idv_vendor) end describe 'before_actions' do @@ -55,6 +59,16 @@ } end + context 'when we try to use this controller but we should be using the Socure version' do + let(:idv_vendor) { Idp::Constants::Vendors::SOCURE } + + it 'redirects to the Socure controller' do + get :show + + expect(response).to redirect_to idv_hybrid_mobile_socure_document_capture_url + end + end + it 'renders the show template' do expect(subject).to receive(:render).with( :show, diff --git a/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb b/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb index 4fd74fcd0c7..d87e5061368 100644 --- a/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb +++ b/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb @@ -4,7 +4,7 @@ include FlowPolicyHelper let(:idv_vendor) { Idp::Constants::Vendors::SOCURE } - let(:fake_socure_endpoint) { 'https://fake-socure.com' } + let(:fake_socure_endpoint) { 'https://fake-socure.test' } let(:user) { create(:user) } let(:stored_result) { nil } let(:socure_enabled) { true } @@ -63,8 +63,17 @@ end end + context 'when we try to use this controller but we should be using the LN/mock version' do + let(:idv_vendor) { Idp::Constants::Vendors::LEXIS_NEXIS } + + it 'redirects to the LN/mock controller' do + get :show + expect(response).to redirect_to idv_hybrid_mobile_document_capture_url + end + end + context 'happy path' do - let(:response_redirect_url) { 'https://idv.test/dance' } + let(:socure_capture_app_url) { 'https://verify.socure.test/' } let(:docv_transaction_token) { '176dnc45d-2e34-46f3-82217-6f540ae90673' } let(:response_body) do { @@ -74,7 +83,7 @@ customerUserId: document_capture_session_uuid, docvTransactionToken: docv_transaction_token, qrCode: 'data:image/png;base64,iVBO......K5CYII=', - url: response_redirect_url, + url: socure_capture_app_url, }, } end @@ -82,6 +91,7 @@ before do allow(I18n).to receive(:locale).and_return(expected_language) allow(request_class).to receive(:new).and_call_original + allow(request_class).to receive(:handle_connection_error).and_call_original get(:show) end @@ -94,6 +104,11 @@ ) end + it 'sets DocumentCaptureSession socure_docv_capture_app_url value' do + document_capture_session.reload + expect(document_capture_session.socure_docv_capture_app_url).to eq(socure_capture_app_url) + end + context 'language is english' do let(:expected_language) { :en } @@ -105,7 +120,7 @@ config: { documentType: 'license', redirect: { - method: 'POST', + method: 'GET', url: idv_hybrid_mobile_socure_document_capture_url, }, language: expected_language, @@ -128,7 +143,7 @@ config: { documentType: 'license', redirect: { - method: 'POST', + method: 'GET', url: idv_hybrid_mobile_socure_document_capture_url, }, language: 'zh-cn', @@ -143,9 +158,9 @@ context 'renders the interstital page' do render_views - it 'it includes the socure redirect url' do + it 'response includes the socure capture app url' do expect(response).to have_http_status 200 - expect(response.body).to have_link(href: response_redirect_url) + expect(response.body).to have_link(href: socure_capture_app_url) end it 'puts the docvTransactionToken into the document capture session' do @@ -175,6 +190,74 @@ expect(response).to be_not_found end end + + context 'when socure error encountered' do + let(:fake_socure_endpoint) { 'https://fake-socure.test/' } + let(:failed_response_body) do + { 'status' => 'Error', + 'referenceId' => '1cff6d33-1cc0-4205-b740-c9a9e6b8bd66', + 'data' => {}, + 'msg' => 'No active account is associated with this request' } + end + let(:response_body_401) do + { + status: 'Error', + referenceId: '7ff0cdc5-395e-45d1-8467-0ff1b41c11dc', + msg: 'string', + } + end + let(:no_doc_found_response_body) do + { + referenceId: '0dc21b0d-04df-4dd5-8533-ec9ecdafe0f4', + msg: { + status: 400, + msg: 'No Documents found', + }, + } + end + before do + allow(IdentityConfig.store).to receive(:socure_document_request_endpoint). + and_return(fake_socure_endpoint) + end + it 'connection timeout still responds to user' do + stub_request(:post, fake_socure_endpoint).to_raise(Faraday::ConnectionFailed) + get(:show) + expect(response).to redirect_to(idv_unavailable_path) + end + + it 'socure error response still gives a result to user' do + stub_request(:post, fake_socure_endpoint).to_return( + status: 401, + body: JSON.generate(failed_response_body), + ) + get(:show) + expect(response).to redirect_to(idv_unavailable_path) + end + it 'socure nil response still gives a result to user' do + stub_request(:post, fake_socure_endpoint).to_return( + status: 500, + body: nil, + ) + get(:show) + expect(response).to redirect_to(idv_unavailable_path) + end + it 'socure nil response still gives a result to user' do + stub_request(:post, fake_socure_endpoint).to_return( + status: 401, + body: JSON.generate(response_body_401), + ) + get(:show) + expect(response).to redirect_to(idv_unavailable_path) + end + it 'socure nil response still gives a result to user' do + stub_request(:post, fake_socure_endpoint).to_return( + status: 401, + body: JSON.generate(no_doc_found_response_body), + ) + get(:show) + expect(response).to redirect_to(idv_unavailable_path) + end + end end describe '#update' do diff --git a/spec/controllers/idv/socure/document_capture_controller_spec.rb b/spec/controllers/idv/socure/document_capture_controller_spec.rb index 67525862117..6ecf9d88f05 100644 --- a/spec/controllers/idv/socure/document_capture_controller_spec.rb +++ b/spec/controllers/idv/socure/document_capture_controller_spec.rb @@ -4,9 +4,19 @@ include FlowPolicyHelper let(:idv_vendor) { Idp::Constants::Vendors::SOCURE } - let(:fake_socure_endpoint) { 'https://fake-socure.com' } + let(:fake_socure_endpoint) { 'https://fake-socure.test' } let(:user) { create(:user) } - let(:stored_result) { nil } + let(:doc_auth_success) { true } + let(:stored_result) do + DocumentCaptureSessionResult.new( + id: SecureRandom.uuid, + success: doc_auth_success, + doc_auth_success: doc_auth_success, + selfie_status: :none, + pii: { first_name: 'Testy', last_name: 'Testerson' }, + attention_with_barcode: false, + ) + end let(:socure_enabled) { true } let(:document_capture_session) do @@ -23,6 +33,7 @@ and_return(fake_socure_endpoint) allow(IdentityConfig.store).to receive(:doc_auth_vendor).and_return(idv_vendor) allow(IdentityConfig.store).to receive(:doc_auth_vendor_default).and_return(idv_vendor) + allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user) allow(subject).to receive(:stored_result).and_return(stored_result) @@ -65,8 +76,17 @@ subject.idv_session.document_capture_session_uuid = expected_uuid end + context 'when we try to use this controller but we should be using the LN/mock version' do + let(:idv_vendor) { Idp::Constants::Vendors::LEXIS_NEXIS } + + it 'redirects to the LN/mock controller' do + get :show + expect(response).to redirect_to idv_document_capture_url + end + end + context 'happy path' do - let(:response_redirect_url) { 'https://idv.test/dance' } + let(:socure_capture_app_url) { 'https://verify.socure.test/' } let(:docv_transaction_token) { '176dnc45d-2e34-46f3-82217-6f540ae90673' } let(:response_body) do { @@ -76,7 +96,7 @@ customerUserId: '121212', docvTransactionToken: docv_transaction_token, qrCode: 'data:image/png;base64,iVBO......K5CYII=', - url: response_redirect_url, + url: socure_capture_app_url, }, } end @@ -84,6 +104,7 @@ before do allow(request_class).to receive(:new).and_call_original allow(I18n).to receive(:locale).and_return(expected_language) + allow(DocumentCaptureSession).to receive(:find_by).and_return(document_capture_session) get(:show) end @@ -91,11 +112,16 @@ expect(request_class).to have_received(:new). with( document_capture_session_uuid: expected_uuid, - redirect_url: idv_socure_document_capture_url, + redirect_url: idv_socure_document_capture_update_url, language: expected_language, ) end + it 'sets DocumentCaptureSession socure_docv_capture_app_url value' do + document_capture_session.reload + expect(document_capture_session.socure_docv_capture_app_url).to eq(socure_capture_app_url) + end + context 'language is english' do let(:expected_language) { :en } @@ -107,8 +133,8 @@ config: { documentType: 'license', redirect: { - method: 'POST', - url: idv_socure_document_capture_url, + method: 'GET', + url: idv_socure_document_capture_update_url, }, language: :en, }, @@ -130,8 +156,8 @@ config: { documentType: 'license', redirect: { - method: 'POST', - url: idv_socure_document_capture_url, + method: 'GET', + url: idv_socure_document_capture_update_url, }, language: 'zh-cn', }, @@ -145,9 +171,9 @@ context 'renders the interstital page' do render_views - it 'it includes the socure redirect url' do + it 'response includes the socure capture app url' do expect(response).to have_http_status 200 - expect(response.body).to have_link(href: response_redirect_url) + expect(response.body).to have_link(href: socure_capture_app_url) end it 'puts the docvTransactionToken into the document capture session' do @@ -177,13 +203,91 @@ expect(response).to be_not_found end end + + context 'when socure error encountered' do + let(:fake_socure_endpoint) { 'https://fake-socure.test/' } + let(:failed_response_body) do + { 'status' => 'Error', + 'referenceId' => '1cff6d33-1cc0-4205-b740-c9a9e6b8bd66', + 'data' => {}, + 'msg' => 'No active account is associated with this request' } + end + let(:response_body_401) do + { + status: 'Error', + referenceId: '7ff0cdc5-395e-45d1-8467-0ff1b41c11dc', + msg: 'string', + } + end + let(:no_doc_found_response_body) do + { + referenceId: '0dc21b0d-04df-4dd5-8533-ec9ecdafe0f4', + msg: { + status: 400, + msg: 'No Documents found', + }, + } + end + before do + allow(IdentityConfig.store).to receive(:socure_document_request_endpoint). + and_return(fake_socure_endpoint) + end + it 'connection timeout still responds to user' do + stub_request(:post, fake_socure_endpoint).to_raise(Faraday::ConnectionFailed) + get(:show) + expect(response).to redirect_to(idv_unavailable_path) + end + + it 'socure error response still gives a result to user' do + stub_request(:post, fake_socure_endpoint).to_return( + status: 401, + body: JSON.generate(failed_response_body), + ) + get(:show) + expect(response).to redirect_to(idv_unavailable_path) + end + it 'socure nil response still gives a result to user' do + stub_request(:post, fake_socure_endpoint).to_return( + status: 500, + body: nil, + ) + get(:show) + expect(response).to redirect_to(idv_unavailable_path) + end + it 'socure nil response still gives a result to user' do + stub_request(:post, fake_socure_endpoint).to_return( + status: 401, + body: JSON.generate(response_body_401), + ) + get(:show) + expect(response).to redirect_to(idv_unavailable_path) + end + it 'socure nil response still gives a result to user' do + stub_request(:post, fake_socure_endpoint).to_return( + status: 401, + body: JSON.generate(no_doc_found_response_body), + ) + get(:show) + expect(response).to redirect_to(idv_unavailable_path) + end + end end describe '#update' do it 'returns OK (200)' do - post(:update) + get(:update) - expect(response).to have_http_status(:ok) + expect(response).to redirect_to(idv_ssn_path) + end + + context 'when doc auth fails' do + let(:doc_auth_success) { false } + + it 'redirects to document capture' do + get(:update) + + expect(response).to redirect_to(idv_socure_document_capture_path) + end end context 'when socure is disabled' do diff --git a/spec/controllers/sign_up/passwords_controller_spec.rb b/spec/controllers/sign_up/passwords_controller_spec.rb index bf20d2ce5eb..9d910e9bf57 100644 --- a/spec/controllers/sign_up/passwords_controller_spec.rb +++ b/spec/controllers/sign_up/passwords_controller_spec.rb @@ -3,6 +3,47 @@ RSpec.describe SignUp::PasswordsController do let(:token) { 'new token' } + describe '#new' do + let!(:user) { create(:user, :unconfirmed, confirmation_token: token) } + subject(:response) { get :new, params: { confirmation_token: token } } + + it 'flashes a message informing the user that they need to set a password' do + response + + expect(flash.now[:success]).to eq(t('devise.confirmations.confirmed_but_must_set_password')) + end + + it 'processes valid token' do + expect(controller).to receive(:process_valid_confirmation_token) + + response + end + + it 'assigns variables expected to be available in the view' do + response + + expect(assigns(:password_form)).to be_instance_of(PasswordForm) + expect(assigns(:email_address)).to be_instance_of(EmailAddress) + expect(assigns(:forbidden_passwords)).to be_present.and all be_kind_of(String) + expect(assigns(:confirmation_token)).to be_kind_of(String) + end + + context 'with invalid confirmation_token' do + let!(:user) do + create( + :user, + :unconfirmed, + confirmation_token: token, + confirmation_sent_at: (IdentityConfig.store.add_email_link_valid_for_hours + 1).hours.ago, + ) + end + + it 'redirects to sign up page' do + expect(response).to redirect_to(sign_up_register_url) + end + end + end + describe '#create' do subject(:response) { post :create, params: params } let(:params) do @@ -28,12 +69,6 @@ it 'tracks analytics' do subject - expect(@analytics).to have_logged_event( - 'User Registration: Email Confirmation', - success: true, - errors: {}, - user_id: user.uuid, - ) expect(@analytics).to have_logged_event( 'Password Creation', success: true, @@ -77,12 +112,6 @@ it 'tracks an invalid password event' do subject - expect(@analytics).to have_logged_event( - 'User Registration: Email Confirmation', - errors: {}, - success: true, - user_id: user.uuid, - ) expect(@analytics).to have_logged_event( 'Password Creation', success: false, @@ -111,12 +140,6 @@ it 'tracks invalid password_confirmation error' do subject - expect(@analytics).to have_logged_event( - 'User Registration: Email Confirmation', - errors: {}, - success: true, - user_id: user.uuid, - ) expect(@analytics).to have_logged_event( 'Password Creation', success: false, @@ -161,22 +184,4 @@ end end end - - describe '#new' do - render_views - - it 'rejects when confirmation_token is invalid' do - invalid_confirmation_sent_at = - Time.zone.now - (IdentityConfig.store.add_email_link_valid_for_hours.hours.in_seconds + 1) - create( - :user, - :unconfirmed, - confirmation_token: token, - confirmation_sent_at: invalid_confirmation_sent_at, - ) - - get :new, params: { confirmation_token: token } - expect(response).to redirect_to(sign_up_register_url) - end - end end diff --git a/spec/controllers/sign_up/registrations_controller_spec.rb b/spec/controllers/sign_up/registrations_controller_spec.rb index b815b87fd1d..c4611e90ffa 100644 --- a/spec/controllers/sign_up/registrations_controller_spec.rb +++ b/spec/controllers/sign_up/registrations_controller_spec.rb @@ -56,6 +56,36 @@ ) end end + + context 'with threatmetrix enabled' do + let(:tmx_session_id) { '1234' } + + before do + allow(FeatureManagement).to receive(:account_creation_device_profiling_collecting_enabled?). + and_return(true) + allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_org_id).and_return('org1') + allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_mock_enabled). + and_return(false) + subject.session[:threatmetrix_session_id] = tmx_session_id + end + + it 'renders new valid request' do + tmx_url = 'https://h.online-metrix.net/fp' + expect(subject).to receive(:render).with( + :new, + formats: :html, + locals: { threatmetrix_session_id: tmx_session_id, + threatmetrix_javascript_urls: + ["#{tmx_url}/tags.js?org_id=org1&session_id=#{tmx_session_id}"], + threatmetrix_iframe_url: + "#{tmx_url}/tags?org_id=org1&session_id=#{tmx_session_id}" }, + ).and_call_original + + get :new + + expect(response).to render_template(:new) + end + end end describe '#create' do @@ -172,5 +202,34 @@ expect(response).to render_template(:new) end + + context 'with threatmetrix enabled' do + let(:tmx_session_id) { '1234' } + + before do + allow(FeatureManagement).to receive(:account_creation_device_profiling_collecting_enabled?). + and_return(true) + allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_org_id).and_return('org1') + allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_mock_enabled). + and_return(false) + subject.session[:threatmetrix_session_id] = tmx_session_id + end + + it 'renders new with invalid request' do + tmx_url = 'https://h.online-metrix.net/fp' + expect(subject).to receive(:render).with( + :new, + locals: { threatmetrix_session_id: tmx_session_id, + threatmetrix_javascript_urls: + ["#{tmx_url}/tags.js?org_id=org1&session_id=#{tmx_session_id}"], + threatmetrix_iframe_url: + "#{tmx_url}/tags?org_id=org1&session_id=#{tmx_session_id}" }, + ).and_call_original + + post :create, params: params.deep_merge(user: { email: 'invalid@' }) + + expect(response).to render_template(:new) + end + end end end diff --git a/spec/controllers/sign_up/select_email_controller_spec.rb b/spec/controllers/sign_up/select_email_controller_spec.rb index 2e9a841af3d..73adfe3dbb4 100644 --- a/spec/controllers/sign_up/select_email_controller_spec.rb +++ b/spec/controllers/sign_up/select_email_controller_spec.rb @@ -59,6 +59,16 @@ expect(response).to be_not_found end end + + context 'with only one verified email address' do + let(:user) { create(:user) } + + it 'redirects to the sign up completed path' do + response + + expect(response).to redirect_to(sign_up_completed_path) + end + end end describe '#create' do diff --git a/spec/controllers/users/backup_code_setup_controller_spec.rb b/spec/controllers/users/backup_code_setup_controller_spec.rb index 5a6ca70cb1d..e5c8f26596d 100644 --- a/spec/controllers/users/backup_code_setup_controller_spec.rb +++ b/spec/controllers/users/backup_code_setup_controller_spec.rb @@ -15,11 +15,20 @@ end shared_examples 'valid backup codes creation' do + let(:threatmetrix_attrs) do + { + user_id: user.id, + request_ip: Faker::Internet.ip_v4_address, + threatmetrix_session_id: 'test-session', + email: user.email, + } + end + it 'creates backup codes and logs expected events' do stub_analytics allow(controller).to receive(:in_multi_mfa_selection_flow?).and_return(true) - Funnel::Registration::AddMfa.call(user.id, 'phone', @analytics) + Funnel::Registration::AddMfa.call(user.id, 'phone', @analytics, threatmetrix_attrs) expect(PushNotification::HttpPush).to receive(:deliver). with(PushNotification::RecoveryInformationChangedEvent.new(user: user)) diff --git a/spec/controllers/users/email_confirmations_controller_spec.rb b/spec/controllers/users/email_confirmations_controller_spec.rb index c6a7613e381..8d3197ad185 100644 --- a/spec/controllers/users/email_confirmations_controller_spec.rb +++ b/spec/controllers/users/email_confirmations_controller_spec.rb @@ -87,5 +87,58 @@ expect(flash[:error]).to eq t('errors.messages.confirmation_invalid_token') end end + + describe '#process_successful_confirmation' do + let(:user) { create(:user) } + + context 'adding an email from the account page' do + before do + stub_sign_in(user) + end + + it 'redirects to the account page' do + new_email = Faker::Internet.email + + add_email_form = AddUserEmailForm.new + add_email_form.submit(user, email: new_email) + email_record = add_email_form.email_address_record(new_email) + + get :create, params: { confirmation_token: email_record.reload.confirmation_token } + + expect(response).to redirect_to(account_url) + end + end + + context 'adding an email from the service provider consent flow' do + let(:confirmation_token) { 'token' } + let(:sp_request_uuid) { 'request-id' } + let(:request_id_param) {} + + before do + stub_sign_in(user) + ServiceProviderRequestProxy.create( + issuer: 'http://localhost:3000', + url: '', + uuid: sp_request_uuid, + ial: '1', + acr_values: Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, + ) + end + + it 'adds an email from the service provider consent flow' do + new_email = Faker::Internet.email + add_email_form = AddUserEmailForm.new + add_email_form.submit(user, email: new_email, request_id: sp_request_uuid) + email_record = add_email_form.email_address_record(new_email) + + get :create, params: { + confirmation_token: email_record.reload.confirmation_token, + request_id: sp_request_uuid, + } + + expect(response).to redirect_to(sign_up_select_email_url) + end + end + end end end diff --git a/spec/controllers/users/webauthn_setup_controller_spec.rb b/spec/controllers/users/webauthn_setup_controller_spec.rb index 2756a6a9815..d6416d45ae4 100644 --- a/spec/controllers/users/webauthn_setup_controller_spec.rb +++ b/spec/controllers/users/webauthn_setup_controller_spec.rb @@ -88,6 +88,15 @@ } end + let(:threatmetrix_attrs) do + { + user_id: user.id, + request_ip: Faker::Internet.ip_v4_address, + threatmetrix_session_id: 'test-session', + email: user.email, + } + end + before do allow(IdentityConfig.store).to receive(:domain_name).and_return('localhost:3000') request.host = 'localhost:3000' @@ -95,7 +104,7 @@ end it 'tracks the submission' do - Funnel::Registration::AddMfa.call(user.id, 'phone', @analytics) + Funnel::Registration::AddMfa.call(user.id, 'phone', @analytics, threatmetrix_attrs) patch :confirm, params: params @@ -229,12 +238,21 @@ } end + let(:threatmetrix_attrs) do + { + user_id: user.id, + request_ip: Faker::Internet.ip_v4_address, + threatmetrix_session_id: 'test-session', + email: user.email, + } + end + before do controller.user_session[:in_account_creation_flow] = true end it 'should log expected events' do - Funnel::Registration::AddMfa.call(user.id, 'phone', @analytics) + Funnel::Registration::AddMfa.call(user.id, 'phone', @analytics, threatmetrix_attrs) patch :confirm, params: params @@ -383,8 +401,17 @@ controller.user_session[:mfa_attempts] = { auth_method: 'webauthn', attempts: 1 } end + let(:threatmetrix_attrs) do + { + user_id: user.id, + request_ip: Faker::Internet.ip_v4_address, + threatmetrix_session_id: 'test-session', + email: user.email, + } + end + it 'tracks the submission' do - Funnel::Registration::AddMfa.call(user.id, 'phone', @analytics) + Funnel::Registration::AddMfa.call(user.id, 'phone', @analytics, threatmetrix_attrs) patch :confirm, params: params diff --git a/spec/features/account_connected_apps_spec.rb b/spec/features/account_connected_apps_spec.rb index b3be84b37a5..70a0254b563 100644 --- a/spec/features/account_connected_apps_spec.rb +++ b/spec/features/account_connected_apps_spec.rb @@ -87,7 +87,7 @@ expect(page).to have_field(user.email) { |field| !field[:checked] } choose user.email - click_on t('help_text.requested_attributes.change_email_link') + click_on t('help_text.requested_attributes.select_email_link') within('li', text: identity.display_name) do expect(page).not_to have_content(t('account.connected_apps.email_not_selected')) @@ -97,7 +97,7 @@ expect(page).to have_field(user.email) { |field| field[:checked] } - click_on(t('help_text.requested_attributes.change_email_link')) + click_on(t('help_text.requested_attributes.select_email_link')) expect(page).to have_content strip_tags( t('account.connected_apps.email_update_success_html', sp_name: identity.display_name), diff --git a/spec/features/account_creation/threat_metrix_spec.rb b/spec/features/account_creation/threat_metrix_spec.rb new file mode 100644 index 00000000000..0a17582b3d1 --- /dev/null +++ b/spec/features/account_creation/threat_metrix_spec.rb @@ -0,0 +1,39 @@ +require 'rails_helper' + +RSpec.feature 'ThreatMetrix in account creation', :js do + before do + allow(IdentityConfig.store).to receive(:account_creation_device_profiling).and_return(:enabled) + allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_org_id).and_return('test_org') + end + + it 'logs the threatmetrix result once the account is fully registered' do + visit root_url + click_on t('links.create_account') + fill_in t('forms.registration.labels.email'), with: Faker::Internet.email + check t('sign_up.terms', app_name: APP_NAME) + select 'Reject', from: :mock_profiling_result + click_button t('forms.buttons.submit.default') + user = confirm_last_user + set_password(user) + fake_analytics = FakeAnalytics.new + expect_any_instance_of(AccountCreationThreatMetrixJob).to receive(:analytics).with(user). + and_return(fake_analytics) + select_2fa_option('backup_code') + click_continue + + expect(fake_analytics).to have_logged_event( + :account_creation_tmx_result, + account_lex_id: 'super-cool-test-lex-id', + errors: { review_status: ['reject'] }, + response_body: { + **JSON.parse(LexisNexisFixtures.ddp_success_redacted_response_json), + 'review_status' => 'reject', + }, + review_status: 'reject', + session_id: 'super-cool-test-session-id', + success: true, + timed_out: false, + transaction_id: 'ddp-mock-transaction-id-123', + ) + end +end diff --git a/spec/features/idv/hybrid_mobile/entry_spec.rb b/spec/features/idv/hybrid_mobile/entry_spec.rb index 9b252f78109..4261ce82806 100644 --- a/spec/features/idv/hybrid_mobile/entry_spec.rb +++ b/spec/features/idv/hybrid_mobile/entry_spec.rb @@ -22,6 +22,10 @@ let(:link_to_visit) { link_sent_via_sms } context 'valid link' do + before do + allow(IdentityConfig.store).to receive(:socure_enabled).and_return(true) + end + it 'puts the user on the document capture page' do expect(link_to_visit).to be @@ -29,6 +33,11 @@ visit link_to_visit # Should have redirected to the actual doc capture url expect(current_url).to eql(idv_hybrid_mobile_document_capture_url) + + # Confirm that we end up on the LN / Mock page even if we try to + # go to the Socure one. + visit idv_hybrid_mobile_socure_document_capture_url + expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url) end end end diff --git a/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb b/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb index 9456bb8a772..5b53406d281 100644 --- a/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb +++ b/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb @@ -10,6 +10,7 @@ before do allow(FeatureManagement).to receive(:doc_capture_polling_enabled?).and_return(true) + allow(IdentityConfig.store).to receive(:socure_enabled).and_return(true) allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true) allow(Telephony).to receive(:send_doc_auth_link).and_wrap_original do |impl, config| @sms_link = config[:link] @@ -44,7 +45,11 @@ # Confirm that jumping to LinkSent page does not cause errors visit idv_link_sent_url expect(page).to have_current_path(root_url) - visit idv_hybrid_mobile_document_capture_url + + # Confirm that we end up on the LN / Mock page even if we try to + # go to the Socure one. + visit idv_hybrid_mobile_socure_document_capture_url + expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url) # Confirm that clicking cancel and then coming back doesn't cause errors click_link 'Cancel' diff --git a/spec/features/multiple_emails/sp_sign_in_spec.rb b/spec/features/multiple_emails/sp_sign_in_spec.rb index 33b0a5fc6f8..5b9f6e33992 100644 --- a/spec/features/multiple_emails/sp_sign_in_spec.rb +++ b/spec/features/multiple_emails/sp_sign_in_spec.rb @@ -36,7 +36,7 @@ choose emails.second - click_button(t('help_text.requested_attributes.change_email_link')) + click_button(t('help_text.requested_attributes.select_email_link')) expect(current_path).to eq(sign_up_completed_path) click_agree_and_continue @@ -55,7 +55,7 @@ click_submit_default click_link(t('help_text.requested_attributes.change_email_link')) choose email2.email - click_button(t('help_text.requested_attributes.change_email_link')) + click_button(t('help_text.requested_attributes.select_email_link')) expect(current_path).to eq(sign_up_completed_path) click_agree_and_continue click_submit_default @@ -103,7 +103,7 @@ click_link(t('help_text.requested_attributes.change_email_link')) choose emails.second - click_button(t('help_text.requested_attributes.change_email_link')) + click_button(t('help_text.requested_attributes.select_email_link')) expect(current_path).to eq(sign_up_completed_path) @@ -127,7 +127,7 @@ click_submit_default_twice click_link(t('help_text.requested_attributes.change_email_link')) choose email2.email - click_button(t('help_text.requested_attributes.change_email_link')) + click_button(t('help_text.requested_attributes.select_email_link')) expect(current_path).to eq(sign_up_completed_path) click_agree_and_continue click_submit_default diff --git a/spec/features/visitors/email_confirmation_spec.rb b/spec/features/visitors/email_confirmation_spec.rb index 1925545deca..299234123c6 100644 --- a/spec/features/visitors/email_confirmation_spec.rb +++ b/spec/features/visitors/email_confirmation_spec.rb @@ -18,6 +18,7 @@ end scenario 'confirms valid email and sets valid password' do + stub_analytics reset_email email = 'test@example.com' sign_up_with(email) @@ -28,6 +29,9 @@ expect(page).to have_title t('titles.confirmations.show') expect(page).to have_content t('forms.confirmation.show_hdr') + # Regression: Previously, this event had been logged multiple times per confirmation. + expect(@analytics).to have_logged_event('User Registration: Email Confirmation').once + fill_in t('forms.password'), with: Features::SessionHelper::VALID_PASSWORD fill_in t('components.password_confirmation.confirm_label'), with: Features::SessionHelper::VALID_PASSWORD diff --git a/spec/fixtures/proofing/lexis_nexis/ddp/account_creation_request.json b/spec/fixtures/proofing/lexis_nexis/ddp/account_creation_request.json new file mode 100644 index 00000000000..9d638cc1748 --- /dev/null +++ b/spec/fixtures/proofing/lexis_nexis/ddp/account_creation_request.json @@ -0,0 +1,27 @@ +{ + "api_key": "test_api_key", + "org_id": "test_org_id", + "account_email": "test@example.com", + "event_type": "ACCOUNT_CREATION", + "policy": "test-authentication-policy", + "service_type": "all", + "session_id": "UNIQUE_SESSION_ID", + "input_ip_address": "127.0.0.1", + "account_address_street1": "", + "account_address_street2": "", + "account_address_city": "", + "account_address_state": "", + "account_address_country": "", + "account_address_zip": "", + "account_date_of_birth": "", + "account_first_name": "", + "account_last_name": "", + "account_telephone": "", + "account_drivers_license_issuer": "", + "account_drivers_license_number": "", + "account_drivers_license_type": "", + "national_id_number": "", + "national_id_type": "", + "local_attrib_1": "" + } + \ No newline at end of file diff --git a/spec/fixtures/proofing/lexis_nexis/ddp/error_response.json b/spec/fixtures/proofing/lexis_nexis/ddp/error_response.json index 0de8a11bf99..0fea4b30420 100644 --- a/spec/fixtures/proofing/lexis_nexis/ddp/error_response.json +++ b/spec/fixtures/proofing/lexis_nexis/ddp/error_response.json @@ -4,4 +4,4 @@ "request_result":"fail_invalid_parameter", "review_status":"REVIEW_STATUS", "tmx_summary_reason_code": ["Identity_Negative_History"] -} +} \ No newline at end of file diff --git a/spec/fixtures/proofing/lexis_nexis/ddp/failed_response.json b/spec/fixtures/proofing/lexis_nexis/ddp/failed_response.json new file mode 100644 index 00000000000..891067d6928 --- /dev/null +++ b/spec/fixtures/proofing/lexis_nexis/ddp/failed_response.json @@ -0,0 +1,7 @@ +{ + "error_detail": "service_type", + "request_id":"1234-abcd", + "request_result":"fail_invalid_parameter", + "review_status":"reject", + "tmx_summary_reason_code": ["Identity_Negative_History"] +} diff --git a/spec/helpers/threat_metrix_helper_spec.rb b/spec/helpers/threat_metrix_helper_spec.rb new file mode 100644 index 00000000000..d0cf955a2b0 --- /dev/null +++ b/spec/helpers/threat_metrix_helper_spec.rb @@ -0,0 +1,77 @@ +require 'rails_helper' + +RSpec.describe ThreatMetrixHelper do + include ThreatMetrixHelper + + describe '#threatmetrix_javascript_urls' do + let(:session_id) { '1234' } + before do + allow(IdentityConfig.store). + to receive(:lexisnexis_threatmetrix_org_id). + and_return('test_id') + + allow(Rails.application.config.asset_sources).to receive(:get_sources). + with('mock-device-profiling').and_return(['/mock-device-profiling.js']) + end + context 'mock is enabled' do + before do + allow(IdentityConfig.store). + to receive(:lexisnexis_threatmetrix_mock_enabled). + and_return(true) + end + it 'should return mock config source' do + sources = threatmetrix_javascript_urls(session_id) + expect(sources).to eq(['/mock-device-profiling.js?org_id=test_id&session_id=1234']) + end + end + context 'mock is not enabled' do + before do + allow(IdentityConfig.store). + to receive(:lexisnexis_threatmetrix_mock_enabled). + and_return(false) + end + it 'should return actual url' do + javascript_sources = threatmetrix_javascript_urls(session_id) + expect(javascript_sources). + to eq(['https://h.online-metrix.net/fp/tags.js?org_id=test_id&session_id=1234']) + end + end + end + + describe '#threatmetrix_iframe_url' do + let(:session_id) { '1234' } + before do + allow(IdentityConfig.store). + to receive(:lexisnexis_threatmetrix_org_id). + and_return('test_id') + + allow(Rails.application.config.asset_sources).to receive(:get_sources). + with('mock-device-profiling').and_return(['/mock-device-profiling.js']) + end + context 'mock is enabled' do + before do + allow(IdentityConfig.store). + to receive(:lexisnexis_threatmetrix_mock_enabled). + and_return(true) + end + it 'should return mock javascript config' do + iframe_sources = threatmetrix_iframe_url(session_id) + expect(iframe_sources). + to eq('http://www.example.com/test/device_profiling?org_id=test_id&session_id=1234') + end + end + + context 'mock is not enabled' do + before do + allow(IdentityConfig.store). + to receive(:lexisnexis_threatmetrix_mock_enabled). + and_return(false) + end + it 'should return mock config source' do + iframe_sources = threatmetrix_iframe_url(session_id) + expect(iframe_sources). + to eq('https://h.online-metrix.net/fp/tags?org_id=test_id&session_id=1234') + end + end + end +end diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb index 370714691d6..220d6842536 100644 --- a/spec/i18n_spec.rb +++ b/spec/i18n_spec.rb @@ -70,7 +70,6 @@ class BaseTask { key: 'simple_form.no', locales: %i[es] }, # "No" is "No" in Spanish { key: 'telephony.format_length.six', locales: %i[zh] }, # numeral is not translated { key: 'telephony.format_length.ten', locales: %i[zh] }, # numeral is not translated - { key: 'time.formats.event_date', locales: %i[es zh] }, { key: 'time.formats.event_time', locales: %i[es zh] }, { key: 'time.formats.event_timestamp', locales: %i[zh] }, { key: 'time.formats.full_date', locales: %i[es] }, # format is the same in Spanish and English diff --git a/spec/jobs/account_creation_threat_metrix_job_spec.rb b/spec/jobs/account_creation_threat_metrix_job_spec.rb new file mode 100644 index 00000000000..cffb09cfda7 --- /dev/null +++ b/spec/jobs/account_creation_threat_metrix_job_spec.rb @@ -0,0 +1,118 @@ +require 'rails_helper' + +RSpec.describe AccountCreationThreatMetrixJob, type: :job do + let(:user) { create(:user, :fully_registered) } + let(:request_ip) { Faker::Internet.ip_v4_address } + let(:threatmetrix_session_id) { SecureRandom.uuid } + let(:authentication_device_profiling) { :collect_only } + let(:lexisnexis_threatmetrix_mock_enabled) { false } + let(:threatmetrix_response) { LexisNexisFixtures.ddp_success_response_json } + let(:threatmetrix_stub) { stub_threatmetrix_request(threatmetrix_response) } + let(:job_analytics) { FakeAnalytics.new } + + before do + allow(IdentityConfig.store).to receive(:account_creation_device_profiling). + and_return(authentication_device_profiling) + allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_mock_enabled). + and_return(lexisnexis_threatmetrix_mock_enabled) + allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_base_url). + and_return('https://www.example.com') + allow(instance).to receive(:analytics).and_return(job_analytics) + end + + describe '#perform' do + let(:instance) { AccountCreationThreatMetrixJob.new } + + subject(:perform) do + instance.perform( + user_id: user.id, + threatmetrix_session_id: threatmetrix_session_id, + request_ip: request_ip, + ) + end + + context 'Threat Metrix Account Creation analysis passes' do + let(:threatmetrix_response) { LexisNexisFixtures.ddp_success_response_json } + it 'logs a successful result' do + threatmetrix_stub + + perform + + expect(job_analytics).to have_logged_event( + :account_creation_tmx_result, + hash_including( + success: true, + review_status: 'pass', + ), + ) + end + end + + context 'with an error response result' do + let(:threatmetrix_response) { LexisNexisFixtures.ddp_failure_response_json } + + it 'stores an unsuccessful result' do + threatmetrix_stub + + perform + + expect(job_analytics).to have_logged_event( + :account_creation_tmx_result, + hash_including( + success: false, + review_status: 'reject', + ), + ) + end + end + + context 'with threatmetrix disabled' do + let(:authentication_device_profiling) { :disabled } + + it 'does not make a request to threatmetrix' do + threatmetrix_stub + + perform + + expect(threatmetrix_stub).to_not have_been_requested + expect(job_analytics).to have_logged_event( + :account_creation_tmx_result, + hash_including( + success: true, + client: 'tmx_disabled', + review_status: 'pass', + ), + ) + end + end + + context 'without a threatmetrix session ID' do + let(:threatmetrix_session_id) { nil } + let(:ipp_enrollment_in_progress) { false } + let(:threatmetrix_response) { LexisNexisFixtures.ddp_failure_response_json } + + it 'does not make a request to threatmetrix' do + threatmetrix_stub + + perform + + expect(threatmetrix_stub).to_not have_been_requested + expect(job_analytics).to have_logged_event( + :account_creation_tmx_result, + hash_including( + success: false, + client: 'tmx_session_id_missing', + review_status: 'reject', + ), + ) + end + end + end + + def stub_threatmetrix_request(threatmetrix_response) + stub_request( + :post, + 'https://www.example.com/api/session-query', + ).to_return(body: threatmetrix_response) + end +end diff --git a/spec/lib/tasks/backfill_sponsor_id_rake_spec.rb b/spec/lib/tasks/backfill_sponsor_id_rake_spec.rb deleted file mode 100644 index 9f26042b370..00000000000 --- a/spec/lib/tasks/backfill_sponsor_id_rake_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'rails_helper' -require 'rake' - -RSpec.describe 'in_person_enrollments:backfill_sponsor_id rake task' do - let!(:task) do - Rake.application.rake_require 'tasks/backfill_sponsor_id' - Rake::Task.define_task(:environment) - Rake::Task['in_person_enrollments:backfill_sponsor_id'] - end - - subject(:invoke_task) do - actual_stderr = $stderr - proxy_stderr = StringIO.new - begin - $stderr = proxy_stderr - task.reenable - task.invoke - proxy_stderr.string - ensure - $stderr = actual_stderr - end - end - - let(:pending_enrollment) { create(:in_person_enrollment, :pending) } - let(:expired_enrollment) { create(:in_person_enrollment, :expired) } - let(:failed_enrollment) { create(:in_person_enrollment, :failed) } - let(:enrollment_with_service_provider) do - create(:in_person_enrollment, :with_service_provider) - end - let(:enrollment_with_sponsor_id) { create(:in_person_enrollment) } - - before do - allow(IdentityConfig.store).to receive(:usps_ipp_sponsor_id).and_return('31459') - end - - it 'does not change the value of an existing sponsor id' do - original_sponsor_id = enrollment_with_sponsor_id.sponsor_id - subject - expect(enrollment_with_sponsor_id.sponsor_id).to eq(original_sponsor_id) - end - - it 'sets a sponsor id that is a string' do - subject - enrollments = InPersonEnrollment.all - enrollments.each do |enrollment| - expect(enrollment.sponsor_id).to be_a String - end - end -end diff --git a/spec/mailers/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb index ac6dc41e16e..5aa21140cc6 100644 --- a/spec/mailers/previews/user_mailer_preview.rb +++ b/spec/mailers/previews/user_mailer_preview.rb @@ -136,7 +136,8 @@ def verify_by_mail_letter_requested end def add_email - UserMailer.with(user: user, email_address: email_address_record).add_email(SecureRandom.hex) + UserMailer.with(user: user, email_address: email_address_record). + add_email(token: SecureRandom.hex, request_id: nil) end def email_added diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 71f615af5fe..fef65205fe3 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -32,7 +32,10 @@ describe '#add_email' do let(:token) { SecureRandom.hex } - let(:mail) { UserMailer.with(user: user, email_address: email_address).add_email(token) } + let(:mail) do + UserMailer.with(user: user, email_address: email_address). + add_email(token: token, request_id: nil, from_select_email_flow: nil) + end it_behaves_like 'a system email' it_behaves_like 'an email that respects user email locale preference' @@ -47,7 +50,8 @@ context 'when user adds email from select email flow' do let(:token) { SecureRandom.hex } let(:mail) do - UserMailer.with(user: user, email_address: email_address).add_email(token, true) + UserMailer.with(user: user, email_address: email_address). + add_email(token: token, request_id: nil, from_select_email_flow: true) end it 'renders the add_email_confirmation_url' do diff --git a/spec/presenters/idv/in_person/ready_to_verify_presenter_spec.rb b/spec/presenters/idv/in_person/ready_to_verify_presenter_spec.rb index 21bb5c2e455..aae5df981fb 100644 --- a/spec/presenters/idv/in_person/ready_to_verify_presenter_spec.rb +++ b/spec/presenters/idv/in_person/ready_to_verify_presenter_spec.rb @@ -23,21 +23,33 @@ ) end subject(:presenter) { described_class.new(enrollment: enrollment) } - describe '#formatted_due_date' do - subject(:formatted_due_date) { presenter.formatted_due_date } - - around do |example| - Time.use_zone('UTC') { example.run } - end - it 'returns a formatted due date' do - expect(formatted_due_date).to eq 'August 12, 2023' + describe '#formatted_due_date' do + let(:enrollment_established_at) { DateTime.new(2024, 7, 5) } + + context 'when the enrollment has an enrollment_established_at time' do + [ + ['English', :en, 'August 3, 2024'], + ['Spanish', :es, '3 de agosto de 2024'], + ['French', :fr, '3 août 2024'], + ['Chinese', :zh, '2024年8月3日'], + ].each do |language, locale, expected| + context "when locale is #{language}" do + before do + I18n.locale = locale + end + + it "returns the formatted due date in #{language}" do + expect(presenter.formatted_due_date).to eq(expected) + end + end + end end - context 'there is no enrollment_established_at' do + context 'when the enrollment does not have an enrollment_established_at time' do let(:enrollment_established_at) { nil } it 'returns formatted due date when no enrollment_established_at' do - expect(formatted_due_date).to eq 'July 13, 2023' + expect(presenter.formatted_due_date).to eq 'July 13, 2023' end end end diff --git a/spec/presenters/idv/in_person/verification_results_email_presenter_spec.rb b/spec/presenters/idv/in_person/verification_results_email_presenter_spec.rb index e320151a0a7..563a1f3d620 100644 --- a/spec/presenters/idv/in_person/verification_results_email_presenter_spec.rb +++ b/spec/presenters/idv/in_person/verification_results_email_presenter_spec.rb @@ -31,13 +31,25 @@ end describe '#formatted_verified_date' do - around do |example| - Time.use_zone('UTC') { example.run } + before do + enrollment.update(status_updated_at: DateTime.new(2024, 7, 5)) end - it 'returns a formatted verified date' do - enrollment.update(status_updated_at: status_updated_at) - expect(presenter.formatted_verified_date).to eq 'July 13, 2022' + [ + ['English', :en, 'July 4, 2024'], + ['Spanish', :es, '4 de julio de 2024'], + ['French', :fr, '4 juillet 2024'], + ['Chinese', :zh, '2024年7月4日'], + ].each do |language, locale, expected| + context "when locale is #{language}" do + before do + I18n.locale = locale + end + + it "returns the formatted due date in #{language}" do + expect(presenter.formatted_verified_date).to eq(expected) + end + end end end diff --git a/spec/services/account_creation/device_profiling_spec.rb b/spec/services/account_creation/device_profiling_spec.rb new file mode 100644 index 00000000000..3178f51170e --- /dev/null +++ b/spec/services/account_creation/device_profiling_spec.rb @@ -0,0 +1,66 @@ +require 'rails_helper' + +RSpec.describe AccountCreation::DeviceProfiling do + let(:threatmetrix_session_id) { '13232' } + let(:threatmetrix_proofer_result) do + instance_double(Proofing::DdpResult, success?: true, transaction_id: 'ddp-123') + end + let(:threatmetrix_proofer) do + instance_double( + Proofing::LexisNexis::Ddp::Proofer, + proof: threatmetrix_proofer_result, + ) + end + + subject(:device_profiling) { described_class.new } + + describe '#proof' do + before do + allow(device_profiling).to receive(:proofer).and_return(threatmetrix_proofer) + end + + subject(:result) do + device_profiling.proof( + request_ip: Faker::Internet.ip_v4_address, + threatmetrix_session_id: threatmetrix_session_id, + user_email: Faker::Internet.email, + ) + end + + context 'ThreatMetrix is enabled' do + before do + allow(IdentityConfig.store).to receive(:account_creation_device_profiling). + and_return(:collect_only) + allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_mock_enabled). + and_return(false) + end + + context 'session id is missing' do + let(:threatmetrix_session_id) { nil } + + it 'does not make a request to the ThreatMetrix proofer' do + result + expect(threatmetrix_proofer).not_to have_received(:proof) + end + + it 'returns a failed result' do + expect(result.success?).to be(false) + expect(result.client).to eq('tmx_session_id_missing') + expect(result.review_status).to eq('reject') + end + end + + context 'valid threatmetrix input' do + it 'makes a request to the ThreatMetrix proofer' do + result + expect(threatmetrix_proofer).to have_received(:proof) + end + + it 'returns a passed result' do + expect(result.success?).to be(true) + expect(result.transaction_id).to eq('ddp-123') + end + end + end + end +end diff --git a/spec/services/doc_auth/socure/request_spec.rb b/spec/services/doc_auth/socure/request_spec.rb index feedb8318f8..10f2cc1fad1 100644 --- a/spec/services/doc_auth/socure/request_spec.rb +++ b/spec/services/doc_auth/socure/request_spec.rb @@ -10,7 +10,7 @@ end describe '#fetch' do - let(:fake_socure_endpoint) { 'https://fake-socure.com/' } + let(:fake_socure_endpoint) { 'https://fake-socure.test/' } let(:fake_metric_name) { 'fake metric' } before do @@ -40,8 +40,10 @@ let(:response) { nil } let(:response_status) { 403 } - it 'returns {}' do - expect(request.fetch).to eq({}) + # Because we have not implemented handle_connection_error at this level + # (defined in docv_result and document_request) + it 'raises a NotImplementedError' do + expect { request.fetch }.to raise_error NotImplementedError end end end diff --git a/spec/services/doc_auth/socure/requests/document_request_spec.rb b/spec/services/doc_auth/socure/requests/document_request_spec.rb index 46db7c5ed90..17c8032118b 100644 --- a/spec/services/doc_auth/socure/requests/document_request_spec.rb +++ b/spec/services/doc_auth/socure/requests/document_request_spec.rb @@ -15,7 +15,7 @@ describe '#fetch' do let(:document_type) { 'license' } - let(:fake_socure_endpoint) { 'https://fake-socure.com/' } + let(:fake_socure_endpoint) { 'https://fake-socure.test/' } let(:fake_socure_document_capture_app_url) { 'https://verify.socure.us/something' } let(:docv_transaction_token) { 'fake docv transaction token' } let(:fake_socure_response) do @@ -38,7 +38,7 @@ documentType: document_type, redirect: { - method: 'POST', + method: 'GET', url: redirect_url, }, language: language, @@ -99,5 +99,31 @@ expect { document_request.fetch }.not_to raise_error end end + context 'with timeout exception' do + let(:response) { nil } + let(:response_status) { 403 } + let(:faraday_connection_failed_exception) { Faraday::ConnectionFailed } + + before do + stub_request(:post, fake_socure_endpoint).to_raise(faraday_connection_failed_exception) + end + it 'expect handle_connection_error method to be called' do + connection_error_attributes = { + success: false, + errors: { network: true }, + exception: faraday_connection_failed_exception, + extra: { + vendor: 'Socure', + vendor_status_code: nil, + vendor_status_message: nil, + }.compact, + } + result = document_request.fetch + expect(result[:success]).to eq(connection_error_attributes[:success]) + expect(result[:errors]).to eq(connection_error_attributes[:errors]) + expect(result[:exception]).to be_a Faraday::ConnectionFailed + expect(result[:extra]).to eq(connection_error_attributes[:extra]) + end + end end end diff --git a/spec/services/doc_auth/socure/requests/docv_result_request_spec.rb b/spec/services/doc_auth/socure/requests/docv_result_request_spec.rb new file mode 100644 index 00000000000..2fe380431d8 --- /dev/null +++ b/spec/services/doc_auth/socure/requests/docv_result_request_spec.rb @@ -0,0 +1,60 @@ +require 'rails_helper' + +RSpec.describe DocAuth::Socure::Requests::DocvResultRequest do + let(:document_capture_session_uuid) { 'fake uuid' } + let(:biometric_comparison_required) { false } + + subject(:docv_result_request) do + described_class.new( + document_capture_session_uuid:, + biometric_comparison_required: biometric_comparison_required, + ) + end + + describe '#fetch' do + let(:fake_socure_endpoint) { 'https://fake-socure.test/' } + let(:fake_socure_api_endpoint) { 'https://fake-socure.test/api/3.0/EmailAuthScore' } + let(:docv_transaction_token) { 'fake docv transaction token' } + let(:user) { create(:user) } + let(:document_capture_session) do + DocumentCaptureSession.create(user:).tap do |dcs| + dcs.socure_docv_transaction_token = docv_transaction_token + end + end + + before do + allow(IdentityConfig.store).to receive(:socure_idplus_base_url). + and_return(fake_socure_endpoint) + allow(DocumentCaptureSession).to receive(:find_by).and_return(document_capture_session) + end + + context 'with socure failures' do + let(:fake_socure_response) { {} } + let(:fake_socure_status) { 500 } + + it 'expect correct doc auth response during a connection failure' do + stub_request(:post, fake_socure_api_endpoint).to_raise(Faraday::ConnectionFailed) + response_hash = docv_result_request.fetch.to_h + expect(response_hash[:success]).to eq(false) + expect(response_hash[:errors]).to eq({ network: true }) + expect(response_hash[:vendor]).to eq('Socure') + expect(response_hash[:exception]).to be_a(Faraday::ConnectionFailed) + end + + it 'expect correct doc auth response for a socure fail response' do + stub_request(:post, fake_socure_api_endpoint). + to_return( + status: fake_socure_status, + body: JSON.generate(fake_socure_response), + ) + response_hash = docv_result_request.fetch.to_h + expect(response_hash[:success]).to eq(false) + expect(response_hash[:errors]).to eq({ network: true }) + expect(response_hash[:errors]).to eq({ network: true }) + expect(response_hash[:vendor]).to eq('Socure') + expect(response_hash[:exception]).to be_a(DocAuth::RequestError) + expect(response_hash[:exception].message).to include('Unexpected HTTP response 500') + end + end + end +end diff --git a/spec/services/form_response_spec.rb b/spec/services/form_response_spec.rb index 5efcefb17ac..dbd1880aa64 100644 --- a/spec/services/form_response_spec.rb +++ b/spec/services/form_response_spec.rb @@ -273,6 +273,21 @@ end end + describe '#to_hash' do + it 'allows for splatting response as alias of #to_h' do + errors = ActiveModel::Errors.new(build_stubbed(:user)) + errors.add(:email_language, :blank, message: 'Language cannot be blank') + response = FormResponse.new(success: false, errors:, serialize_error_details_only: true) + + expect(**response).to eq( + success: false, + error_details: { + email_language: { blank: true }, + }, + ) + end + end + describe '#extra' do it 'returns the extra hash' do extra = { foo: 'bar' } diff --git a/spec/services/funnel/registration/add_mfa_spec.rb b/spec/services/funnel/registration/add_mfa_spec.rb index 58d977ff077..d6f21fc19ed 100644 --- a/spec/services/funnel/registration/add_mfa_spec.rb +++ b/spec/services/funnel/registration/add_mfa_spec.rb @@ -3,20 +3,39 @@ RSpec.describe Funnel::Registration::AddMfa do let(:analytics) { FakeAnalytics.new } subject { described_class } + let(:user) { create(:user) } - let(:user_id) do - user = create(:user) - user.id - end + let(:user_id) { user.id } let(:funnel) { RegistrationLog.first } + let(:threatmetrix_attrs) do + { + user_id: user_id, + request_ip: Faker::Internet.ip_v4_address, + threatmetrix_session_id: SecureRandom.uuid, + email: user.email, + } + end + it 'shows user is not fully registered with no mfa' do expect(funnel&.registered_at).to_not be_present end it 'shows user is fully registered after adding an mfa' do - subject.call(user_id, 'phone', analytics) + subject.call(user_id, 'phone', analytics, threatmetrix_attrs) expect(funnel.registered_at).to be_present end + + context 'with threat metrix for account creation enabled' do + before do + allow(FeatureManagement). + to receive(:account_creation_device_profiling_collecting_enabled?). + and_return(:collect_only) + end + it 'triggers threatmetrix job call' do + expect(AccountCreationThreatMetrixJob).to receive(:perform_later) + subject.call(user_id, 'phone', analytics, threatmetrix_attrs) + end + end end diff --git a/spec/services/funnel/registration/total_registered_count_spec.rb b/spec/services/funnel/registration/total_registered_count_spec.rb index 39fa0db3280..11d8efd5b9f 100644 --- a/spec/services/funnel/registration/total_registered_count_spec.rb +++ b/spec/services/funnel/registration/total_registered_count_spec.rb @@ -1,7 +1,16 @@ require 'rails_helper' RSpec.describe Funnel::Registration::TotalRegisteredCount do + let(:user) { create(:user) } let(:analytics) { FakeAnalytics.new } + let(:threatmetrix_attrs) do + { + user_id: user.id, + request_ip: Faker::Internet.ip_v4_address, + threatmetrix_session_id: 'test-session', + email: user.email, + } + end subject { described_class } it 'returns 0' do @@ -9,13 +18,12 @@ end it 'returns 0 until the user is fully registered' do - user = create(:user) user_id = user.id expect(Funnel::Registration::TotalRegisteredCount.call).to eq(0) expect(Funnel::Registration::TotalRegisteredCount.call).to eq(0) - Funnel::Registration::AddMfa.call(user_id, 'phone', analytics) + Funnel::Registration::AddMfa.call(user_id, 'phone', analytics, threatmetrix_attrs) expect(Funnel::Registration::TotalRegisteredCount.call).to eq(1) end @@ -36,6 +44,6 @@ def register_user user = create(:user) user_id = user.id - Funnel::Registration::AddMfa.call(user_id, 'backup_codes', analytics) + Funnel::Registration::AddMfa.call(user_id, 'backup_codes', analytics, threatmetrix_attrs) end end diff --git a/spec/services/proofing/lexis_nexis/ddp/proofing_spec.rb b/spec/services/proofing/lexis_nexis/ddp/proofing_spec.rb index c57e331ba28..0f9485bf8b1 100644 --- a/spec/services/proofing/lexis_nexis/ddp/proofing_spec.rb +++ b/spec/services/proofing/lexis_nexis/ddp/proofing_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' RSpec.describe Proofing::LexisNexis::Ddp::Proofer do - let(:applicant) do + let(:proofing_applicant) do { first_name: 'Testy', last_name: 'McTesterson', @@ -22,10 +22,25 @@ } end - let(:verification_request) do + let(:authentication_applicant) do + { + threatmetrix_session_id: '123456', + email: 'test@example.com', + request_ip: '127.0.0.1', + } + end + + let(:proofing_verification_request) do + Proofing::LexisNexis::Ddp::VerificationRequest.new( + applicant: proofing_applicant, + config: LexisNexisFixtures.example_ddp_proofing_config, + ) + end + + let(:authentication_verification_request) do Proofing::LexisNexis::Ddp::VerificationRequest.new( - applicant: applicant, - config: LexisNexisFixtures.example_config, + applicant: authentication_applicant, + config: LexisNexisFixtures.example_ddp_authentication_config, ) end @@ -40,10 +55,10 @@ it 'raises a timeout error' do stub_request( :post, - verification_request.url, + proofing_verification_request.url, ).to_timeout - expect { verification_request.send_request }.to raise_error( + expect { proofing_verification_request.send_request }.to raise_error( Proofing::TimeoutError, 'LexisNexis timed out waiting for verification response', ) @@ -55,24 +70,24 @@ request = stub_request( :post, - verification_request.url, + proofing_verification_request.url, ).with( - body: verification_request.body, - headers: verification_request.headers, + body: proofing_verification_request.body, + headers: proofing_verification_request.headers, ).to_return( body: LexisNexisFixtures.ddp_success_response_json, status: 200, ) - verification_request.send_request + proofing_verification_request.send_request expect(request).to have_been_requested.once end end end - subject do - described_class.new(LexisNexisFixtures.example_config.to_h) + subject(:proofer) do + described_class.new(LexisNexisFixtures.example_ddp_proofing_config.to_h) end describe '#proof' do @@ -84,56 +99,102 @@ ) stub_request( :post, - verification_request.url, + proofing_verification_request.url, ).to_return( body: response_body, status: 200, ) end - context 'when the response is a full match' do - let(:response_body) { LexisNexisFixtures.ddp_success_response_json } + context 'when user is going through Idv' do + context 'when the response is a full match' do + let(:response_body) { LexisNexisFixtures.ddp_success_response_json } - it 'is a successful result' do - result = subject.proof(applicant) + it 'is a successful result' do + result = proofer.proof(proofing_applicant) - expect(result.success?).to eq(true) - expect(result.errors).to be_empty - expect(result.review_status).to eq('pass') - expect(result.session_id).to eq('super-cool-test-session-id') - expect(result.account_lex_id).to eq('super-cool-test-lex-id') + expect(result.success?).to eq(true) + expect(result.errors).to be_empty + expect(result.review_status).to eq('pass') + expect(result.session_id).to eq('super-cool-test-session-id') + expect(result.account_lex_id).to eq('super-cool-test-lex-id') + end end - end - context 'when the response raises an exception' do - let(:response_body) { '' } + context 'when the response raises an exception' do + let(:response_body) { '' } - it 'returns an exception result' do - error = RuntimeError.new('hi') + it 'returns an exception result' do + error = RuntimeError.new('hi') - expect(NewRelic::Agent).to receive(:notice_error).with(error) + expect(NewRelic::Agent).to receive(:notice_error).with(error) - stub_request( - :post, - verification_request.url, - ).to_raise(error) + stub_request( + :post, + proofing_verification_request.url, + ).to_raise(error) - result = subject.proof(applicant) + result = proofer.proof(proofing_applicant) - expect(result.success?).to eq(false) - expect(result.errors).to be_empty - expect(result.exception).to eq(error) + expect(result.success?).to eq(false) + expect(result.errors).to be_empty + expect(result.exception).to eq(error) + end + end + + context 'when the review status has an unexpected value' do + let(:response_body) { LexisNexisFixtures.ddp_unexpected_review_status_response_json } + + it 'returns an exception result' do + result = proofer.proof(proofing_applicant) + + expect(result.success?).to eq(false) + expect(result.exception.inspect). + to include(LexisNexisFixtures.ddp_unexpected_review_status) + end end end - context 'when the review status has an unexpected value' do - let(:response_body) { LexisNexisFixtures.ddp_unexpected_review_status_response_json } + context 'when user is going through account creation' do + subject(:proofer) do + described_class.new(LexisNexisFixtures.example_ddp_authentication_config.to_h) + end + + before do + allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_authentication_policy). + and_return('test-authentication-policy') + end + context 'when the response is a full match' do + let(:response_body) { LexisNexisFixtures.ddp_success_response_json } + + it 'is a successful result' do + result = proofer.proof(authentication_applicant) + + expect(result.success?).to eq(true) + expect(result.errors).to be_empty + expect(result.review_status).to eq('pass') + end + end + + context 'when the response raises an exception' do + let(:response_body) { '' } + + it 'returns an exception result' do + error = RuntimeError.new('hi') + + expect(NewRelic::Agent).to receive(:notice_error).with(error) + + stub_request( + :post, + authentication_verification_request.url, + ).to_raise(error) - it 'returns an exception result' do - result = subject.proof(applicant) + result = proofer.proof(authentication_applicant) - expect(result.success?).to eq(false) - expect(result.exception.inspect).to include(LexisNexisFixtures.ddp_unexpected_review_status) + expect(result.success?).to eq(false) + expect(result.errors).to be_empty + expect(result.exception).to eq(error) + end end end end diff --git a/spec/services/proofing/lexis_nexis/ddp/verification_request_spec.rb b/spec/services/proofing/lexis_nexis/ddp/verification_request_spec.rb index 8541278f1a4..199cef5e19a 100644 --- a/spec/services/proofing/lexis_nexis/ddp/verification_request_spec.rb +++ b/spec/services/proofing/lexis_nexis/ddp/verification_request_spec.rb @@ -15,8 +15,8 @@ zipcode: '70802-12345', state_id_number: '12345678', state_id_jurisdiction: 'LA', - threatmetrix_session_id: 'UNIQUE_SESSION_ID', phone: '5551231234', + threatmetrix_session_id: 'UNIQUE_SESSION_ID', email: 'test@example.com', request_ip: '127.0.0.1', uuid_prefix: 'ABCD', @@ -25,29 +25,61 @@ let(:response_body) { LexisNexisFixtures.ddp_success_response_json } subject do - described_class.new(applicant: applicant, config: LexisNexisFixtures.example_ddp_config) + described_class.new( + applicant: applicant, + config: LexisNexisFixtures.example_ddp_proofing_config, + ) end before do allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_policy). and_return('test-policy') + allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_authentication_policy). + and_return('test-authentication-policy') end describe '#body' do - it 'returns a properly formed request body' do - expect(subject.body).to eq(LexisNexisFixtures.ddp_request_json) + context 'Idv verification request' do + it 'returns a properly formed request body' do + response_json = JSON.parse(subject.body) + expected_json = JSON.parse(LexisNexisFixtures.ddp_request_json) + expect(response_json).to eq(expected_json) + end + + context 'without an address line 2' do + let(:applicant) do + hash = super() + hash.delete(:address2) + hash + end + + it 'sets StreetAddress2 to and empty string' do + parsed_body = JSON.parse(subject.body, symbolize_names: true) + expect(parsed_body[:account_address_street2]).to eq('') + end + end end - context 'without an address line 2' do + context 'Authentication verification request' do let(:applicant) do - hash = super() - hash.delete(:address2) - hash + { + threatmetrix_session_id: 'UNIQUE_SESSION_ID', + email: 'test@example.com', + request_ip: '127.0.0.1', + } + end + + subject do + described_class.new( + applicant: applicant, + config: LexisNexisFixtures.example_ddp_authentication_config, + ) end - it 'sets StreetAddress2 to and empty string' do - parsed_body = JSON.parse(subject.body, symbolize_names: true) - expect(parsed_body[:account_address_street2]).to eq('') + it 'returns a properly formed request body' do + response_json = JSON.parse(subject.body) + expected_json = JSON.parse(LexisNexisFixtures.ddp_authentication_request_json) + expect(response_json).to eq(expected_json) end end end diff --git a/spec/services/send_add_email_confirmation_spec.rb b/spec/services/send_add_email_confirmation_spec.rb new file mode 100644 index 00000000000..722b6b97422 --- /dev/null +++ b/spec/services/send_add_email_confirmation_spec.rb @@ -0,0 +1,36 @@ +require 'rails_helper' + +RSpec.describe SendAddEmailConfirmation do + subject(:instance) { described_class.new(user) } + + describe '#call' do + subject(:result) { instance.call(email_address:, request_id:, in_select_email_flow:) } + + let(:user) { create(:user, confirmed_at: nil) } + let(:email_address) { user.email_addresses.take } + let(:request_id) { '1234-abcd' } + let(:in_select_email_flow) { nil } + let(:confirmation_token) { 'confirm-me' } + + before do + allow(Devise).to receive(:friendly_token).once.and_return(confirmation_token) + email_address.update!( + confirmed_at: nil, + confirmation_token: nil, + confirmation_sent_at: nil, + ) + end + it 'sends the user an email with a confirmation link and the request id' do + email_address.update!(confirmed_at: Time.zone.now) + + result + + expect_delivered_email_count(1) + expect_delivered_email( + to: [user.email_addresses.first.email], + subject: t('user_mailer.add_email.subject'), + body: [request_id], + ) + end + end +end diff --git a/spec/support/analytics_helper.rb b/spec/support/analytics_helper.rb index 1c2ae7fe23a..90920f23576 100644 --- a/spec/support/analytics_helper.rb +++ b/spec/support/analytics_helper.rb @@ -2,13 +2,15 @@ module AnalyticsHelper def stub_analytics(user: nil) analytics = FakeAnalytics.new - if user - allow(controller).to receive(:analytics).and_wrap_original do |original| - expect(original.call.user).to eq(user) - analytics - end - else - controller.analytics = analytics + stub = if defined?(controller) + allow(controller) + else + allow_any_instance_of(ApplicationController) + end + + stub.to receive(:analytics).and_wrap_original do |original| + expect(original.call.user).to eq(user) if user + analytics end @analytics = analytics diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb index 60ecb049ee1..9c9ba9f72d5 100644 --- a/spec/support/features/session_helper.rb +++ b/spec/support/features/session_helper.rb @@ -127,8 +127,7 @@ def sign_up confirm_last_user end - def sign_up_and_set_password - user = sign_up + def set_password(user) user.password = VALID_PASSWORD fill_in t('forms.password'), with: user.password fill_in t('components.password_confirmation.confirm_label'), with: user.password @@ -136,6 +135,10 @@ def sign_up_and_set_password user end + def sign_up_and_set_password + set_password(sign_up) + end + def sign_in_user(user = create(:user), email = nil) email ||= user.email_addresses.first.email signin(email, user.password) diff --git a/spec/support/lexis_nexis_fixtures.rb b/spec/support/lexis_nexis_fixtures.rb index 4eb114cf331..38bcd8545bf 100644 --- a/spec/support/lexis_nexis_fixtures.rb +++ b/spec/support/lexis_nexis_fixtures.rb @@ -16,14 +16,29 @@ def example_config ) end - def example_ddp_config + def example_ddp_proofing_config Proofing::LexisNexis::Config.new( api_key: 'test_api_key', base_url: 'https://example.com', org_id: 'test_org_id', + ddp_policy: 'test-policy', ) end + def example_ddp_authentication_config + Proofing::LexisNexis::Config.new( + api_key: 'test_api_key', + base_url: 'https://example.com', + org_id: 'test_org_id', + ddp_policy: 'test-authentication-policy', + ) + end + + def ddp_authentication_request_json + raw = read_fixture_file_at_path('ddp/account_creation_request.json') + JSON.parse(raw).to_json + end + def ddp_request_json raw = read_fixture_file_at_path('ddp/request.json') JSON.parse(raw).to_json @@ -44,11 +59,6 @@ def ddp_failure_response_json JSON.parse(raw).to_json end - def ddp_error_response_json - raw = read_fixture_file_at_path('ddp/error_response.json') - JSON.parse(raw).to_json - end - def ddp_unexpected_review_status 'unexpected_review_status_that_causes_problems' end diff --git a/spec/views/accounts/connected_accounts/selected_email/edit.html.erb_spec.rb b/spec/views/accounts/connected_accounts/selected_email/edit.html.erb_spec.rb index 8fc3f51dd18..4645965a8ed 100644 --- a/spec/views/accounts/connected_accounts/selected_email/edit.html.erb_spec.rb +++ b/spec/views/accounts/connected_accounts/selected_email/edit.html.erb_spec.rb @@ -27,9 +27,6 @@ inputs = page.find_all('[type="radio"]') expect(inputs.count).to eq(2) expect(inputs).to be_logically_grouped(t('titles.select_email')) - fieldset = page.find('fieldset') - expect(fieldset).to have_description( - t('help_text.select_preferred_email', sp: identity.display_name, app_name: APP_NAME), - ) + expect(rendered).to have_content(identity.display_name) end end