diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a5545255da1..de107c2bb95 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -270,7 +270,9 @@ def signed_in_url end def after_mfa_setup_path - if needs_completion_screen_reason + if user_account_creation_device_profile_failed? + return device_profiling_failed_url + elsif needs_completion_screen_reason sign_up_completed_url elsif user_needs_to_reactivate_account? reactivate_account_url @@ -521,6 +523,21 @@ def user_is_banned? BannedUserResolver.new(current_user).banned_for_sp?(issuer: current_sp&.issuer) end + def user_account_creation_device_profile_failed? + return false unless IdentityConfig.store.account_creation_device_profiling == :enabled + profiling_result = find_device_profiling_result( + DeviceProfilingResult::PROFILING_TYPES[:account_creation], + ) + profiling_result&.rejected? + end + + def find_device_profiling_result(type) + DeviceProfilingResult.for_user( + user_id: current_user.id, + type: type, + ).last + end + def user_duplicate_profiles_detected? return false unless sp_eligible_for_one_account? profile = current_user&.active_profile diff --git a/app/controllers/concerns/mfa_setup_concern.rb b/app/controllers/concerns/mfa_setup_concern.rb index 138a22a5364..852f33c5d4d 100644 --- a/app/controllers/concerns/mfa_setup_concern.rb +++ b/app/controllers/concerns/mfa_setup_concern.rb @@ -98,6 +98,7 @@ def threatmetrix_attrs email: current_user.last_sign_in_email_address.email, uuid_prefix: current_sp&.app_id, user_uuid: current_user.uuid, + in_ab_test_bucket: in_tmx_ab_test_bucket?, } end @@ -114,6 +115,10 @@ def track_user_registration_mfa_setup_complete_event ) end + def in_tmx_ab_test_bucket? + ab_test_bucket(:ACCOUNT_CREATION_TMX_PROCESSED) == (:account_creation_tmx_processed) + end + def determine_next_mfa return unless user_session[:mfa_selections] current_setup_step = user_session[:next_mfa_selection_choice] diff --git a/app/controllers/device_profiling_failed_controller.rb b/app/controllers/device_profiling_failed_controller.rb new file mode 100644 index 00000000000..f5c408ff6d1 --- /dev/null +++ b/app/controllers/device_profiling_failed_controller.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class DeviceProfilingFailedController < ApplicationController + def show + analytics.device_profiling_failed_visited + sign_out + end +end diff --git a/app/controllers/mfa_confirmation_controller.rb b/app/controllers/mfa_confirmation_controller.rb index a87a440b6cb..a6f99a2ce90 100644 --- a/app/controllers/mfa_confirmation_controller.rb +++ b/app/controllers/mfa_confirmation_controller.rb @@ -21,7 +21,7 @@ def skip pii_like_keypaths: [[:mfa_method_counts, :phone]], success: true, ) - redirect_to sign_up_completed_path + redirect_to after_mfa_setup_path end private diff --git a/app/controllers/sign_up/completions_controller.rb b/app/controllers/sign_up/completions_controller.rb index 94eacd36a13..dcd5b8641f2 100644 --- a/app/controllers/sign_up/completions_controller.rb +++ b/app/controllers/sign_up/completions_controller.rb @@ -8,6 +8,7 @@ class CompletionsController < ApplicationController before_action :confirm_identity_verified, if: :identity_proofing_required? before_action :apply_secure_headers_override, only: [:show, :update] before_action :verify_needs_completions_screen + before_action :verify_profiling_passed def show analytics.user_registration_agency_handoff_page_visit( @@ -37,12 +38,17 @@ def update private + def verify_profiling_passed + return unless user_account_creation_device_profile_failed? + redirect_to device_profiling_failed_url + end + def confirm_identity_verified redirect_to idv_url if current_user.identity_not_verified? end def verify_needs_completions_screen - return_to_account unless needs_completion_screen_reason + return_to_next_path unless needs_completion_screen_reason end def completions_presenter @@ -65,9 +71,14 @@ def ial2_requested? resolved_authn_context_result.identity_proofing_or_ialmax? && current_user.identity_verified? end - def return_to_account + def return_to_next_path + @return_path = if user_session[:in_account_creation_flow] + after_mfa_setup_path + else + after_sign_in_path_for(current_user) + end track_completion_event('account-page') - redirect_to account_url + redirect_to @return_path end def decider diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index 6daaa64ba3f..8bb862ddcbf 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -273,6 +273,8 @@ def next_url_after_valid_authentication analytics.banned_user_redirect sign_out banned_user_url + elsif user_account_creation_device_profile_failed? + device_profiling_failed_url elsif pending_account_reset_request.present? account_reset_pending_url elsif current_user.accepted_rules_of_use_still_valid? @@ -337,6 +339,14 @@ def randomize_check_password? SecureRandom.random_number(IdentityConfig.store.compromised_password_randomizer_value) >= IdentityConfig.store.compromised_password_randomizer_threshold end + + def user_account_creation_device_profile_failed? + return false unless IdentityConfig.store.account_creation_device_profiling == :enabled + profiling_result = find_device_profiling_result( + DeviceProfilingResult::PROFILING_TYPES[:account_creation], + ) + profiling_result&.rejected? + end end def unsafe_redirect_error(_exception) diff --git a/app/jobs/account_creation_threat_metrix_job.rb b/app/jobs/account_creation_threat_metrix_job.rb index d93778108e6..a26aa34cc2c 100644 --- a/app/jobs/account_creation_threat_metrix_job.rb +++ b/app/jobs/account_creation_threat_metrix_job.rb @@ -19,10 +19,27 @@ def perform( ) ensure user = User.find_by(id: user_id) + store_device_profiling_result(user_id, device_profiling_result) 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 + + private + + def store_device_profiling_result(user_id, result) + return unless user_id.present? + return unless IdentityConfig.store.account_creation_device_profiling == :enabled + device_profiling_result = DeviceProfilingResult.find_or_create_by( + user_id:, + profiling_type: DeviceProfilingResult::PROFILING_TYPES[:account_creation], + ) + device_profiling_result.update( + client: result.client, + review_status: result.review_status, + transaction_id: result.transaction_id, + ) + end end diff --git a/app/models/device_profiling_result.rb b/app/models/device_profiling_result.rb index c7d40ee8412..fd19570029a 100644 --- a/app/models/device_profiling_result.rb +++ b/app/models/device_profiling_result.rb @@ -6,4 +6,21 @@ class DeviceProfilingResult < ApplicationRecord PROFILING_TYPES = { account_creation: 'ACCOUNT_CREATION', }.freeze + + def self.find_or_create_by(user_id:, profiling_type:) + obj = find_by(user_id:, profiling_type:) + return obj if obj + create( + user_id:, + profiling_type:, + ) + end + + def self.for_user(user_id:, type:) + where(user_id:, profiling_type: type) + end + + def rejected? + review_status == 'reject' + end end diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index e689a2bb4d4..98f32229923 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -518,6 +518,12 @@ def create_new_device_alert_job_emails_sent(count:, **extra) track_event(:create_new_device_alert_job_emails_sent, count:, **extra) end + # User directed to this page after TMX returns a failure + + def device_profiling_failed_visited + track_event(:device_profiling_failed_visited) + end + # @param [String] message the warning # @param [Array] unknown_alerts Names of alerts not recognized by our code # @param [Hash] response_info Response payload diff --git a/app/services/funnel/registration/add_mfa.rb b/app/services/funnel/registration/add_mfa.rb index 99ef121b994..6dc98d0b493 100644 --- a/app/services/funnel/registration/add_mfa.rb +++ b/app/services/funnel/registration/add_mfa.rb @@ -17,7 +17,8 @@ def self.call(user_id, mfa_method, analytics, threatmetrix_attrs) def self.process_threatmetrix_for_user(threatmetrix_attrs) return unless FeatureManagement.account_creation_device_profiling_collecting_enabled? - AccountCreationThreatMetrixJob.perform_later(**threatmetrix_attrs) + return unless threatmetrix_attrs.delete(:in_ab_test_bucket) + AccountCreationThreatMetrixJob.perform_now(**threatmetrix_attrs) end end end diff --git a/app/views/device_profiling_failed/show.html.erb b/app/views/device_profiling_failed/show.html.erb new file mode 100644 index 00000000000..49dfea3207e --- /dev/null +++ b/app/views/device_profiling_failed/show.html.erb @@ -0,0 +1,13 @@ +<% self.title = t('profiling_failed.title') %> + +<%= render AlertIconComponent.new(icon_name: :error, class: 'display-block margin-bottom-4') %> +<%= render PageHeadingComponent.new.with_content(t('profiling_failed.title')) %> +

+ <%= t('profiling_failed.details') %> +

+ +<%= link_to( + root_url, + class: 'usa-button usa-button--big usa-button--wide', + ) { t('links.exit_login', app_name: APP_NAME) } %> + diff --git a/config/application.yml.default b/config/application.yml.default index be67bcdcba4..2681044c5d1 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -23,6 +23,7 @@ aamva_supported_jurisdictions: '["AL","AR","AZ","CO","CT","DC","DE","FL","GA","H aamva_verification_request_timeout: 5.0 aamva_verification_url: https://example.org:12345/verification/url account_creation_device_profiling: disabled +account_creation_tmx_processed_percent: 0 account_reset_fraud_user_wait_period_days: account_reset_token_valid_for_days: 1 account_reset_wait_period_days: 1 @@ -498,6 +499,7 @@ development: aamva_private_key: 123abc aamva_public_key: 123abc account_creation_device_profiling: collect_only + account_creation_tmx_processed_percent: 100 attribute_encryption_key: 2086dfbd15f5b0c584f3664422a1d3409a0d2aa6084f65b6ba57d64d4257431c124158670c7655e45cabe64194f7f7b6c7970153c285bdb8287ec0c4f7553e25 attribute_encryption_key_queue: '[{ "key": "11111111111111111111111111111111" }, { "key": "22222222222222222222222222222222" }]' check_user_password_compromised_enabled: true diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb index 59edbcb6049..dc88ef9bd62 100644 --- a/config/initializers/ab_tests.rb +++ b/config/initializers/ab_tests.rb @@ -125,6 +125,18 @@ def self.all }, ).freeze + ACCOUNT_CREATION_TMX_PROCESSED = AbTest.new( + experiment_name: 'Account Creation Threat Metrix Processed', + should_log: [ + :account_creation_tmx_result, + ].to_set, + buckets: { + account_creation_tmx_processed: IdentityConfig.store.account_creation_tmx_processed_percent, + }, + ) do |user:, user_session:, **| + user&.uuid + end.freeze + SOCURE_IDV_SHADOW_MODE_FOR_NON_DOCV_USERS = AbTest.new( experiment_name: 'Socure shadow mode', should_log: ['IdV: doc auth verify proofing results'].to_set, diff --git a/config/locales/en.yml b/config/locales/en.yml index 1c83abe0232..8bbd5c33f4f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1583,6 +1583,8 @@ openid_connect.user_info.errors.no_authorization: No Authorization header provid openid_connect.user_info.errors.not_found: Could not find authorization for the contents of the provided access_token or it may have expired pages.page_took_too_long.body: You might want to wait a few minutes and try again. (503) pages.page_took_too_long.header: The server took too long to process your request. +profiling_failed.details: We are unable to authenticate or sign you in. Reach out to your agency to access services. +profiling_failed.title: We couldn’t sign you into your account report_mailer.deleted_accounts_report.issuers: Issuers report_mailer.deleted_accounts_report.name: Name report_mailer.deleted_accounts_report.subject: Deleted accounts report diff --git a/config/locales/es.yml b/config/locales/es.yml index 4001001bd30..d6d36fc1986 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1595,6 +1595,8 @@ openid_connect.user_info.errors.no_authorization: No se proporcionó ningún enc openid_connect.user_info.errors.not_found: No se pudo encontrar la autorización para el contenido del access_token proporcionado o puede ser que este haya vencido pages.page_took_too_long.body: Es conveniente que espere unos minutos y vuelva a intentarlo. (503) pages.page_took_too_long.header: El servidor tardó demasiado en procesar su solicitud. +profiling_failed.details: No podemos autenticarle ni iniciar sesión. Contacte con su agencia para acceder a los servicios. +profiling_failed.title: No pudimos iniciar sesión en su cuenta report_mailer.deleted_accounts_report.issuers: Emisores report_mailer.deleted_accounts_report.name: Nombre report_mailer.deleted_accounts_report.subject: Informe de cuentas eliminadas diff --git a/config/locales/fr.yml b/config/locales/fr.yml index a09aeb747bf..051afd3f957 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1583,6 +1583,8 @@ openid_connect.user_info.errors.no_authorization: Aucune en-tête d’autorisati openid_connect.user_info.errors.not_found: L’autorisation pour le contenu du access_token fourni introuvable ou il peut être expiré pages.page_took_too_long.body: Nous vous recommandons de patienter quelques minutes, puis de réessayer. (503) pages.page_took_too_long.header: Le serveur a mis trop de temps à traiter votre demande. +profiling_failed.details: Nous ne sommes pas en mesure de vous authentifier pour vous connecter. Veuillez contacter votre organisme pour accéder à ses services. +profiling_failed.title: Nous n’avons pas pu vous connecter à votre compte report_mailer.deleted_accounts_report.issuers: Émetteurs report_mailer.deleted_accounts_report.name: Nom report_mailer.deleted_accounts_report.subject: Rapport sur les comptes supprimés diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 19f53e3e7f5..eefa629f1d9 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -1596,6 +1596,8 @@ openid_connect.user_info.errors.no_authorization: 未提供授权标头 openid_connect.user_info.errors.not_found: 就提供的 access_token 内容找不到授权或者授权已过期。 pages.page_took_too_long.body: 你也许想等几分钟后再试。(503) pages.page_took_too_long.header: 服务器处理你请求的时间过长。 +profiling_failed.details: 我们无法证实你的身份并让你登录。请联系你的机构以获取服务。 +profiling_failed.title: 我们无法把你登入你的帐户。 report_mailer.deleted_accounts_report.issuers: 发放方 report_mailer.deleted_accounts_report.name: 姓名 report_mailer.deleted_accounts_report.subject: 已删除账户报告 diff --git a/config/routes.rb b/config/routes.rb index 7c072d5558d..d0ec92e9502 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -212,6 +212,8 @@ post '/duplicate_profiles_detected/recognize_all_profiles' => 'duplicate_profiles_detected#recognize_all_profiles' post '/duplicate_profiles_detected/do_not_recognize_profiles' => 'duplicate_profiles_detected#do_not_recognize_profiles' + get '/device_profiling_failed', to: 'device_profiling_failed#show' + get '/auth_method_confirmation' => 'mfa_confirmation#show' post '/auth_method_confirmation/skip' => 'mfa_confirmation#skip' diff --git a/db/primary_migrate/20250611195441_remove_unused_attributes_on_device_profile_result.rb b/db/primary_migrate/20250611195441_remove_unused_attributes_on_device_profile_result.rb new file mode 100644 index 00000000000..8dd71fa817b --- /dev/null +++ b/db/primary_migrate/20250611195441_remove_unused_attributes_on_device_profile_result.rb @@ -0,0 +1,8 @@ +class RemoveUnusedAttributesOnDeviceProfileResult < ActiveRecord::Migration[8.0] + def change + safety_assured do + remove_column :device_profiling_results, :reason + remove_column :device_profiling_results, :success + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 826b7ebb7b6..1745735842d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_05_19_152453) do +ActiveRecord::Schema[8.0].define(version: 2025_06_11_195441) do # These are extensions that must be enabled in order to support this database enable_extension "citext" enable_extension "pg_catalog.plpgsql" @@ -89,11 +89,9 @@ create_table "device_profiling_results", force: :cascade do |t| t.bigint "user_id", null: false, comment: "sensitive=false" - t.boolean "success", default: false, null: false, comment: "sensitive=false" t.string "client", comment: "sensitive=false" t.string "review_status", comment: "sensitive=false" t.string "transaction_id", comment: "sensitive=false" - t.string "reason", comment: "sensitive=false" t.datetime "processed_at", comment: "sensitive=false" t.string "profiling_type", comment: "sensitive=false" t.datetime "created_at", null: false, comment: "sensitive=false" diff --git a/lib/identity_config.rb b/lib/identity_config.rb index dcf877e22e6..dff988f66d9 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -42,6 +42,7 @@ def self.store type: :symbol, enum: [:disabled, :collect_only, :enabled], ) + config.add(:account_creation_tmx_processed_percent, type: :integer) config.add(:account_reset_token_valid_for_days, type: :integer) config.add(:account_reset_wait_period_days, type: :integer) config.add(:account_reset_fraud_user_wait_period_days, type: :integer, allow_nil: true) diff --git a/spec/controllers/users/sessions_controller_spec.rb b/spec/controllers/users/sessions_controller_spec.rb index b3e69076603..d07b317e9b8 100644 --- a/spec/controllers/users/sessions_controller_spec.rb +++ b/spec/controllers/users/sessions_controller_spec.rb @@ -447,6 +447,53 @@ end end + context 'with account creation device profiling enabled' do + let(:user) { create(:user, :fully_registered) } + let(:valid_params) { { user: { email: user.email, password: user.password } } } + + before do + allow(IdentityConfig.store) + .to receive(:account_creation_device_profiling).and_return(:enabled) + allow(controller).to receive(:find_device_profiling_result).and_return(profiling_result) + stub_analytics(user: user) + end + + context 'when device profiling fails' do + let(:profiling_result) { create(:device_profiling_result, :rejected, user: user) } + + it 'redirects to device profiling failed url after successful authentication' do + expect(@attempts_api_tracker).to receive(:login_email_and_password_auth).with( + success: true, + ) + + post :create, params: valid_params + + expect(response).to redirect_to(device_profiling_failed_url) + end + + it 'signs in the user but redirects to device profiling failed page' do + post :create, params: valid_params + + expect(controller.current_user).to eq(user) + expect(response).to redirect_to(device_profiling_failed_url) + end + end + + context 'when device profiling passes' do + let(:profiling_result) { nil } + + it 'continues normal authentication flow to 2FA' do + expect(@attempts_api_tracker).to receive(:login_email_and_password_auth).with( + success: true, + ) + + post :create, params: valid_params + + expect(response).to redirect_to(user_two_factor_authentication_url) + end + end + end + it 'tracks count of multiple unsuccessful authentication attempts' do user = create( :user, diff --git a/spec/factories/device_profiling_results.rb b/spec/factories/device_profiling_results.rb new file mode 100644 index 00000000000..cee0a1c4708 --- /dev/null +++ b/spec/factories/device_profiling_results.rb @@ -0,0 +1,17 @@ +FactoryBot.define do + factory :device_profiling_result do + association :user + profiling_type { DeviceProfilingResult::PROFILING_TYPES[:account_creation] } + review_status { 'pass' } + created_at { Time.zone.now } + updated_at { Time.zone.now } + + trait :rejected do + review_status { 'reject' } + end + + trait :pending do + review_status { 'pending' } + end + end +end diff --git a/spec/features/account_creation/threat_metrix_spec.rb b/spec/features/account_creation/threat_metrix_spec.rb index 38a53336f4d..a66c8a2dd8e 100644 --- a/spec/features/account_creation/threat_metrix_spec.rb +++ b/spec/features/account_creation/threat_metrix_spec.rb @@ -2,38 +2,114 @@ 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') + allow(IdentityConfig.store) + .to receive(:lexisnexis_threatmetrix_mock_enabled) + .and_return(true) + allow_any_instance_of(ApplicationController) + .to receive(:ab_test_bucket) + allow_any_instance_of(ApplicationController) + .to receive(:ab_test_bucket).with(:ACCOUNT_CREATION_TMX_PROCESSED) + .and_return(:account_creation_tmx_processed) 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) - 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 'Reject', from: :mock_profiling_result - 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', - ) + context 'when tmx is in collect only' do + before do + allow(IdentityConfig.store) + .to receive(:account_creation_device_profiling).and_return(:collect_only) + 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) + click_button t('forms.buttons.submit.default') + user = confirm_last_user + set_password(user) + + fake_analytics = FakeAnalytics.new + expect(page).to have_current_path(authentication_methods_setup_path) + expect_any_instance_of(AccountCreationThreatMetrixJob).to receive(:analytics).with(user) + .and_return(fake_analytics) + select 'Reject', from: :mock_profiling_result + check t('two_factor_authentication.two_factor_choice_options.phone') + check t('two_factor_authentication.two_factor_choice_options.backup_code') + click_continue + + expect(page).to have_current_path(phone_setup_path) + set_up_mfa_with_valid_phone + + expect(page).to have_current_path(backup_code_setup_path) + check t('forms.backup_code.saved') + 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 + + context 'when tmx is enabled' do + before do + allow(IdentityConfig.store) + .to receive(:account_creation_device_profiling).and_return(:enabled) + end + + context 'when tmx returns a rejected response' do + it 'logs repsonse and redirects to profiling failed page' 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) + click_button t('forms.buttons.submit.default') + user = confirm_last_user + set_password(user) + + fake_analytics = FakeAnalytics.new + + expect(page).to have_current_path(authentication_methods_setup_path) + expect_any_instance_of(AccountCreationThreatMetrixJob).to receive(:analytics).with(user) + .and_return(fake_analytics) + select 'Reject', from: :mock_profiling_result + check t('two_factor_authentication.two_factor_choice_options.phone') + check t('two_factor_authentication.two_factor_choice_options.backup_code') + click_continue + + expect(page).to have_current_path(phone_setup_path) + set_up_mfa_with_valid_phone + + expect(page).to have_current_path(backup_code_setup_path) + check t('forms.backup_code.saved') + 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', + ) + + expect(page).to have_current_path(device_profiling_failed_path) + end + end end end diff --git a/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb b/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb index f109775f2ff..227f8414b72 100644 --- a/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb +++ b/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb @@ -34,7 +34,6 @@ click_continue - expect(fake_analytics).to have_logged_event('User registration: complete') expect(page).to have_title(t('titles.account')) end end diff --git a/spec/jobs/account_creation_threat_metrix_job_spec.rb b/spec/jobs/account_creation_threat_metrix_job_spec.rb index ac28615b274..9204e484969 100644 --- a/spec/jobs/account_creation_threat_metrix_job_spec.rb +++ b/spec/jobs/account_creation_threat_metrix_job_spec.rb @@ -36,6 +36,8 @@ context 'Threat Metrix Account Creation analysis passes' do let(:threatmetrix_response) { LexisNexisFixtures.ddp_success_response_json } + let(:authentication_device_profiling) { :enabled } + it 'logs a successful result' do threatmetrix_stub @@ -49,6 +51,18 @@ ), ) end + + it 'creates a DeviceProfilingResult' do + threatmetrix_stub + + perform + result = DeviceProfilingResult.find_by( + user_id: user.id, + profiling_type: DeviceProfilingResult::PROFILING_TYPES[:account_creation], + ) + expect(result).to be_truthy + expect(result.user_id).to eq(user.id) + end end context 'with an error response result' do diff --git a/spec/models/device_profiling_result_spec.rb b/spec/models/device_profiling_result_spec.rb new file mode 100644 index 00000000000..a5aed55e69f --- /dev/null +++ b/spec/models/device_profiling_result_spec.rb @@ -0,0 +1,37 @@ +require 'rails_helper' + +RSpec.describe DeviceProfilingResult do + describe 'profiling types' do + it 'includes account_creation type' do + expect(DeviceProfilingResult::PROFILING_TYPES).to include(:account_creation) + end + end + + describe '#rejected?' do + let(:user) { create(:user) } + + context 'when result is rejected' do + let(:result) { create(:device_profiling_result, :rejected, user: user) } + + it 'returns true' do + expect(result.rejected?).to be true + end + end + + context 'when result is approved' do + let(:result) { create(:device_profiling_result, user: user) } + + it 'returns false' do + expect(result.rejected?).to be false + end + end + + context 'when result is pending' do + let(:result) { create(:device_profiling_result, :pending, user: user) } + + it 'returns false' do + expect(result.rejected?).to be false + end + end + end +end diff --git a/spec/services/funnel/registration/add_mfa_spec.rb b/spec/services/funnel/registration/add_mfa_spec.rb index c19760414f8..593638f0fba 100644 --- a/spec/services/funnel/registration/add_mfa_spec.rb +++ b/spec/services/funnel/registration/add_mfa_spec.rb @@ -14,6 +14,7 @@ request_ip: Faker::Internet.ip_v4_address, threatmetrix_session_id: SecureRandom.uuid, email: user.email, + in_ab_test_bucket: true, } end @@ -32,9 +33,12 @@ allow(FeatureManagement) .to receive(:account_creation_device_profiling_collecting_enabled?) .and_return(:collect_only) + + allow(IdentityConfig).to receive(:account_creation_tmx_processed_percent) + .and_return(100) end it 'triggers threatmetrix job call' do - expect(AccountCreationThreatMetrixJob).to receive(:perform_later) + expect(AccountCreationThreatMetrixJob).to receive(:perform_now) subject.call(user_id, 'phone', analytics, threatmetrix_attrs) end end