From 847d49a2558220b69f2a41d8bdda84d8b627cea6 Mon Sep 17 00:00:00 2001 From: Davida Marion Date: Fri, 27 Jun 2025 14:58:59 -0400 Subject: [PATCH 1/2] changelog: Upcoming Features, Attempts API, Add device_fingerprint to idv-device-risk-assessment event --- .../concerns/idv/verify_info_concern.rb | 2 ++ app/services/attempts_api/tracker_events.rb | 4 ++- app/services/proofing/ddp_result.rb | 4 +++ .../proofing/resolution/result_adjudicator.rb | 1 + .../IdvDeviceRiskAssessment.yml | 6 +++- .../idv/verify_info_controller_spec.rb | 17 +++++++++++ spec/services/proofing/ddp_result_spec.rb | 28 +++++++++++++++++-- 7 files changed, 58 insertions(+), 4 deletions(-) diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb index 82e7c2ce7c5..1656a59636a 100644 --- a/app/controllers/concerns/idv/verify_info_concern.rb +++ b/app/controllers/concerns/idv/verify_info_concern.rb @@ -194,6 +194,7 @@ def async_state_done(current_async_state) last_name_spaced: pii[:last_name].split(' ').many?, previous_ssn_edit_distance: previous_ssn_edit_distance, pii_like_keypaths: [ + [:device_fingerprint], [:errors, :ssn], [:errors, :state_id_jurisdiction], [:proofing_results, :context, :stages, :resolution, :errors, :ssn], @@ -345,6 +346,7 @@ def create_fraud_review_request_if_needed(result) success = (threatmetrix_result[:review_status] == 'pass') attempts_api_tracker.idv_device_risk_assessment( + device_fingerprint: result.dig(:device_fingerprint), success:, failure_reason: device_risk_failure_reason(success, threatmetrix_result), ) diff --git a/app/services/attempts_api/tracker_events.rb b/app/services/attempts_api/tracker_events.rb index e3b0ea64e9b..622f6556e63 100644 --- a/app/services/attempts_api/tracker_events.rb +++ b/app/services/attempts_api/tracker_events.rb @@ -527,12 +527,14 @@ def mfa_submission_code_rate_limited(mfa_device_type:) end # @param [Boolean] success True means TMX's device risk check has a 'pass' review status + # @param [String] device_fingerprint 32-character string based on device attributes # @param [Hash>] failure_reason # Tracks the result of the Device fraud check during Identity Verification - def idv_device_risk_assessment(success:, failure_reason: nil) + def idv_device_risk_assessment(success:, device_fingerprint: nil, failure_reason: nil) track_event( 'idv-device-risk-assessment', success:, + device_fingerprint:, failure_reason:, ) end diff --git a/app/services/proofing/ddp_result.rb b/app/services/proofing/ddp_result.rb index 7c5dddc3ebd..14e7079a965 100644 --- a/app/services/proofing/ddp_result.rb +++ b/app/services/proofing/ddp_result.rb @@ -82,6 +82,10 @@ def to_h } end + def device_fingerprint + response_body&.dig(:fuzzy_device_id) + end + private def redacted_response_body diff --git a/app/services/proofing/resolution/result_adjudicator.rb b/app/services/proofing/resolution/result_adjudicator.rb index c19fa4a773f..2933e650e93 100644 --- a/app/services/proofing/resolution/result_adjudicator.rb +++ b/app/services/proofing/resolution/result_adjudicator.rb @@ -43,6 +43,7 @@ def adjudicated_result errors: errors, extra: { exception: exception, + device_fingerprint: device_profiling_result.device_fingerprint, timed_out: timed_out?, threatmetrix_review_status: device_profiling_result.review_status, phone_finder_precheck_passed: phone_finder_result.success?, diff --git a/docs/attempts-api/schemas/events/identity-proofing/IdvDeviceRiskAssessment.yml b/docs/attempts-api/schemas/events/identity-proofing/IdvDeviceRiskAssessment.yml index 0f47b501cd7..e0d28cbc38e 100644 --- a/docs/attempts-api/schemas/events/identity-proofing/IdvDeviceRiskAssessment.yml +++ b/docs/attempts-api/schemas/events/identity-proofing/IdvDeviceRiskAssessment.yml @@ -4,6 +4,10 @@ allOf: - $ref: "../shared/EventProperties.yml" - type: object properties: + device_fingerprint: + type: string + description: | + A 32-character string based exclusively on device attributes to improve detection of returning visitors, especially those trying to elude identification. failure_reason: type: object description: | @@ -38,4 +42,4 @@ allOf: success: type: boolean description: | - Indicates whether the TMX response status pass. + Indicates whether the user has passed the device risk assessment. diff --git a/spec/controllers/idv/verify_info_controller_spec.rb b/spec/controllers/idv/verify_info_controller_spec.rb index 3247414352a..4e1d383f497 100644 --- a/spec/controllers/idv/verify_info_controller_spec.rb +++ b/spec/controllers/idv/verify_info_controller_spec.rb @@ -187,6 +187,7 @@ let(:review_status) { 'pass' } let(:applicant_pii) { Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN } let(:success) { true } + let(:device_fingerprint) { SecureRandom.hex(32) } let(:idv_result) do { @@ -206,6 +207,7 @@ }, }, }, + device_fingerprint:, errors: {}, exception: nil, success: true, @@ -283,6 +285,10 @@ ), ), ) + expect(@analytics).to have_logged_event( + 'IdV: doc auth verify proofing results', + hash_excluding(device_fingerprint:), + ) expect(@analytics).to have_logged_event( :idv_threatmetrix_response_body, response_body: hash_including( @@ -294,6 +300,7 @@ it 'tracks the attempts events' do expect(@attempts_api_tracker).to receive(:idv_device_risk_assessment).with( success: true, + device_fingerprint:, failure_reason: nil, ) expect(@attempts_api_tracker).to receive(:idv_verification_submitted).with( @@ -330,6 +337,7 @@ it 'tracks a failed tmx fraud check' do expect(@attempts_api_tracker).to receive(:idv_device_risk_assessment).with( success: false, + device_fingerprint:, failure_reason: { fraud_risk_summary_reason_code: ['Identity_Negative_History'] }, ) @@ -380,6 +388,7 @@ }, }, }, + device_fingerprint:, success: false, } end @@ -420,6 +429,11 @@ ), ), ) + + expect(@analytics).to have_logged_event( + 'IdV: doc auth verify proofing results', + hash_excluding(device_fingerprint:), + ) end it 'tracks the event for the attempts api' do @@ -451,6 +465,7 @@ stub_attempts_tracker expect(@attempts_api_tracker).to receive(:idv_device_risk_assessment).with( success: false, + device_fingerprint:, failure_reason: { fraud_risk_summary_reason_code: ['Fraud risk assessment has failed for unknown reasons'], @@ -473,6 +488,7 @@ it 'tracks a failed tmx fraud check' do expect(@attempts_api_tracker).to receive(:idv_device_risk_assessment).with( success:, + device_fingerprint:, failure_reason: { fraud_risk_summary_reason_code: ['Identity_Negative_History'], }, @@ -516,6 +532,7 @@ stub_attempts_tracker expect(@attempts_api_tracker).to receive(:idv_device_risk_assessment).with( success: false, + device_fingerprint:, failure_reason: { fraud_risk_summary_reason_code: ['Identity_Negative_History'], }, diff --git a/spec/services/proofing/ddp_result_spec.rb b/spec/services/proofing/ddp_result_spec.rb index f8fdf6a230a..e367dbf0907 100644 --- a/spec/services/proofing/ddp_result_spec.rb +++ b/spec/services/proofing/ddp_result_spec.rb @@ -111,8 +111,7 @@ context 'when provided' do it 'is present' do transaction_id = 'foo' - result = Proofing::DdpResult.new - result.transaction_id = transaction_id + result = Proofing::DdpResult.new(transaction_id:) expect(result.transaction_id).to eq(transaction_id) end end @@ -144,4 +143,29 @@ end end end + + describe '#device_fingerprint' do + let(:response_body) { { fuzzy_device_id: '12345' } } + subject { described_class.new(response_body:) } + + context 'when response_body is present' do + it 'returns the device fingerprint' do + expect(subject.device_fingerprint).to eq('12345') + end + end + + context 'when response_body is nil' do + let(:response_body) { nil } + it 'returns nil' do + expect(subject.device_fingerprint).to be_nil + end + end + + context 'when response_body does not contain fuzzy_device_id' do + let(:response_body) { { some_other_key: 'value' } } + it 'returns nil' do + expect(subject.device_fingerprint).to be_nil + end + end + end end From cf92fef499f29eeacfdc73031cf639d092276bd2 Mon Sep 17 00:00:00 2001 From: Davida Marion Date: Tue, 1 Jul 2025 10:11:30 -0400 Subject: [PATCH 2/2] Move fingerprint into threatmetrix stage and actually remove from analytics --- .../concerns/idv/verify_info_concern.rb | 4 +-- .../proofing/resolution/result_adjudicator.rb | 9 +++++-- .../idv/verify_info_controller_spec.rb | 25 ++++++++++++++++--- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb index 1656a59636a..300ef869d8d 100644 --- a/app/controllers/concerns/idv/verify_info_concern.rb +++ b/app/controllers/concerns/idv/verify_info_concern.rb @@ -194,7 +194,6 @@ def async_state_done(current_async_state) last_name_spaced: pii[:last_name].split(' ').many?, previous_ssn_edit_distance: previous_ssn_edit_distance, pii_like_keypaths: [ - [:device_fingerprint], [:errors, :ssn], [:errors, :state_id_jurisdiction], [:proofing_results, :context, :stages, :resolution, :errors, :ssn], @@ -346,7 +345,7 @@ def create_fraud_review_request_if_needed(result) success = (threatmetrix_result[:review_status] == 'pass') attempts_api_tracker.idv_device_risk_assessment( - device_fingerprint: result.dig(:device_fingerprint), + device_fingerprint: threatmetrix_result.dig(:device_fingerprint), success:, failure_reason: device_risk_failure_reason(success, threatmetrix_result), ) @@ -374,6 +373,7 @@ def delete_threatmetrix_response_body(form_response) ) return if threatmetrix_result.blank? + threatmetrix_result.delete(:device_fingerprint) threatmetrix_result.delete(:response_body) end diff --git a/app/services/proofing/resolution/result_adjudicator.rb b/app/services/proofing/resolution/result_adjudicator.rb index 2933e650e93..08db2795df2 100644 --- a/app/services/proofing/resolution/result_adjudicator.rb +++ b/app/services/proofing/resolution/result_adjudicator.rb @@ -43,7 +43,6 @@ def adjudicated_result errors: errors, extra: { exception: exception, - device_fingerprint: device_profiling_result.device_fingerprint, timed_out: timed_out?, threatmetrix_review_status: device_profiling_result.review_status, phone_finder_precheck_passed: phone_finder_result.success?, @@ -55,7 +54,7 @@ def adjudicated_result resolution: resolution_result.to_h, residential_address: residential_resolution_result.to_h, state_id: state_id_result.to_h, - threatmetrix: device_profiling_result.to_h, + threatmetrix:, phone_precheck: phone_finder_result.to_h, }, }, @@ -84,6 +83,12 @@ def exception device_profiling_result.exception end + def threatmetrix + device_profiling_result.to_h.merge( + device_fingerprint: device_profiling_result.device_fingerprint, + ) + end + def timed_out? resolution_result.timed_out? || residential_resolution_result.timed_out? || diff --git a/spec/controllers/idv/verify_info_controller_spec.rb b/spec/controllers/idv/verify_info_controller_spec.rb index 4e1d383f497..404601b08ac 100644 --- a/spec/controllers/idv/verify_info_controller_spec.rb +++ b/spec/controllers/idv/verify_info_controller_spec.rb @@ -197,6 +197,7 @@ stages: { threatmetrix: { success:, + device_fingerprint:, client: threatmetrix_client_id, transaction_id: 1, review_status: review_status, @@ -207,7 +208,6 @@ }, }, }, - device_fingerprint:, errors: {}, exception: nil, success: true, @@ -287,8 +287,17 @@ ) expect(@analytics).to have_logged_event( 'IdV: doc auth verify proofing results', - hash_excluding(device_fingerprint:), + hash_including( + proofing_results: hash_including( + context: hash_including( + stages: hash_including( + threatmetrix: hash_excluding(device_fingerprint:), + ), + ), + ), + ), ) + expect(@analytics).to have_logged_event( :idv_threatmetrix_response_body, response_body: hash_including( @@ -379,6 +388,7 @@ stages: { threatmetrix: { client: nil, + device_fingerprint:, errors: {}, exception: "Unexpected ThreatMetrix review_status value: #{review_status}", response_body: nil, @@ -388,7 +398,6 @@ }, }, }, - device_fingerprint:, success: false, } end @@ -432,7 +441,15 @@ expect(@analytics).to have_logged_event( 'IdV: doc auth verify proofing results', - hash_excluding(device_fingerprint:), + hash_including( + proofing_results: hash_including( + context: hash_including( + stages: hash_including( + threatmetrix: hash_excluding(device_fingerprint:), + ), + ), + ), + ), ) end