From 55007cbac73c6ccf887de3eca6a51e3b1f28f54f Mon Sep 17 00:00:00 2001 From: Aaron Nagucki Date: Tue, 9 Sep 2025 08:57:19 -0500 Subject: [PATCH 1/8] (feature) Add Fraud Ops Tracking for data warehouse and FCMS update specs undo many events update specs (feature) Add Fraud Ops Tracking for data warehouse and FCMS --- app/controllers/application_controller.rb | 11 +++ .../concerns/idv/phone_otp_rate_limitable.rb | 6 ++ .../concerns/idv/verify_by_mail_concern.rb | 1 + .../concerns/idv/verify_info_concern.rb | 24 ++++++ .../concerns/rate_limit_concern.rb | 2 + app/controllers/idv/address_controller.rb | 10 +++ app/controllers/idv/agreement_controller.rb | 1 + .../idv/by_mail/enter_code_controller.rb | 7 ++ .../enter_code_rate_limited_controller.rb | 3 + .../idv/enter_password_controller.rb | 1 + .../idv/hybrid_handoff_controller.rb | 3 + .../idv/image_uploads_controller.rb | 1 + .../in_person/ready_to_verify_controller.rb | 1 + .../idv/in_person/ssn_controller.rb | 6 ++ .../idv/otp_verification_controller.rb | 6 ++ app/controllers/idv/phone_controller.rb | 14 ++++ app/controllers/idv/ssn_controller.rb | 6 ++ .../sign_up/passwords_controller.rb | 3 +- .../sign_up/registrations_controller.rb | 8 +- app/forms/gpo_verify_form.rb | 7 +- app/forms/idv/api_image_upload_form.rb | 27 +++++++ app/jobs/get_usps_proofing_results_job.rb | 12 +++ app/jobs/socure_docv_results_job.rb | 29 +++++++ app/jobs/socure_image_retrieval_job.rb | 23 ++++++ app/services/attempts_api/tracker.rb | 8 +- app/services/fraud_ops/redis_client.rb | 23 ++++++ app/services/fraud_ops/tracker.rb | 81 +++++++++++++++++++ app/services/idv/phone_step.rb | 4 +- config/application.yml.default | 8 ++ config/initializers/01_redis.rb | 5 ++ lib/action_account.rb | 12 +++ lib/identity_config.rb | 6 ++ spec/jobs/socure_docv_results_job_spec.rb | 2 + spec/services/fraud_ops/redis_client_spec.rb | 40 +++++++++ spec/services/fraud_ops/tracker_spec.rb | 64 +++++++++++++++ 35 files changed, 454 insertions(+), 11 deletions(-) create mode 100644 app/services/fraud_ops/redis_client.rb create mode 100644 app/services/fraud_ops/tracker.rb create mode 100644 spec/services/fraud_ops/redis_client_spec.rb create mode 100644 spec/services/fraud_ops/tracker_spec.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 70ac0391c58..a6f924773cd 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -91,6 +91,17 @@ def attempts_api_tracker ) end + def fraud_ops_tracker + @fraud_ops_tracker ||= FraudOps::Tracker.new( + session_id: attempts_api_session_id, + request:, + user: analytics_user, + sp: current_sp, + cookie_device_uuid: cookies[:device], + sp_redirect_uri: attempts_api_redirect_uri, + ) + end + def user_event_creator @user_event_creator ||= UserEventCreator.new(request: request, current_user: current_user) end diff --git a/app/controllers/concerns/idv/phone_otp_rate_limitable.rb b/app/controllers/concerns/idv/phone_otp_rate_limitable.rb index f0e443849ac..3d1fab4b77d 100644 --- a/app/controllers/concerns/idv/phone_otp_rate_limitable.rb +++ b/app/controllers/concerns/idv/phone_otp_rate_limitable.rb @@ -31,6 +31,9 @@ def handle_too_many_otp_sends attempts_api_tracker.idv_rate_limited( limiter_type: :phone_otp, ) + fraud_ops_tracker.idv_rate_limited( + limiter_type: :phone_otp, + ) handle_max_attempts('otp_requests') end @@ -40,6 +43,9 @@ def handle_too_many_otp_attempts attempts_api_tracker.idv_rate_limited( limiter_type: :phone_otp, ) + fraud_ops_tracker.idv_rate_limited( + limiter_type: :phone_otp, + ) handle_max_attempts('otp_login_attempts') end diff --git a/app/controllers/concerns/idv/verify_by_mail_concern.rb b/app/controllers/concerns/idv/verify_by_mail_concern.rb index b043728d4d1..6ddd74e91e5 100644 --- a/app/controllers/concerns/idv/verify_by_mail_concern.rb +++ b/app/controllers/concerns/idv/verify_by_mail_concern.rb @@ -18,6 +18,7 @@ def log_letter_requested_analytics(resend:) **ab_test_analytics_buckets, ) attempts_api_tracker.idv_verify_by_mail_letter_requested(resend:) + fraud_ops_tracker.idv_verify_by_mail_letter_requested(resend:) end def log_letter_enqueued_analytics(resend:) diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb index a3268388c7f..b1a0be540a1 100644 --- a/app/controllers/concerns/idv/verify_info_concern.rb +++ b/app/controllers/concerns/idv/verify_info_concern.rb @@ -352,6 +352,12 @@ def create_fraud_review_request_if_needed(result) failure_reason: device_risk_failure_reason(success, threatmetrix_result), ) + fraud_ops_tracker.idv_device_risk_assessment( + device_fingerprint: threatmetrix_result.dig(:device_fingerprint), + success:, + failure_reason: device_risk_failure_reason(success, threatmetrix_result), + ) + return if success FraudReviewRequest.create( @@ -414,6 +420,24 @@ def log_verification_attempt(success:, failure_reason: nil) zip: pii_from_doc[:zip], failure_reason:, ) + + fraud_ops_tracker.idv_verification_submitted( + success: success, + document_state: pii_from_doc[:state], + document_number: pii_from_doc[:state_id_number], + document_issued: pii_from_doc[:state_id_issued], + document_expiration: pii_from_doc[:state_id_expiration], + first_name: pii_from_doc[:first_name], + last_name: pii_from_doc[:last_name], + date_of_birth: pii_from_doc[:dob], + address1: pii_from_doc[:address1], + address2: pii_from_doc[:address2], + ssn: idv_session.ssn, + city: pii_from_doc[:city], + state: pii_from_doc[:state], + zip: pii_from_doc[:zip], + failure_reason:, + ) end VerificationFailures = Struct.new( diff --git a/app/controllers/concerns/rate_limit_concern.rb b/app/controllers/concerns/rate_limit_concern.rb index 3cd8e38a53f..5bf2b6d5a78 100644 --- a/app/controllers/concerns/rate_limit_concern.rb +++ b/app/controllers/concerns/rate_limit_concern.rb @@ -47,6 +47,7 @@ def confirm_not_rate_limited_for_phone_and_letter_address_verification return true elsif idv_attempter_rate_limited?(:proof_address) || gpo_verify_by_mail_policy.rate_limited? attempts_api_tracker.idv_rate_limited(limiter_type: :proof_address) + fraud_ops_tracker.idv_rate_limited(limiter_type: :proof_address) end end @@ -54,6 +55,7 @@ def rate_limit_redirect!(rate_limit_type, step_name: nil) if idv_attempter_rate_limited?(rate_limit_type) analytics.rate_limit_reached(limiter_type: rate_limit_type, step_name:) attempts_api_tracker.idv_rate_limited(limiter_type: rate_limit_type) + fraud_ops_tracker.idv_rate_limited(limiter_type: rate_limit_type) rate_limited_redirect(rate_limit_type) return true end diff --git a/app/controllers/idv/address_controller.rb b/app/controllers/idv/address_controller.rb index 8a516a9451b..be20366c0b0 100644 --- a/app/controllers/idv/address_controller.rb +++ b/app/controllers/idv/address_controller.rb @@ -103,6 +103,16 @@ def track_submit_event(form_result) zip: @address_form.zipcode, failure_reason: attempts_api_tracker.parse_failure_reason(form_result), ) + fraud_ops_tracker.idv_address_submitted( + success: form_result.success?, + address1: @address_form.address1, + address2: @address_form.address2, + address_edited: address_edited?, + city: @address_form.city, + state: @address_form.state, + zip: @address_form.zipcode, + failure_reason: fraud_ops_tracker.parse_failure_reason(form_result), + ) end def address_update_request? diff --git a/app/controllers/idv/agreement_controller.rb b/app/controllers/idv/agreement_controller.rb index 4f1dd25ffbc..cb2f41cb4d5 100644 --- a/app/controllers/idv/agreement_controller.rb +++ b/app/controllers/idv/agreement_controller.rb @@ -37,6 +37,7 @@ def update if current_user.has_proofed_before? attempts_api_tracker.idv_reproof + fraud_ops_tracker.idv_reproof end if result.success? diff --git a/app/controllers/idv/by_mail/enter_code_controller.rb b/app/controllers/idv/by_mail/enter_code_controller.rb index e0221fff287..c5398dc07ed 100644 --- a/app/controllers/idv/by_mail/enter_code_controller.rb +++ b/app/controllers/idv/by_mail/enter_code_controller.rb @@ -30,6 +30,7 @@ def index prefilled_code = session[:last_gpo_confirmation_code] if FeatureManagement.reveal_gpo_code? @gpo_verify_form = GpoVerifyForm.new( attempts_api_tracker:, + fraud_ops_tracker:, user: current_user, pii: pii, resolved_authn_context_result: resolved_authn_context_result, @@ -61,6 +62,11 @@ def create failure_reason: attempts_api_tracker.parse_failure_reason(result), ) + fraud_ops_tracker.idv_verify_by_mail_enter_code_submitted( + success: result.success?, + failure_reason: fraud_ops_tracker.parse_failure_reason(result), + ) + send_please_call_email_if_necessary(result:) if !result.success? @@ -144,6 +150,7 @@ def send_please_call_email_if_necessary(result:) def build_gpo_verify_form GpoVerifyForm.new( attempts_api_tracker:, + fraud_ops_tracker:, user: current_user, pii: pii, resolved_authn_context_result: resolved_authn_context_result, diff --git a/app/controllers/idv/by_mail/enter_code_rate_limited_controller.rb b/app/controllers/idv/by_mail/enter_code_rate_limited_controller.rb index 7c2513a23f0..9a933bf1ffd 100644 --- a/app/controllers/idv/by_mail/enter_code_rate_limited_controller.rb +++ b/app/controllers/idv/by_mail/enter_code_rate_limited_controller.rb @@ -17,6 +17,9 @@ def index attempts_api_tracker.idv_rate_limited( limiter_type: :verify_gpo_key, ) + fraud_ops_tracker.idv_rate_limited( + limiter_type: :verify_gpo_key, + ) @expires_at = rate_limiter.expires_at end diff --git a/app/controllers/idv/enter_password_controller.rb b/app/controllers/idv/enter_password_controller.rb index 1a089edf299..4ee64683567 100644 --- a/app/controllers/idv/enter_password_controller.rb +++ b/app/controllers/idv/enter_password_controller.rb @@ -149,6 +149,7 @@ def init_profile profile: idv_session.profile, ) attempts_api_tracker.idv_enrollment_complete(reproof:) + fraud_ops_tracker.idv_enrollment_complete(reproof:) end end diff --git a/app/controllers/idv/hybrid_handoff_controller.rb b/app/controllers/idv/hybrid_handoff_controller.rb index 8e6dbb35c83..2dc57246510 100644 --- a/app/controllers/idv/hybrid_handoff_controller.rb +++ b/app/controllers/idv/hybrid_handoff_controller.rb @@ -226,6 +226,9 @@ def rate_limited_failure attempts_api_tracker.idv_rate_limited( limiter_type: :idv_send_link, ) + fraud_ops_tracker.idv_rate_limited( + limiter_type: :idv_send_link, + ) message = I18n.t( 'doc_auth.errors.send_link_limited', timeout: distance_of_time_in_words( diff --git a/app/controllers/idv/image_uploads_controller.rb b/app/controllers/idv/image_uploads_controller.rb index 8876e7ee1a6..bfbc5bcdc9e 100644 --- a/app/controllers/idv/image_uploads_controller.rb +++ b/app/controllers/idv/image_uploads_controller.rb @@ -23,6 +23,7 @@ def image_upload_form acuant_sdk_upgrade_ab_test_bucket: ab_test_bucket(:ACUANT_SDK), analytics:, attempts_api_tracker:, + fraud_ops_tracker:, liveness_checking_required: resolved_authn_context_result.facial_match?, service_provider: current_sp, uuid_prefix: current_sp&.app_id, diff --git a/app/controllers/idv/in_person/ready_to_verify_controller.rb b/app/controllers/idv/in_person/ready_to_verify_controller.rb index a3dc30e2fd8..fe9918c95f5 100644 --- a/app/controllers/idv/in_person/ready_to_verify_controller.rb +++ b/app/controllers/idv/in_person/ready_to_verify_controller.rb @@ -20,6 +20,7 @@ def show @is_enhanced_ipp = resolved_authn_context_result.enhanced_ipp? analytics.idv_in_person_ready_to_verify_visit(**opt_in_analytics_properties) attempts_api_tracker.idv_ipp_ready_to_verify_visit + fraud_ops_tracker.idv_ipp_ready_to_verify_visit @presenter = ReadyToVerifyPresenter.new( enrollment: enrollment, ) diff --git a/app/controllers/idv/in_person/ssn_controller.rb b/app/controllers/idv/in_person/ssn_controller.rb index 95de33d782a..8c06f92878f 100644 --- a/app/controllers/idv/in_person/ssn_controller.rb +++ b/app/controllers/idv/in_person/ssn_controller.rb @@ -52,6 +52,12 @@ def update failure_reason: attempts_api_tracker.parse_failure_reason(form_response), ) + fraud_ops_tracker.idv_ssn_submitted( + success: form_response.success?, + ssn: ssn_params[:ssn], + failure_reason: fraud_ops_tracker.parse_failure_reason(form_response), + ) + if form_response.success? idv_session.previous_ssn = idv_session.ssn idv_session.ssn = SsnFormatter.normalize(ssn_params[:ssn]) diff --git a/app/controllers/idv/otp_verification_controller.rb b/app/controllers/idv/otp_verification_controller.rb index 6034dd061c2..984d2e15c9f 100644 --- a/app/controllers/idv/otp_verification_controller.rb +++ b/app/controllers/idv/otp_verification_controller.rb @@ -30,6 +30,12 @@ def update failure_reason: result.success? ? nil : failure_reason(result.to_h), ) + fraud_ops_tracker.idv_phone_otp_submitted( + phone_number: Phonelib.parse(idv_session.user_phone_confirmation_session.phone).e164, + success: result.success?, + failure_reason: result.success? ? nil : failure_reason(result.to_h), + ) + if result.success? idv_session.mark_phone_step_complete! save_in_person_notification_phone diff --git a/app/controllers/idv/phone_controller.rb b/app/controllers/idv/phone_controller.rb index 2881555cd77..394a998bbd9 100644 --- a/app/controllers/idv/phone_controller.rb +++ b/app/controllers/idv/phone_controller.rb @@ -63,6 +63,12 @@ def create failure_reason: attempts_api_tracker.parse_failure_reason(result), ) + fraud_ops_tracker.idv_phone_submitted( + phone_number: Phonelib.parse(step_params[:phone]).e164, + success: result.success?, + failure_reason: fraud_ops_tracker.parse_failure_reason(result), + ) + if result.success? submit_proofing_attempt redirect_to idv_phone_path @@ -129,6 +135,13 @@ def send_phone_confirmation_otp_and_handle_result failure_reason: result.success? ? nil : otp_sent_tracker_error(result), ) + fraud_ops_tracker.idv_phone_otp_sent( + phone_number: Phonelib.parse(idv_session.user_phone_confirmation_session.phone).e164, + success: result.success?, + otp_delivery_method: result.extra[:otp_delivery_preference], + failure_reason: result.success? ? nil : otp_sent_tracker_error(result), + ) + if result.success? redirect_to idv_otp_verification_url else @@ -154,6 +167,7 @@ def step trace_id: amzn_trace_id, analytics:, attempts_api_tracker:, + fraud_ops_tracker:, ) end diff --git a/app/controllers/idv/ssn_controller.rb b/app/controllers/idv/ssn_controller.rb index 487422fcad2..4737d358d91 100644 --- a/app/controllers/idv/ssn_controller.rb +++ b/app/controllers/idv/ssn_controller.rb @@ -52,6 +52,12 @@ def update failure_reason: attempts_api_tracker.parse_failure_reason(form_response), ) + fraud_ops_tracker.idv_ssn_submitted( + success: form_response.success?, + ssn: ssn_params[:ssn], + failure_reason: fraud_ops_tracker.parse_failure_reason(form_response), + ) + if form_response.success? idv_session.previous_ssn = idv_session.ssn idv_session.ssn = SsnFormatter.normalize(ssn_params[:ssn]) diff --git a/app/controllers/sign_up/passwords_controller.rb b/app/controllers/sign_up/passwords_controller.rb index 05c3b571120..1c01bf5f664 100644 --- a/app/controllers/sign_up/passwords_controller.rb +++ b/app/controllers/sign_up/passwords_controller.rb @@ -39,10 +39,9 @@ def forbidden_passwords def track_analytics(result) analytics.password_creation(**result) - failure_reason = attempts_api_tracker.parse_failure_reason(result) attempts_api_tracker.user_registration_password_submitted( success: result.success?, - failure_reason:, + failure_reason: attempts_api_tracker.parse_failure_reason(result), ) end diff --git a/app/controllers/sign_up/registrations_controller.rb b/app/controllers/sign_up/registrations_controller.rb index 6041d280812..f65b245347a 100644 --- a/app/controllers/sign_up/registrations_controller.rb +++ b/app/controllers/sign_up/registrations_controller.rb @@ -11,12 +11,16 @@ class RegistrationsController < ApplicationController CREATE_ACCOUNT = 'create_account' def new - @register_user_email_form = RegisterUserEmailForm.new(analytics:, attempts_api_tracker:) + @register_user_email_form = RegisterUserEmailForm.new( + analytics:, attempts_api_tracker:, + ) analytics.user_registration_enter_email_visit end def create - @register_user_email_form = RegisterUserEmailForm.new(analytics:, attempts_api_tracker:) + @register_user_email_form = RegisterUserEmailForm.new( + analytics:, attempts_api_tracker:, + ) result = @register_user_email_form.submit(permitted_params.merge(request_id:)) diff --git a/app/forms/gpo_verify_form.rb b/app/forms/gpo_verify_form.rb index 8ef9576be41..7c8257c91c8 100644 --- a/app/forms/gpo_verify_form.rb +++ b/app/forms/gpo_verify_form.rb @@ -9,14 +9,16 @@ class GpoVerifyForm validate :validate_pending_profile attr_accessor :otp, :pii, :pii_attributes - attr_reader :attempts_api_tracker, :user, :resolved_authn_context_result + attr_reader :attempts_api_tracker, :user, :resolved_authn_context_result, :fraud_ops_tracker - def initialize(attempts_api_tracker:, user:, pii:, resolved_authn_context_result:, otp: nil) + def initialize(attempts_api_tracker:, user:, pii:, resolved_authn_context_result:, + fraud_ops_tracker:, otp: nil) @attempts_api_tracker = attempts_api_tracker @user = user @pii = pii @resolved_authn_context_result = resolved_authn_context_result @otp = otp + @fraud_ops_tracker = fraud_ops_tracker end def submit @@ -40,6 +42,7 @@ def submit if pending_profile&.active? attempts_api_tracker.idv_enrollment_complete(reproof:) + fraud_ops_tracker.idv_enrollment_complete(reproof:) end FormResponse.new( diff --git a/app/forms/idv/api_image_upload_form.rb b/app/forms/idv/api_image_upload_form.rb index 11a3eea095f..b43f83a3426 100644 --- a/app/forms/idv/api_image_upload_form.rb +++ b/app/forms/idv/api_image_upload_form.rb @@ -16,6 +16,7 @@ def initialize( params, acuant_sdk_upgrade_ab_test_bucket:, attempts_api_tracker:, + fraud_ops_tracker:, service_provider:, analytics: nil, liveness_checking_required: false, @@ -25,6 +26,7 @@ def initialize( @acuant_sdk_upgrade_ab_test_bucket = acuant_sdk_upgrade_ab_test_bucket @analytics = analytics @attempts_api_tracker = attempts_api_tracker + @fraud_ops_tracker = fraud_ops_tracker @readable = {} @service_provider = service_provider @uuid_prefix = uuid_prefix @@ -89,6 +91,24 @@ def submit zip: pii_from_doc[:zip], failure_reason: failure_reason(response), ) + + fraud_ops_tracker.idv_document_upload_submitted( + **doc_escrow_images, + success: response.success?, + document_state: pii_from_doc[:state], + document_number: pii_from_doc[:state_id_number], + document_issued: pii_from_doc[:state_id_issued], + document_expiration: pii_from_doc[:state_id_expiration], + first_name: pii_from_doc[:first_name], + last_name: pii_from_doc[:last_name], + date_of_birth: pii_from_doc[:dob], + address1: pii_from_doc[:address1], + address2: pii_from_doc[:address2], + city: pii_from_doc[:city], + state: pii_from_doc[:state], + zip: pii_from_doc[:zip], + failure_reason: failure_reason(response), + ) end abandon_any_ipp_progress @@ -100,6 +120,7 @@ def submit attr_reader :acuant_sdk_upgrade_ab_test_bucket, :analytics, :attempts_api_tracker, + :fraud_ops_tracker, :form_response, :liveness_checking_required, :params, @@ -147,6 +168,11 @@ def track_upload_attempt(response) success: response.success?, failure_reason: attempts_api_tracker.parse_failure_reason(response), ) + fraud_ops_tracker.idv_document_uploaded( + **doc_escrow_images, + success: response.success?, + failure_reason: fraud_ops_tracker.parse_failure_reason(response), + ) end def doc_escrow_enabled? @@ -404,6 +430,7 @@ def track_rate_limited user_id: user_uuid, ) attempts_api_tracker.idv_rate_limited(limiter_type: :idv_doc_auth) + fraud_ops_tracker.idv_rate_limited(limiter_type: :idv_doc_auth) end def document_capture_session_uuid diff --git a/app/jobs/get_usps_proofing_results_job.rb b/app/jobs/get_usps_proofing_results_job.rb index 8c5e7d4b2f2..82bf248f144 100644 --- a/app/jobs/get_usps_proofing_results_job.rb +++ b/app/jobs/get_usps_proofing_results_job.rb @@ -495,6 +495,7 @@ def handle_successful_status_update(enrollment, response) if enrollment.profile&.active? attempts_api_tracker(enrollment:).idv_enrollment_complete(reproof:) + fraud_ops_tracker(enrollment:).idv_enrollment_complete(reproof:) end # send SMS and email @@ -520,6 +521,17 @@ def attempts_api_tracker(enrollment:) ) end + def fraud_ops_tracker(enrollment:) + FraudOpsTracker.new( + session_id: nil, + request: nil, + user: enrollment.user, + sp: enrollment.service_provider, + cookie_device_uuid: nil, + sp_redirect_uri: nil, + ) + end + def handle_passed_with_fraud_review_pending(enrollment, response) proofed_at = parse_usps_timestamp(response['transactionEndDateTime']) enrollment_outcomes[:enrollments_in_fraud_review] += 1 diff --git a/app/jobs/socure_docv_results_job.rb b/app/jobs/socure_docv_results_job.rb index 69f4b1baf2f..a6ff4d6d5ba 100644 --- a/app/jobs/socure_docv_results_job.rb +++ b/app/jobs/socure_docv_results_job.rb @@ -143,6 +143,24 @@ def record_attempt( zip: pii_from_doc[:zipcode], failure_reason:, ) + + fraud_ops_tracker.idv_document_upload_submitted( + **image_data, + success:, + document_state: pii_from_doc[:state], + document_number: pii_from_doc[:state_id_number] || pii_from_doc[:document_number], + document_issued: pii_from_doc[:state_id_issued] || pii_from_doc[:passport_issued], + document_expiration: pii_from_doc[:state_id_expiration] || pii_from_doc[:passport_expiration], + first_name: pii_from_doc[:first_name], + last_name: pii_from_doc[:last_name], + date_of_birth: pii_from_doc[:dob], + address1: pii_from_doc[:address1], + address2: pii_from_doc[:address2], + city: pii_from_doc[:city], + state: pii_from_doc[:state], + zip: pii_from_doc[:zipcode], + failure_reason:, + ) end def image_storage_data(ial2:, passport_book:) @@ -178,6 +196,17 @@ def attempts_api_tracker ) end + def fraud_ops_tracker + @fraud_ops_tracker ||= FraudOps::Tracker.new( + session_id: nil, + request: nil, + user: document_capture_session.user, + sp:, + cookie_device_uuid: nil, + sp_redirect_uri: nil, + ) + end + def log_verification_request(docv_result_response:, vendor_request_time_in_ms:) analytics.idv_socure_verification_data_requested( **docv_result_response.to_h.merge( diff --git a/app/jobs/socure_image_retrieval_job.rb b/app/jobs/socure_image_retrieval_job.rb index afcad33258e..18c4555ab39 100644 --- a/app/jobs/socure_image_retrieval_job.rb +++ b/app/jobs/socure_image_retrieval_job.rb @@ -29,6 +29,18 @@ def perform( :document_selfie_image_file_id, ), ) + fraud_ops_tracker.idv_image_retrieval_failed( + document_back_image_file_id: image_storage_data.dig(:back, :document_back_image_file_id), + document_front_image_file_id: image_storage_data.dig(:front, :document_front_image_file_id), + document_passport_image_file_id: image_storage_data.dig( + :passport, + :document_passport_image_file_id, + ), + document_selfie_image_file_id: image_storage_data.dig( + :selfie, + :document_selfie_image_file_id, + ), + ) end end @@ -44,6 +56,17 @@ def attempts_api_tracker ) end + def fraud_ops_tracker + @fraud_ops_tracker ||= FraudOps::Tracker.new( + session_id: nil, + request: nil, + user: document_capture_session.user, + sp:, + cookie_device_uuid: nil, + sp_redirect_uri: nil, + ) + end + def document_capture_session @document_capture_session ||= DocumentCaptureSession.find_by(uuid: document_capture_session_uuid) diff --git a/app/services/attempts_api/tracker.rb b/app/services/attempts_api/tracker.rb index dcc984f3de6..52b9c68c203 100644 --- a/app/services/attempts_api/tracker.rb +++ b/app/services/attempts_api/tracker.rb @@ -62,12 +62,12 @@ def jwe(event) ) end - def extra_attributes + def extra_attributes(event_type: nil) # rubocop:disable Lint/UnusedMethodArgument {} end - def extra_metadata(metadata:) - failure_metadata(metadata:).merge(extra_attributes) + def extra_metadata(event_type:, metadata:) + failure_metadata(metadata:).merge(extra_attributes(event_type:)) end def failure_metadata(metadata:) @@ -91,7 +91,7 @@ def event_metadata(event_type:, metadata:) client_port: CloudFrontHeaderParser.new(request).client_port, aws_region: IdentityConfig.store.aws_region, google_analytics_cookies: google_analytics_cookies(request), - }.merge!(extra_metadata(metadata:)) + }.merge!(extra_metadata(event_type:, metadata:)) end def google_analytics_cookies(request) diff --git a/app/services/fraud_ops/redis_client.rb b/app/services/fraud_ops/redis_client.rb new file mode 100644 index 00000000000..17205228838 --- /dev/null +++ b/app/services/fraud_ops/redis_client.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module FraudOps + class RedisClient + def write_event(event_key:, jwe:, timestamp:) + key = five_minute_key(timestamp) + REDIS_FRAUD_OPS_POOL.with do |client| + client.hset(key, event_key, jwe) + client.expire(key, IdentityConfig.store.fraud_ops_event_ttl_seconds) + end + end + + private + + def five_minute_key(timestamp) + formatted_time = timestamp + .in_time_zone('UTC') + .change(min: (timestamp.min / 5) * 5) + .iso8601 + "fraud-ops-events:#{formatted_time}" + end + end +end diff --git a/app/services/fraud_ops/tracker.rb b/app/services/fraud_ops/tracker.rb new file mode 100644 index 00000000000..b389a9a3bf9 --- /dev/null +++ b/app/services/fraud_ops/tracker.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module FraudOps + class Tracker < AttemptsApi::Tracker + def initialize(session_id:, request:, user:, sp:, cookie_device_uuid:, sp_redirect_uri:) + super( + session_id: session_id, + request: request, + user: user, + sp: sp, + cookie_device_uuid: cookie_device_uuid, + sp_redirect_uri: sp_redirect_uri, + enabled_for_session: true, + ) + end + + def track_event(event_type, metadata = {}) + return unless enabled? + + event = AttemptsApi::AttemptEvent.new( + event_type: event_type, + session_id: session_id, + occurred_at: Time.zone.now, + event_metadata: event_metadata(event_type:, metadata:), + ) + + redis_client.write_event( + event_key: event.jti, + jwe: jwe(event), + timestamp: event.occurred_at, + ) + + event + end + + private + + def extra_attributes(event_type: nil) + { + agency_uuid: agency_uuid(event_type:), + user_uuid: user&.uuid, + user_id: user&.id, + unique_session_id: user&.unique_session_id, + } + end + + def enabled? + IdentityConfig.store.fraud_ops_tracker_enabled + end + + def public_key + @public_key ||= OpenSSL::PKey::RSA.new(IdentityConfig.store.fraud_ops_public_key) + end + + def redis_client + @redis_client ||= FraudOps::RedisClient.new + end + + def jwe(event) + to_jwe( + event: event, + issuer: sp.issuer, + public_key: public_key, + ) + end + + def to_jwe(event:, issuer:, public_key:) + jwk = JWT::JWK.new(public_key) + + JWE.encrypt( + event.payload(issuer:).to_json, + public_key, + typ: 'secevent+jwe', + zip: 'DEF', + alg: 'RSA-OAEP', + enc: 'A256GCM', + kid: jwk.kid, + ) + end + end +end diff --git a/app/services/idv/phone_step.rb b/app/services/idv/phone_step.rb index f1f2a5843b6..aa2b1759822 100644 --- a/app/services/idv/phone_step.rb +++ b/app/services/idv/phone_step.rb @@ -2,11 +2,12 @@ module Idv class PhoneStep - def initialize(idv_session:, trace_id:, analytics:, attempts_api_tracker:) + def initialize(idv_session:, trace_id:, analytics:, attempts_api_tracker:, fraud_ops_tracker:) self.idv_session = idv_session @trace_id = trace_id @analytics = analytics @attempts_api_tracker = attempts_api_tracker + @fraud_ops_tracker = fraud_ops_tracker end def submit(step_params) @@ -126,6 +127,7 @@ def rate_limited_result @attempts_api_tracker.idv_rate_limited( limiter_type: :proof_address, ) + @fraud_ops_tracker.idv_rate_limited(limiter_type: :proof_address) FormResponse.new(success: false) end diff --git a/config/application.yml.default b/config/application.yml.default index 74e16760d4c..d66baca7ff0 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -168,6 +168,11 @@ event_disavowal_expiration_hours: 240 facial_match_general_availability_enabled: true feature_idv_force_gpo_verification_enabled: false feature_idv_hybrid_flow_enabled: true +fraud_ops_encryption_key: '' +fraud_ops_event_ttl_seconds: 604800 +fraud_ops_public_key: '' +fraud_ops_s3_bucket: '' +fraud_ops_tracker_enabled: false geo_data_file_path: 'geo_data/GeoLite2-City.mmdb' get_usps_proofing_results_job_cron: '0/30 * * * *' get_usps_proofing_results_job_reprocess_delay_minutes: 5 @@ -378,6 +383,8 @@ recommend_webauthn_platform_for_sms_ab_test_authentication_percent: 0 recovery_code_length: 4 redis_attempts_api_pool_size: 1 redis_attempts_api_url: redis://localhost:6379/2 +redis_fraud_ops_pool_size: 5 +redis_fraud_ops_url: 'redis://localhost:6379/3' redis_pool_size: 10 redis_throttle_pool_size: 5 redis_throttle_url: redis://localhost:6379/1 @@ -613,6 +620,7 @@ test: domain_name: www.example.com dos_passport_composite_healthcheck_endpoint: 'https://dos-passport-api.test/composite-healthcheck/' email_registrations_per_ip_limit: 3 + fraud_ops_public_key: 123abc hmac_fingerprinter_key: a2c813d4dca919340866ba58063e4072adc459b767a74cf2666d5c1eef3861db26708e7437abde1755eb24f4034386b0fea1850a1cb7e56bff8fae3cc6ade96c hmac_fingerprinter_key_queue: '["old-key-one", "old-key-two"]' identity_pki_disabled: true diff --git a/config/initializers/01_redis.rb b/config/initializers/01_redis.rb index 3a88b1c43f9..832b0086134 100644 --- a/config/initializers/01_redis.rb +++ b/config/initializers/01_redis.rb @@ -14,3 +14,8 @@ ConnectionPool.new(size: IdentityConfig.store.redis_attempts_api_pool_size) do Redis.new(url: IdentityConfig.store.redis_attempts_api_url) end.freeze + +REDIS_FRAUD_OPS_POOL = + ConnectionPool.new(size: IdentityConfig.store.redis_fraud_ops_pool_size) do + Redis.new(url: IdentityConfig.store.redis_fraud_ops_url) + end.freeze diff --git a/lib/action_account.rb b/lib/action_account.rb index e858113fa06..a0a95e031c6 100644 --- a/lib/action_account.rb +++ b/lib/action_account.rb @@ -320,6 +320,7 @@ def run(args:, config:) if profile.active? attempts_api_tracker(profile:).idv_enrollment_complete(reproof:) + fraud_ops_tracker(profile:).idv_enrollment_complete(reproof:) UserEventCreator.new(current_user: user) .create_out_of_band_user_event(:account_verified) UserAlerts::AlertUserAboutAccountVerified.call(profile: profile) @@ -401,6 +402,17 @@ def attempts_api_tracker(profile:) sp_redirect_uri: nil, ) end + + def fraud_ops_tracker(profile:) + FraudOpsTracker.new( + session_id: nil, + request: nil, + user: profile.user, + sp: profile.initiating_service_provider, + cookie_device_uuid: nil, + sp_redirect_uri: nil, + ) + end end class DeactivateDuplicate diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 90a13d2d05e..d95639018fd 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -186,6 +186,10 @@ def self.store config.add(:facial_match_general_availability_enabled, type: :boolean) config.add(:feature_idv_force_gpo_verification_enabled, type: :boolean) config.add(:feature_idv_hybrid_flow_enabled, type: :boolean) + config.add(:fraud_ops_tracker_enabled, type: :boolean) + config.add(:fraud_ops_public_key, type: :string) + config.add(:fraud_ops_s3_bucket, type: :string) + config.add(:fraud_ops_event_ttl_seconds, type: :integer) config.add(:irs_registration_funnel_issuers, type: :json) config.add(:irs_registration_funnel_emails, type: :json) config.add(:irs_fraud_metrics_issuers, type: :json) @@ -400,6 +404,8 @@ def self.store config.add(:recovery_code_length, type: :integer) config.add(:redis_attempts_api_pool_size, type: :integer) config.add(:redis_attempts_api_url, type: :string) + config.add(:redis_fraud_ops_pool_size, type: :integer) + config.add(:redis_fraud_ops_url, type: :string) config.add(:redis_pool_size, type: :integer) config.add(:redis_throttle_pool_size, type: :integer) config.add(:redis_throttle_url, type: :string) diff --git a/spec/jobs/socure_docv_results_job_spec.rb b/spec/jobs/socure_docv_results_job_spec.rb index f8efdbabc54..67d53e14d16 100644 --- a/spec/jobs/socure_docv_results_job_spec.rb +++ b/spec/jobs/socure_docv_results_job_spec.rb @@ -7,6 +7,7 @@ let(:user) { create(:user) } let(:fake_analytics) { FakeAnalytics.new } let(:attempts_api_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } + let(:fraud_opt_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } let(:sp) { create(:service_provider) } let(:socure_docv_transaction_token) { 'abcd' } let(:document_capture_session) { DocumentCaptureSession.create(user:) } @@ -45,6 +46,7 @@ allow(IdentityConfig.store).to receive(:doc_auth_passports_enabled).and_return( doc_auth_passports_enabled, ) + allow(FraudOps::Tracker).to receive(:new).and_return(fraud_opt_tracker) rate_limiter.increment! diff --git a/spec/services/fraud_ops/redis_client_spec.rb b/spec/services/fraud_ops/redis_client_spec.rb new file mode 100644 index 00000000000..aaa84d39678 --- /dev/null +++ b/spec/services/fraud_ops/redis_client_spec.rb @@ -0,0 +1,40 @@ +require 'rails_helper' + +RSpec.describe FraudOps::RedisClient do + subject(:redis_client) { FraudOps::RedisClient.new } + + before do + allow(IdentityConfig.store).to receive(:fraud_ops_event_ttl_seconds).and_return(604800) + + REDIS_FRAUD_OPS_POOL.with do |client| + client.keys('fraud-ops-events:*').each { |key| client.del(key) } + end + end + + describe '#write_event' do + it 'writes a JWE token to Redis with expiration' do + event_key = SecureRandom.uuid + jwe_token = 'eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.test-jwe-token' + timestamp = Time.zone.now + + redis_client.write_event( + event_key: event_key, + jwe: jwe_token, + timestamp: timestamp, + ) + + REDIS_FRAUD_OPS_POOL.with do |client| + five_minute_key = "fraud-ops-events:#{timestamp.in_time_zone('UTC').change( + min: (timestamp.min / 5) * 5, + sec: 0, + ).iso8601}" + stored_data = client.hget(five_minute_key, event_key) + expect(stored_data).to eq(jwe_token) + + ttl = client.ttl(five_minute_key) + expect(ttl).to be > 0 + expect(ttl).to be <= 604800 + end + end + end +end diff --git a/spec/services/fraud_ops/tracker_spec.rb b/spec/services/fraud_ops/tracker_spec.rb new file mode 100644 index 00000000000..282e79c5ffa --- /dev/null +++ b/spec/services/fraud_ops/tracker_spec.rb @@ -0,0 +1,64 @@ +require 'rails_helper' + +RSpec.describe FraudOps::Tracker do + let(:user) { create(:user) } + let(:sp) { create(:service_provider) } + let(:fraud_ops_private_key) { OpenSSL::PKey::RSA.new(2048) } + let(:fraud_ops_public_key) { fraud_ops_private_key.public_key } + let(:request) do + double( + 'request', user_agent: 'test browser', remote_ip: '192.168.1.1', cookies: {}, + headers: {} + ) + end + let(:session_id) { SecureRandom.hex(16) } + let(:cookie_device_uuid) { SecureRandom.hex(16) } + let(:sp_redirect_uri) { 'https://example.com/redirect' } + + subject(:tracker) do + FraudOps::Tracker.new( + session_id: session_id, + request: request, + user: user, + sp: sp, + cookie_device_uuid: cookie_device_uuid, + sp_redirect_uri: sp_redirect_uri, + ) + end + + before do + allow(IdentityConfig.store).to receive(:fraud_ops_tracker_enabled).and_return(true) + allow(IdentityConfig.store).to receive(:fraud_ops_public_key).and_return( + fraud_ops_public_key.to_pem, + ) + allow(IdentityConfig.store).to receive(:fraud_ops_event_ttl_seconds).and_return(604800) + allow(IdentityConfig.store).to receive(:aws_region).and_return('us-east-1') + end + + describe 'inheritance and functionality' do + it 'inherits tracking methods from AttemptsApi::Tracker' do + expect(tracker).to respond_to(:login_email_and_password_auth) + expect(tracker).to respond_to(:logout_initiated) + expect(tracker).to respond_to(:session_timeout) + end + + it 'uses FraudOps::RedisClient for Redis operations' do + redis_wrapper = instance_double(FraudOps::RedisClient) + allow(FraudOps::RedisClient).to receive(:new).and_return(redis_wrapper) + allow(redis_wrapper).to receive(:write_event) + + new_tracker = FraudOps::Tracker.new( + session_id: SecureRandom.hex(16), + request: request, + user: user, + sp: sp, + cookie_device_uuid: cookie_device_uuid, + sp_redirect_uri: sp_redirect_uri, + ) + + new_tracker.login_email_and_password_auth(success: true) + + expect(redis_wrapper).to have_received(:write_event) + end + end +end From d5d7f4fe50b578784ecf0daa25320b2afc6787f1 Mon Sep 17 00:00:00 2001 From: Aaron Nagucki Date: Thu, 18 Sep 2025 13:25:07 -0500 Subject: [PATCH 2/8] changelog: Internal, Reporting, Tracking for Fraud Ops (Team Data-1105) From 9945058262654d8cf70353c7d61d39914757ac42 Mon Sep 17 00:00:00 2001 From: Aaron Nagucki Date: Thu, 18 Sep 2025 14:36:15 -0500 Subject: [PATCH 3/8] test redis url --- .gitlab-ci.yml | 2 +- app/jobs/get_usps_proofing_results_job.rb | 2 +- config/application.yml.default | 2 +- lib/action_account.rb | 2 +- spec/features/idv/doc_auth/document_capture_spec.rb | 4 ++++ spec/forms/idv/api_image_upload_form_spec.rb | 2 ++ spec/jobs/get_usps_proofing_results_job_spec.rb | 2 ++ spec/jobs/socure_image_retrieval_job_spec.rb | 2 ++ spec/lib/action_account_spec.rb | 2 ++ spec/services/idv/phone_step_spec.rb | 2 ++ 10 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index af1449929d4..4957cc4aab4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -292,7 +292,7 @@ specs: - cp -a keys.example keys - cp -a certs.example certs - cp pwned_passwords/pwned_passwords.txt.sample pwned_passwords/pwned_passwords.txt - - "echo -e \"test:\n redis_url: 'redis://db-redis:6379/0'\n redis_throttle_url: 'redis://db-redis:6379/1'\n redis_attempts_api_url: 'redis://db-redis:6379/2'\" > config/application.yml" + - "echo -e \"test:\n redis_url: 'redis://db-redis:6379/0'\n redis_throttle_url: 'redis://db-redis:6379/1'\n redis_attempts_api_url: 'redis://db-redis:6379/2'\n redis_fraud_ops_url: 'redis://db-redis:6379/3'\" > config/application.yml" - bundle exec rake db:create db:migrate --trace - bundle exec rake db:seed - bundle exec rake knapsack:rspec["--format documentation --format RspecJunitFormatter --out rspec.xml --format json --out rspec_json/${CI_NODE_INDEX}.json"] diff --git a/app/jobs/get_usps_proofing_results_job.rb b/app/jobs/get_usps_proofing_results_job.rb index 82bf248f144..f9c9b936863 100644 --- a/app/jobs/get_usps_proofing_results_job.rb +++ b/app/jobs/get_usps_proofing_results_job.rb @@ -522,7 +522,7 @@ def attempts_api_tracker(enrollment:) end def fraud_ops_tracker(enrollment:) - FraudOpsTracker.new( + FraudOps::Tracker.new( session_id: nil, request: nil, user: enrollment.user, diff --git a/config/application.yml.default b/config/application.yml.default index d66baca7ff0..1a6c4aeec45 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -384,7 +384,7 @@ recovery_code_length: 4 redis_attempts_api_pool_size: 1 redis_attempts_api_url: redis://localhost:6379/2 redis_fraud_ops_pool_size: 5 -redis_fraud_ops_url: 'redis://localhost:6379/3' +redis_fraud_ops_url: redis://localhost:6379/3 redis_pool_size: 10 redis_throttle_pool_size: 5 redis_throttle_url: redis://localhost:6379/1 diff --git a/lib/action_account.rb b/lib/action_account.rb index a0a95e031c6..a84d8f4ad7c 100644 --- a/lib/action_account.rb +++ b/lib/action_account.rb @@ -404,7 +404,7 @@ def attempts_api_tracker(profile:) end def fraud_ops_tracker(profile:) - FraudOpsTracker.new( + FraudOps::Tracker.new( session_id: nil, request: nil, user: profile.user, diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb index d22bcb35948..7b060c8da5c 100644 --- a/spec/features/idv/doc_auth/document_capture_spec.rb +++ b/spec/features/idv/doc_auth/document_capture_spec.rb @@ -11,6 +11,7 @@ let(:max_attempts) { IdentityConfig.store.doc_auth_max_attempts } let(:fake_analytics) { FakeAnalytics.new } let(:attempts_api_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } + let(:fraud_ops_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } let(:passports_enabled) { false } before(:each) do @@ -18,6 +19,9 @@ allow_any_instance_of(ApplicationController).to receive(:attempts_api_tracker).and_return( attempts_api_tracker, ) + allow_any_instance_of(ApplicationController).to receive(:fraud_ops_tracker).and_return( + fraud_ops_tracker, + ) allow_any_instance_of(ServiceProviderSession).to receive(:sp_name).and_return(@sp_name) allow(IdentityConfig.store).to receive(:doc_auth_passports_enabled) .and_return(passports_enabled) diff --git a/spec/forms/idv/api_image_upload_form_spec.rb b/spec/forms/idv/api_image_upload_form_spec.rb index f105bf420cd..f51496b6d85 100644 --- a/spec/forms/idv/api_image_upload_form_spec.rb +++ b/spec/forms/idv/api_image_upload_form_spec.rb @@ -22,12 +22,14 @@ service_provider:, analytics: fake_analytics, attempts_api_tracker:, + fraud_ops_tracker:, liveness_checking_required:, acuant_sdk_upgrade_ab_test_bucket:, ) end let(:attempts_api_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } + let(:fraud_ops_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } let(:attempts_api_enabled_for_sp) { false } let(:front_image) { DocAuthImageFixtures.document_front_image_multipart } let(:back_image) { DocAuthImageFixtures.document_back_image_multipart } diff --git a/spec/jobs/get_usps_proofing_results_job_spec.rb b/spec/jobs/get_usps_proofing_results_job_spec.rb index 967b3be794a..a80975659b9 100644 --- a/spec/jobs/get_usps_proofing_results_job_spec.rb +++ b/spec/jobs/get_usps_proofing_results_job_spec.rb @@ -7,6 +7,7 @@ let(:in_person_results_delay_in_hours) { 2 } let(:analytics) { FakeAnalytics.new } let(:attempts_api_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } + let(:fraud_ops_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } let(:default_job_completion_analytics) do { enrollments_checked: 0, @@ -32,6 +33,7 @@ allow(analytics).to receive(:idv_in_person_usps_proofing_results_job_completed) allow(Analytics).to receive(:new).and_return(analytics) allow(AttemptsApi::Tracker).to receive(:new).and_return(attempts_api_tracker) + allow(FraudOps::Tracker).to receive(:new).and_return(fraud_ops_tracker) allow(IdentityConfig.store).to receive(:usps_mock_fallback).and_return(false) allow(IdentityConfig.store).to receive(:in_person_results_delay_in_hours).and_return( in_person_results_delay_in_hours, diff --git a/spec/jobs/socure_image_retrieval_job_spec.rb b/spec/jobs/socure_image_retrieval_job_spec.rb index 7ec069e4daf..733d35c687f 100644 --- a/spec/jobs/socure_image_retrieval_job_spec.rb +++ b/spec/jobs/socure_image_retrieval_job_spec.rb @@ -5,6 +5,7 @@ RSpec.describe SocureImageRetrievalJob do let(:job) { described_class.new } let(:attempts_api_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } + let(:fraud_ops_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } let(:sp) { create(:service_provider) } let(:user) { create(:user) } let(:document_capture_session) do @@ -25,6 +26,7 @@ before do allow(AttemptsApi::Tracker).to receive(:new).and_return(attempts_api_tracker) + allow(FraudOps::Tracker).to receive(:new).and_return(fraud_ops_tracker) allow(EncryptedDocStorage::DocWriter).to receive(:new).and_return(writer) document_capture_session.update(issuer: sp.issuer) diff --git a/spec/lib/action_account_spec.rb b/spec/lib/action_account_spec.rb index 7058c883af2..39958bf69fb 100644 --- a/spec/lib/action_account_spec.rb +++ b/spec/lib/action_account_spec.rb @@ -238,10 +238,12 @@ let(:analytics) { FakeAnalytics.new } let(:attempts_api_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } + let(:fraud_ops_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } before do allow(Analytics).to receive(:new).and_return(analytics) allow(AttemptsApi::Tracker).to receive(:new).and_return(attempts_api_tracker) + allow(FraudOps::Tracker).to receive(:new).and_return(fraud_ops_tracker) end let(:args) { [user.uuid, user_without_profile.uuid, 'uuid-does-not-exist'] } diff --git a/spec/services/idv/phone_step_spec.rb b/spec/services/idv/phone_step_spec.rb index 26fd9783151..a38300dc2de 100644 --- a/spec/services/idv/phone_step_spec.rb +++ b/spec/services/idv/phone_step_spec.rb @@ -37,6 +37,7 @@ let(:trace_id) { SecureRandom.uuid } let(:analytics) { FakeAnalytics.new } let(:attempts_api_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } + let(:fraud_ops_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } subject do described_class.new( @@ -44,6 +45,7 @@ trace_id:, analytics:, attempts_api_tracker:, + fraud_ops_tracker:, ) end From 0ca1a40fd5dfacd47eed842f9b1feecfa8143c2b Mon Sep 17 00:00:00 2001 From: Aaron Nagucki Date: Thu, 18 Sep 2025 14:55:47 -0500 Subject: [PATCH 4/8] update specs --- config/application.yml.default | 1 - spec/forms/gpo_verify_form_spec.rb | 2 ++ spec/services/gpo_reminder_sender_spec.rb | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/config/application.yml.default b/config/application.yml.default index 1a6c4aeec45..12743ab3650 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -168,7 +168,6 @@ event_disavowal_expiration_hours: 240 facial_match_general_availability_enabled: true feature_idv_force_gpo_verification_enabled: false feature_idv_hybrid_flow_enabled: true -fraud_ops_encryption_key: '' fraud_ops_event_ttl_seconds: 604800 fraud_ops_public_key: '' fraud_ops_s3_bucket: '' diff --git a/spec/forms/gpo_verify_form_spec.rb b/spec/forms/gpo_verify_form_spec.rb index ac90ea53bee..b1aa441016b 100644 --- a/spec/forms/gpo_verify_form_spec.rb +++ b/spec/forms/gpo_verify_form_spec.rb @@ -4,6 +4,7 @@ subject(:form) do GpoVerifyForm.new( attempts_api_tracker:, + fraud_ops_tracker:, user:, pii: applicant, resolved_authn_context_result: Vot::Parser::Result.no_sp_result, @@ -12,6 +13,7 @@ end let(:attempts_api_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } + let(:fraud_ops_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } let(:user) { create(:user, :fully_registered) } let(:applicant) { Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE } let(:entered_otp) { otp } diff --git a/spec/services/gpo_reminder_sender_spec.rb b/spec/services/gpo_reminder_sender_spec.rb index ac9a73e99c2..34be183f9b2 100644 --- a/spec/services/gpo_reminder_sender_spec.rb +++ b/spec/services/gpo_reminder_sender_spec.rb @@ -45,6 +45,7 @@ end let(:attempts_api_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } + let(:fraud_ops_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } let(:fake_analytics) { FakeAnalytics.new } let(:wait_for_reminder) { 14.days } let(:time_due_for_reminder) { Time.zone.now - wait_for_reminder } @@ -175,6 +176,7 @@ def set_reminder_sent_at(to_time) GpoVerifyForm.new( attempts_api_tracker:, + fraud_ops_tracker:, user:, pii: Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE, resolved_authn_context_result: Vot::Parser::Result.no_sp_result.with( From 50550a6ee7cb406bcff0c3a2759153106538cec5 Mon Sep 17 00:00:00 2001 From: Aaron Nagucki Date: Thu, 18 Sep 2025 15:14:44 -0500 Subject: [PATCH 5/8] last test --- spec/views/idv/by_mail/enter_code/index.html.erb_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/views/idv/by_mail/enter_code/index.html.erb_spec.rb b/spec/views/idv/by_mail/enter_code/index.html.erb_spec.rb index a6f8d614fb0..38f7d11c2d7 100644 --- a/spec/views/idv/by_mail/enter_code/index.html.erb_spec.rb +++ b/spec/views/idv/by_mail/enter_code/index.html.erb_spec.rb @@ -5,6 +5,7 @@ create(:user) end let(:attempts_api_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } + let(:fraud_ops_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } let(:pii) do {} @@ -21,6 +22,7 @@ @gpo_verify_form = GpoVerifyForm.new( attempts_api_tracker:, + fraud_ops_tracker:, user:, pii:, resolved_authn_context_result: Vot::Parser::Result.no_sp_result, From 58ba2f1ec1dbd1a8140426948200a49aa6ddab7e Mon Sep 17 00:00:00 2001 From: Aaron Nagucki Date: Thu, 25 Sep 2025 11:05:14 -0500 Subject: [PATCH 6/8] updates against attemtps_api needs --- app/controllers/application_controller.rb | 2 -- app/services/fraud_ops/tracker.rb | 6 +++--- config/application.yml.default | 1 + 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a6f924773cd..2e7abe0191f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -93,12 +93,10 @@ def attempts_api_tracker def fraud_ops_tracker @fraud_ops_tracker ||= FraudOps::Tracker.new( - session_id: attempts_api_session_id, request:, user: analytics_user, sp: current_sp, cookie_device_uuid: cookies[:device], - sp_redirect_uri: attempts_api_redirect_uri, ) end diff --git a/app/services/fraud_ops/tracker.rb b/app/services/fraud_ops/tracker.rb index b389a9a3bf9..5c8dcef46a3 100644 --- a/app/services/fraud_ops/tracker.rb +++ b/app/services/fraud_ops/tracker.rb @@ -2,15 +2,15 @@ module FraudOps class Tracker < AttemptsApi::Tracker - def initialize(session_id:, request:, user:, sp:, cookie_device_uuid:, sp_redirect_uri:) + def initialize(request:, user:, sp:, cookie_device_uuid:) super( - session_id: session_id, + session_id: nil, request: request, user: user, sp: sp, cookie_device_uuid: cookie_device_uuid, - sp_redirect_uri: sp_redirect_uri, enabled_for_session: true, + sp_redirect_uri: nil, ) end diff --git a/config/application.yml.default b/config/application.yml.default index 12743ab3650..d48f9ee3086 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -534,6 +534,7 @@ development: doc_auth_selfie_desktop_test_mode: true domain_name: localhost:3000 enable_rate_limiting: false + fraud_ops_public_key: 123abc hmac_fingerprinter_key: a2c813d4dca919340866ba58063e4072adc459b767a74cf2666d5c1eef3861db26708e7437abde1755eb24f4034386b0fea1850a1cb7e56bff8fae3cc6ade96c hmac_fingerprinter_key_queue: '["11111111111111111111111111111111", "22222222222222222222222222222222"]' identity_pki_local_dev: true From 7a5b203ba16de2ad1d8affe754261c9c3c043e76 Mon Sep 17 00:00:00 2001 From: Aaron Nagucki Date: Thu, 25 Sep 2025 13:52:14 -0500 Subject: [PATCH 7/8] update PR comments and attempts session --- app/jobs/get_usps_proofing_results_job.rb | 2 -- app/jobs/socure_docv_results_job.rb | 2 -- app/jobs/socure_image_retrieval_job.rb | 2 -- config/application.yml.default | 1 - lib/action_account.rb | 2 -- spec/services/fraud_ops/tracker_spec.rb | 4 ---- 6 files changed, 13 deletions(-) diff --git a/app/jobs/get_usps_proofing_results_job.rb b/app/jobs/get_usps_proofing_results_job.rb index f9c9b936863..1244f2e7fcd 100644 --- a/app/jobs/get_usps_proofing_results_job.rb +++ b/app/jobs/get_usps_proofing_results_job.rb @@ -523,12 +523,10 @@ def attempts_api_tracker(enrollment:) def fraud_ops_tracker(enrollment:) FraudOps::Tracker.new( - session_id: nil, request: nil, user: enrollment.user, sp: enrollment.service_provider, cookie_device_uuid: nil, - sp_redirect_uri: nil, ) end diff --git a/app/jobs/socure_docv_results_job.rb b/app/jobs/socure_docv_results_job.rb index a6ff4d6d5ba..b5fb7e7639a 100644 --- a/app/jobs/socure_docv_results_job.rb +++ b/app/jobs/socure_docv_results_job.rb @@ -198,12 +198,10 @@ def attempts_api_tracker def fraud_ops_tracker @fraud_ops_tracker ||= FraudOps::Tracker.new( - session_id: nil, request: nil, user: document_capture_session.user, sp:, cookie_device_uuid: nil, - sp_redirect_uri: nil, ) end diff --git a/app/jobs/socure_image_retrieval_job.rb b/app/jobs/socure_image_retrieval_job.rb index 18c4555ab39..02717f8db22 100644 --- a/app/jobs/socure_image_retrieval_job.rb +++ b/app/jobs/socure_image_retrieval_job.rb @@ -58,12 +58,10 @@ def attempts_api_tracker def fraud_ops_tracker @fraud_ops_tracker ||= FraudOps::Tracker.new( - session_id: nil, request: nil, user: document_capture_session.user, sp:, cookie_device_uuid: nil, - sp_redirect_uri: nil, ) end diff --git a/config/application.yml.default b/config/application.yml.default index d48f9ee3086..12743ab3650 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -534,7 +534,6 @@ development: doc_auth_selfie_desktop_test_mode: true domain_name: localhost:3000 enable_rate_limiting: false - fraud_ops_public_key: 123abc hmac_fingerprinter_key: a2c813d4dca919340866ba58063e4072adc459b767a74cf2666d5c1eef3861db26708e7437abde1755eb24f4034386b0fea1850a1cb7e56bff8fae3cc6ade96c hmac_fingerprinter_key_queue: '["11111111111111111111111111111111", "22222222222222222222222222222222"]' identity_pki_local_dev: true diff --git a/lib/action_account.rb b/lib/action_account.rb index a84d8f4ad7c..cf8bd9d9adb 100644 --- a/lib/action_account.rb +++ b/lib/action_account.rb @@ -405,12 +405,10 @@ def attempts_api_tracker(profile:) def fraud_ops_tracker(profile:) FraudOps::Tracker.new( - session_id: nil, request: nil, user: profile.user, sp: profile.initiating_service_provider, cookie_device_uuid: nil, - sp_redirect_uri: nil, ) end end diff --git a/spec/services/fraud_ops/tracker_spec.rb b/spec/services/fraud_ops/tracker_spec.rb index 282e79c5ffa..a28d542a5ce 100644 --- a/spec/services/fraud_ops/tracker_spec.rb +++ b/spec/services/fraud_ops/tracker_spec.rb @@ -17,12 +17,10 @@ subject(:tracker) do FraudOps::Tracker.new( - session_id: session_id, request: request, user: user, sp: sp, cookie_device_uuid: cookie_device_uuid, - sp_redirect_uri: sp_redirect_uri, ) end @@ -48,12 +46,10 @@ allow(redis_wrapper).to receive(:write_event) new_tracker = FraudOps::Tracker.new( - session_id: SecureRandom.hex(16), request: request, user: user, sp: sp, cookie_device_uuid: cookie_device_uuid, - sp_redirect_uri: sp_redirect_uri, ) new_tracker.login_email_and_password_auth(success: true) From dcf73c80fc91c979d9de1f599c4cc65ec9fe86a8 Mon Sep 17 00:00:00 2001 From: Aaron Nagucki Date: Thu, 25 Sep 2025 14:02:53 -0500 Subject: [PATCH 8/8] update lint --- app/controllers/sign_up/passwords_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/sign_up/passwords_controller.rb b/app/controllers/sign_up/passwords_controller.rb index 1c01bf5f664..05c3b571120 100644 --- a/app/controllers/sign_up/passwords_controller.rb +++ b/app/controllers/sign_up/passwords_controller.rb @@ -39,9 +39,10 @@ def forbidden_passwords def track_analytics(result) analytics.password_creation(**result) + failure_reason = attempts_api_tracker.parse_failure_reason(result) attempts_api_tracker.user_registration_password_submitted( success: result.success?, - failure_reason: attempts_api_tracker.parse_failure_reason(result), + failure_reason:, ) end