Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
4e42470
changelog: Upcoming Features, Account Creation, Threatmetrix result
mdiarra3 May 19, 2025
eddcf24
add scheam
mdiarra3 May 19, 2025
b8e036f
profiling concern and device result
mdiarra3 May 21, 2025
5531b4d
update schema
mdiarra3 May 27, 2025
ee54017
add document type
mdiarra3 May 27, 2025
8a589c7
fix concern
mdiarra3 May 27, 2025
245988a
fix analytics events
mdiarra3 May 27, 2025
545f928
simplify returning device profiling result
mdiarra3 May 28, 2025
942ef86
changelog: Upcoming Features, Account Creation, Block failing Threatm…
mdiarra3 May 29, 2025
ec94491
update migration
mdiarra3 May 29, 2025
b79028e
changelog: Upcoming Features, Threatmetrix, add migration for threat …
mdiarra3 May 29, 2025
e3071f6
remove change thats not a migration
mdiarra3 May 29, 2025
5614991
fix migration
mdiarra3 May 29, 2025
637f57d
update result
mdiarra3 May 29, 2025
78ca77a
update schema
mdiarra3 May 29, 2025
9f3f71e
add ab test bucket for tmx processing
mdiarra3 Jun 2, 2025
49de3bc
delete element if has relation
mdiarra3 Jun 2, 2025
eb3d4e6
Merge remote-tracking branch 'origin/main' into LG-16063-tmx-result-h…
mdiarra3 Jun 2, 2025
bb3f5a0
do perform now
mdiarra3 Jun 2, 2025
41cf80d
update spec
mdiarra3 Jun 3, 2025
3988044
fix profiling result migration to remove index
mdiarra3 Jun 3, 2025
762e028
add translations for failure page
mdiarra3 Jun 3, 2025
af7f406
update set up concern to sign_up completed path
mdiarra3 Jun 3, 2025
e9eb27a
after sign in path update
mdiarra3 Jun 3, 2025
dbdd694
move to have session deleted
mdiarra3 Jun 3, 2025
a544aaa
Merge remote-tracking branch 'origin/main' into LG-16063-tmx-result-h…
mdiarra3 Jun 3, 2025
2a9c0f6
Merge remote-tracking branch 'origin/LG-16063-tmx-result-handling-acc…
mdiarra3 Jun 3, 2025
8e943a4
add resultsg
mdiarra3 Jun 3, 2025
6de7229
add profiling failed analytic event
mdiarra3 Jun 3, 2025
68aeddd
remove trailing whitespace
mdiarra3 Jun 3, 2025
3f8ed2a
fix schema
mdiarra3 Jun 3, 2025
fff0854
update tmx job
mdiarra3 Jun 3, 2025
8e57047
update
mdiarra3 Jun 3, 2025
d8e0f03
update features
mdiarra3 Jun 3, 2025
99879be
device profiling result
mdiarra3 Jun 3, 2025
72f526c
add mfa
mdiarra3 Jun 3, 2025
6b77e7c
fix threat metrix spec
mdiarra3 Jun 3, 2025
01ed27d
update job to be performed later
mdiarra3 Jun 3, 2025
6a337ae
Merge remote-tracking branch 'origin/main' into LG-16063-tmx-result-h…
mdiarra3 Jun 3, 2025
cdfcfc5
test commit
mdiarra3 Jun 4, 2025
e56db08
updated device profiling failed
mdiarra3 Jun 4, 2025
0df8777
update threat metrix specs and completions controller
mdiarra3 Jun 4, 2025
70af7ae
change path name in tmx spec
mdiarra3 Jun 5, 2025
9be67ec
update to be complete
mdiarra3 Jun 9, 2025
2f1a3ff
fix device profiling result
mdiarra3 Jun 10, 2025
813a16b
Merge remote-tracking branch 'origin/main' into LG-16063-tmx-result-h…
mdiarra3 Jun 10, 2025
3f2192a
account device profile rejected after authentication
mdiarra3 Jun 11, 2025
367a522
add check to session controller
mdiarra3 Jun 11, 2025
3c93f35
remove unused columns
mdiarra3 Jun 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It is not clear what success vs review_status is in DeviceProfilingResult. Also, what is the difference between failed and rejected?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

success Is simple of what we think is a success vs failure. But I think review_status is a little bit more involved/ in depth than the success attribute.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If success can be derived from review_status, we should only persist review_status and leave success as a method on the model.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

removed success as well

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
Expand Down
5 changes: 5 additions & 0 deletions app/controllers/concerns/mfa_setup_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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]
Expand Down
8 changes: 8 additions & 0 deletions app/controllers/device_profiling_failed_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

class DeviceProfilingFailedController < ApplicationController
def show
analytics.device_profiling_failed_visited
sign_out
end
end
2 changes: 1 addition & 1 deletion app/controllers/mfa_confirmation_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 14 additions & 3 deletions app/controllers/sign_up/completions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -37,12 +38,17 @@ def update

private

def verify_profiling_passed
return unless user_account_creation_device_profile_failed?
Comment thread
mdiarra3 marked this conversation as resolved.
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
Expand All @@ -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
Expand Down
10 changes: 10 additions & 0 deletions app/controllers/users/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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)
Expand Down
17 changes: 17 additions & 0 deletions app/jobs/account_creation_threat_metrix_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 17 additions & 0 deletions app/models/device_profiling_result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions app/services/analytics_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>] unknown_alerts Names of alerts not recognized by our code
# @param [Hash] response_info Response payload
Expand Down
3 changes: 2 additions & 1 deletion app/services/funnel/registration/add_mfa.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions app/views/device_profiling_failed/show.html.erb
Original file line number Diff line number Diff line change
@@ -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')) %>
<p>
<%= t('profiling_failed.details') %>
</p>

<%= link_to(
root_url,
class: 'usa-button usa-button--big usa-button--wide',
) { t('links.exit_login', app_name: APP_NAME) } %>

2 changes: 2 additions & 0 deletions config/application.yml.default
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions config/initializers/ab_tests.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
mdiarra3 marked this conversation as resolved.
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
Expand Down
2 changes: 2 additions & 0 deletions config/locales/es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions config/locales/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions config/locales/zh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: 已删除账户报告
Expand Down
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
Original file line number Diff line number Diff line change
@@ -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
4 changes: 1 addition & 3 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions lib/identity_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
47 changes: 47 additions & 0 deletions spec/controllers/users/sessions_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading