diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb index 82e7c2ce7c5..300ef869d8d 100644 --- a/app/controllers/concerns/idv/verify_info_concern.rb +++ b/app/controllers/concerns/idv/verify_info_concern.rb @@ -345,6 +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: threatmetrix_result.dig(:device_fingerprint), success:, failure_reason: device_risk_failure_reason(success, threatmetrix_result), ) @@ -372,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/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..08db2795df2 100644 --- a/app/services/proofing/resolution/result_adjudicator.rb +++ b/app/services/proofing/resolution/result_adjudicator.rb @@ -54,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, }, }, @@ -83,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/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..404601b08ac 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 { @@ -196,6 +197,7 @@ stages: { threatmetrix: { success:, + device_fingerprint:, client: threatmetrix_client_id, transaction_id: 1, review_status: review_status, @@ -283,6 +285,19 @@ ), ), ) + expect(@analytics).to have_logged_event( + 'IdV: doc auth verify proofing results', + 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( @@ -294,6 +309,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 +346,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'] }, ) @@ -371,6 +388,7 @@ stages: { threatmetrix: { client: nil, + device_fingerprint:, errors: {}, exception: "Unexpected ThreatMetrix review_status value: #{review_status}", response_body: nil, @@ -420,6 +438,19 @@ ), ), ) + + expect(@analytics).to have_logged_event( + 'IdV: doc auth verify proofing results', + hash_including( + proofing_results: hash_including( + context: hash_including( + stages: hash_including( + threatmetrix: hash_excluding(device_fingerprint:), + ), + ), + ), + ), + ) end it 'tracks the event for the attempts api' do @@ -451,6 +482,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 +505,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 +549,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