From 206a4683f244c3575410f7dfcd02f467aa4267bb Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Tue, 20 Aug 2024 10:48:50 -0700 Subject: [PATCH 01/16] Add Socure configs to IdentityConfig --- config/application.yml.default | 6 ++++-- lib/identity_config.rb | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/config/application.yml.default b/config/application.yml.default index 017ef81a2d7..8e5fb9732e1 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -335,6 +335,9 @@ sign_in_user_id_per_ip_attempt_window_exponential_factor: 1.1 sign_in_user_id_per_ip_attempt_window_in_minutes: 720 sign_in_user_id_per_ip_attempt_window_max_minutes: 43_200 sign_in_user_id_per_ip_max_attempts: 50 +socure_idplus_api_key: '' +socure_idplus_base_url: '' +socure_idplus_timeout_in_seconds: 5 socure_webhook_secret_key: '' socure_webhook_secret_key_queue: '[]' sp_handoff_bounce_max_seconds: 2 @@ -427,8 +430,7 @@ development: show_unsupported_passkey_platform_authentication_setup: true sign_in_recaptcha_score_threshold: 0.3 skip_encryption_allowed_list: '["urn:gov:gsa:SAML:2.0.profiles:sp:sso:localhost"]' - socure_webhook_secret_key: 'secret-key' - socure_webhook_secret_key_queue: '["old-key-one", "old-key-two"]' + socure_idplus_base_url: 'https://sandbox.socure.us/api/3.0/EmailAuthScore' state_tracking_enabled: true telephony_adapter: test use_dashboard_service_providers: true diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 51d032a7b1e..80271c11c5d 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -359,6 +359,9 @@ def self.store config.add(:s3_reports_enabled, type: :boolean) config.add(:saml_endpoint_configs, type: :json, options: { symbolize_names: true }) config.add(:saml_secret_rotation_enabled, type: :boolean) + config.add(:socure_idplus_api_key, type: :string) + config.add(:socure_idplus_base_url, type: :string) + config.add(:socure_idplus_timeout_in_seconds, type: :integer) config.add(:scrypt_cost, type: :string) config.add(:second_mfa_reminder_account_age_in_days, type: :integer) config.add(:second_mfa_reminder_sign_in_count, type: :integer) From dc29a66cc13235b937add2cd2bc5d3185b44de7e Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Tue, 20 Aug 2024 14:07:27 -0700 Subject: [PATCH 02/16] Add failure_message_when_negated to HaveLoggedEventMatcher --- spec/support/have_logged_event_matcher.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spec/support/have_logged_event_matcher.rb b/spec/support/have_logged_event_matcher.rb index 86d09a48164..52e06b2ca2b 100644 --- a/spec/support/have_logged_event_matcher.rb +++ b/spec/support/have_logged_event_matcher.rb @@ -22,6 +22,15 @@ def failure_message end end + def failure_message_when_negated + adjective = attributes_matcher_description(expected_attributes) ? 'matching ' : '' + [ + "Expected that FakeAnalytics would not have received #{adjective}event", + expected_event_name.inspect, + has_expected_count? ? count_failure_reason('it was received').strip : nil, + ].compact.join(' ') + end + def matches?(actual) @actual = actual From 8bf0ec482cf1ebb55c5b6601cb8fb934a35a6616 Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Tue, 20 Aug 2024 15:55:29 -0700 Subject: [PATCH 03/16] Add SocureShadowModeProofingJob Add job to make requests to Socure's KYC API and log the results alongside the original resolution proofing result. changelog: Upcoming Features, Identity verification, Add background job for Socure KYC proofing --- app/jobs/socure_shadow_mode_proofing_job.rb | 109 ++++++++ app/services/analytics_events.rb | 22 ++ .../socure_shadow_mode_proofing_job_spec.rb | 259 ++++++++++++++++++ 3 files changed, 390 insertions(+) create mode 100644 app/jobs/socure_shadow_mode_proofing_job.rb create mode 100644 spec/jobs/socure_shadow_mode_proofing_job_spec.rb diff --git a/app/jobs/socure_shadow_mode_proofing_job.rb b/app/jobs/socure_shadow_mode_proofing_job.rb new file mode 100644 index 00000000000..7c83a438c0e --- /dev/null +++ b/app/jobs/socure_shadow_mode_proofing_job.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +class SocureShadowModeProofingJob < ApplicationJob + include JobHelpers::StaleJobHelper + + queue_as :low + + discard_on JobHelpers::StaleJobHelper::StaleJobError + + # @param [String] document_capture_session_result_id + # @param [String] encrypted_arguments + # @param [String,nil] service_provider_issuer + # @param [String] user_email + # @param [String] user_uuid + def perform( + document_capture_session_result_id:, + encrypted_arguments:, + service_provider_issuer:, + user_email:, + user_uuid: + ) + raise_stale_job! if stale_job?(enqueued_at) + + user = User.find_by(uuid: user_uuid) + raise "User not found: #{user_uuid}" if !user + + analytics = create_analytics( + user:, + service_provider_issuer:, + ) + + proofing_result = load_proofing_result(document_capture_session_result_id:) + if !proofing_result + analytics.idv_socure_shadow_mode_proofing_result_missing( + # NOTE: user_id in Analytics parlance is *actually* the uuid. Passing it + # here will make sure that properties.user_id is set correctly on + # the logged event + user_id: user.uuid, + ) + return + end + + applicant = build_applicant(encrypted_arguments:, user_email:) + + socure_result = proofer.proof(applicant) + + analytics.idv_socure_shadow_mode_proofing_result( + resolution_result: proofing_result.to_h, + socure_result: socure_result.to_h, + user_id: user.uuid, + ) + end + + def create_analytics( + user:, + service_provider_issuer: + ) + Analytics.new( + user:, + request: nil, + sp: service_provider_issuer, + session: {}, + ) + end + + def load_proofing_result(document_capture_session_result_id:) + DocumentCaptureSession.new( + result_id: document_capture_session_result_id, + ).load_proofing_result&.result + end + + def build_applicant( + encrypted_arguments:, + user_email: + ) + decrypted_arguments = JSON.parse( + Encryption::Encryptors::BackgroundProofingArgEncryptor.new.decrypt(encrypted_arguments), + symbolize_names: true, + ) + + applicant_pii = decrypted_arguments[:applicant_pii] + + { + **applicant_pii.slice( + :first_name, + :last_name, + :address1, + :address2, + :city, + :state, + :zipcode, + :phone, + :dob, + :ssn, + ), + email: user_email, + } + end + + def proofer + @proofer ||= Proofing::Socure::IdPlus::Proofer.new( + Proofing::Socure::IdPlus::Config.new( + api_key: IdentityConfig.store.socure_idplus_api_key, + base_url: IdentityConfig.store.socure_idplus_base_url, + timeout: IdentityConfig.store.socure_idplus_timeout_in_seconds, + ), + ) + end +end diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index a5ad1b763cb..acb2a4c67b9 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -4197,6 +4197,28 @@ def idv_session_error_visited( ) end + # Logs a Socure KYC result alongside a resolution result for later comparison. + # @param [Hash] socure_result Result from Socure KYC API call + # @param [Hash] resolution_result Result from resolution proofing + def idv_socure_shadow_mode_proofing_result( + socure_result:, + resolution_result:, + **extra + ) + track_event( + :idv_socure_shadow_mode_proofing_result, + resolution_result: resolution_result.to_h, + socure_result: socure_result.to_h, + **extra, + ) + end + + # Indicates that no proofing result was found when SocureShadowModeProofingJob + # attempted to look for one. + def idv_socure_shadow_mode_proofing_result_missing(**extra) + track_event(:idv_socure_shadow_mode_proofing_result_missing, **extra) + end + # @param [String] step # @param [String] location # @param [Hash,nil] proofing_components User's current proofing components diff --git a/spec/jobs/socure_shadow_mode_proofing_job_spec.rb b/spec/jobs/socure_shadow_mode_proofing_job_spec.rb new file mode 100644 index 00000000000..86793b86c2d --- /dev/null +++ b/spec/jobs/socure_shadow_mode_proofing_job_spec.rb @@ -0,0 +1,259 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe SocureShadowModeProofingJob do + let(:job) do + described_class.new + end + + let(:document_capture_session) do + DocumentCaptureSession.create(user:).tap do |dcs| + dcs.create_proofing_session + end + end + + let(:document_capture_session_result_id) do + document_capture_session.result_id + end + + let(:applicant_pii) do + Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE + end + + let(:encrypted_arguments) do + Encryption::Encryptors::BackgroundProofingArgEncryptor.new.encrypt( + JSON.generate({ applicant_pii: }), + ) + end + + let(:service_provider_issuer) {} + + let(:user) { create(:user) } + + let(:user_uuid) { user.uuid } + + let(:user_email) { user.email } + + let(:proofing_result) do + FormResponse.new( + success: true, + errors: {}, + extra: {}, + ) + end + + let(:socure_idplus_base_url) { 'https://example.org' } + + before do + document_capture_session.store_proofing_result(proofing_result) + + allow(IdentityConfig.store).to receive(:socure_idplus_base_url). + and_return(socure_idplus_base_url) + end + + describe '#perform' do + subject(:perform) do + allow(job).to receive(:create_analytics).and_return(analytics) + + job.perform( + document_capture_session_result_id:, + encrypted_arguments:, + service_provider_issuer:, + user_email:, + user_uuid:, + ) + end + + let(:analytics) do + FakeAnalytics.new + end + + let(:socure_response_body) do + { + referenceId: 'a1234b56-e789-0123-4fga-56b7c890d123', + kyc: { + reasonCodes: [ + 'I919', + 'I914', + 'I905', + ], + fieldValidations: { + firstName: 0.99, + surName: 0.99, + streetAddress: 0.99, + city: 0.99, + state: 0.99, + zip: 0.99, + mobileNumber: 0.99, + dob: 0.99, + ssn: 0.99, + }, + }, + customerProfile: { + customerUserId: '129', + userId: 'u8JpWn4QsF3R7tA2', + }, + } + end + + before do + stub_request(:post, 'https://example.org/api/3.0/EmailAuthScore'). + to_return( + headers: { + 'Content-Type' => 'application/json', + }, + body: JSON.generate(socure_response_body), + ) + end + + context 'when document_capture_session_result_id is valid' do + it 'makes a proofing call' do + expect(job.proofer).to receive(:proof).and_call_original + perform + end + + it 'does not log an idv_socure_shadow_mode_proofing_result_missing event' do + perform + expect(analytics).not_to have_logged_event(:idv_socure_shadow_mode_proofing_result_missing) + end + + it 'logs an event' do + perform + expect(analytics).to have_logged_event( + :idv_socure_shadow_mode_proofing_result, + user_id: user.uuid, + resolution_result: { + errors: {}, + extra: {}, + serialize_error_details_only: false, + success: true, + }, + socure_result: { + attributes_requiring_additional_verification: [], + can_pass_with_additional_verification: false, + errors: { reason_codes: ['I905', 'I914', 'I919'] }, + exception: nil, + reference: '', + success: true, + timed_out: false, + transaction_id: 'a1234b56-e789-0123-4fga-56b7c890d123', + vendor_name: 'socure_kyc', + vendor_workflow: nil, + }, + ) + end + + context 'when socure proofer raises an error' do + before do + allow(job.proofer).to receive(:proof).and_raise + end + + it 'does not squash the error' do + # The Proofer converts errors raised during requests into results + # with the `exception` property set. Any other error + expect { perform }.to raise_error + end + end + end + + context 'when document_capture_session_result_id is not valid' do + let(:document_capture_session_result_id) { 'some-id-that-is-not-valid' } + + it 'logs an idv_socure_shadow_mode_proofing_result_missing event' do + perform + + expect(analytics).to have_logged_event( + :idv_socure_shadow_mode_proofing_result_missing, + user_id: user.uuid, + ) + end + end + + context 'when job is stale' do + before do + allow(job).to receive(:stale_job?).and_return(true) + end + it 'raises StaleJobError' do + expect { perform }.to raise_error(JobHelpers::StaleJobHelper::StaleJobError) + end + end + + context 'when user is not found' do + let(:user_uuid) { 'some-user-id-that-does-not-exist' } + it 'raises an error' do + expect do + perform + end.to raise_error(RuntimeError, 'User not found: some-user-id-that-does-not-exist') + end + end + + context 'when encrypted_arguments cannot be decrypted' do + let(:encrypted_arguments) { 'bG9sIHRoaXMgaXMgbm90IGV2ZW4gZW5jcnlwdGVk' } + it 'raises an error' do + expect { perform }.to raise_error(Encryption::EncryptionError) + end + end + + context 'when encrypted_arguments contains invalid JSON' do + let(:encrypted_arguments) do + Encryption::Encryptors::BackgroundProofingArgEncryptor.new.encrypt( + 'this is not valid JSON', + ) + end + it 'raises an error' do + expect { perform }.to raise_error(JSON::ParserError) + end + end + end + + describe '#build_applicant' do + subject(:build_applicant) do + job.build_applicant(encrypted_arguments:, user_email:) + end + + it 'builds an applicant structure that looks right' do + expect(build_applicant).to eql( + { + first_name: 'FAKEY', + last_name: 'MCFAKERSON', + address1: '1 FAKE RD', + address2: nil, + city: 'GREAT FALLS', + state: 'MT', + zipcode: '59010-1234', + phone: '12025551212', + dob: '1938-10-06', + ssn: '900-66-1234', + email: user.email, + }, + ) + end + end + + describe '#create_analytics' do + it 'creates an Analytics instance with user and sp configured' do + analytics = job.create_analytics( + user:, + service_provider_issuer:, + ) + expect(analytics.sp).to eql(service_provider_issuer) + expect(analytics.user).to eql(user) + end + end + + describe '#proofer' do + let(:service_provider_issuer) { 'some-issuer' } + it 'returns a configured proofer' do + allow(IdentityConfig.store).to receive(:socure_idplus_api_key).and_return('an-api-key') + allow(IdentityConfig.store).to receive(:socure_idplus_base_url).and_return('https://example.org') + allow(IdentityConfig.store).to receive(:socure_idplus_timeout_in_seconds).and_return(6) + + expect(job.proofer.config.to_h).to eql( + api_key: 'an-api-key', + base_url: 'https://example.org', + timeout: 6, + ) + end + end +end From 8ebc93a66628da94f9db99d9333fe35d6e1cb3a0 Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Wed, 21 Aug 2024 14:59:43 -0700 Subject: [PATCH 04/16] Make ResolutionProofingJob schedule Socure KYC call - When flag is enabled, invoke Socure KYC as well --- app/jobs/resolution_proofing_job.rb | 16 +++++- config/application.yml.default | 1 + lib/identity_config.rb | 1 + spec/jobs/resolution_proofing_job_spec.rb | 62 +++++++++++++++++++++++ 4 files changed, 79 insertions(+), 1 deletion(-) diff --git a/app/jobs/resolution_proofing_job.rb b/app/jobs/resolution_proofing_job.rb index a0959d4f7bc..44401a8914e 100644 --- a/app/jobs/resolution_proofing_job.rb +++ b/app/jobs/resolution_proofing_job.rb @@ -73,6 +73,16 @@ def perform( device_profiling_success: callback_log_data&.device_profiling_success, timing: timer.results, ) + + if IdentityConfig.store.idv_socure_shadow_mode_enabled + SocureShadowModeProofingJob.perform_later( + document_capture_session_result_id: document_capture_session.result_id, + encrypted_arguments:, + service_provider_issuer:, + user_email: user_email_for_proofing(user), + user_uuid: user.uuid, + ) + end end private @@ -89,7 +99,7 @@ def make_vendor_proofing_requests( ) result = progressive_proofer.proof( applicant_pii: applicant_pii, - user_email: user.confirmed_email_addresses.first.email, + user_email: user_email_for_proofing(user), threatmetrix_session_id: threatmetrix_session_id, request_ip: request_ip, ipp_enrollment_in_progress: ipp_enrollment_in_progress, @@ -109,6 +119,10 @@ def make_vendor_proofing_requests( ) end + def user_email_for_proofing(user) + user.confirmed_email_addresses.first.email + end + def log_threatmetrix_info(threatmetrix_result, user) logger_info_hash( name: 'ThreatMetrix', diff --git a/config/application.yml.default b/config/application.yml.default index 8e5fb9732e1..a9eedc31ed7 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -141,6 +141,7 @@ idv_max_attempts: 5 idv_min_age_years: 13 idv_send_link_attempt_window_in_minutes: 10 idv_send_link_max_attempts: 5 +idv_socure_shadow_mode_enabled: false idv_sp_required: false in_person_completion_survey_url: 'https://login.gov' in_person_doc_auth_button_enabled: true diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 80271c11c5d..4341be0566b 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -164,6 +164,7 @@ def self.store config.add(:idv_min_age_years, type: :integer) config.add(:idv_send_link_attempt_window_in_minutes, type: :integer) config.add(:idv_send_link_max_attempts, type: :integer) + config.add(:idv_socure_shadow_mode_enabled, type: :boolean) config.add(:idv_sp_required, type: :boolean) config.add(:in_person_completion_survey_url, type: :string) config.add(:in_person_doc_auth_button_enabled, type: :boolean) diff --git a/spec/jobs/resolution_proofing_job_spec.rb b/spec/jobs/resolution_proofing_job_spec.rb index d4c2db323c2..bb5a31a8779 100644 --- a/spec/jobs/resolution_proofing_job_spec.rb +++ b/spec/jobs/resolution_proofing_job_spec.rb @@ -525,6 +525,68 @@ end end + context 'socure shadow mode' do + context 'turned on' do + before do + allow(IdentityConfig.store).to receive(:idv_socure_shadow_mode_enabled).and_return(true) + end + + it 'schedules a SocureShadowModeProofingJob' do + stub_vendor_requests + expect(SocureShadowModeProofingJob).to receive(:perform_later).with( + user_email: user.email, + user_uuid: user.uuid, + document_capture_session_result_id: document_capture_session.result_id, + encrypted_arguments: satisfy do |ciphertext| + json = JSON.parse( + Encryption::Encryptors::BackgroundProofingArgEncryptor.new.decrypt(ciphertext), + symbolize_names: true, + ) + expect(json[:applicant_pii]).to eql( + { + first_name: 'FAKEY', + middle_name: nil, + last_name: 'MCFAKERSON', + address1: '1 FAKE RD', + identity_doc_address1: '1 FAKE RD', + identity_doc_address2: nil, + identity_doc_city: 'GREAT FALLS', + identity_doc_address_state: 'MT', + identity_doc_zipcode: '59010-1234', + issuing_country_code: 'US', + address2: nil, + same_address_as_id: 'true', + city: 'GREAT FALLS', + state: 'MT', + zipcode: '59010-1234', + dob: '1938-10-06', + ssn: '900-66-1234', + state_id_jurisdiction: 'ND', + state_id_expiration: '2099-12-31', + state_id_issued: '2019-12-31', + state_id_number: '1111111111111', + state_id_type: 'drivers_license', + }, + ) + end, + service_provider_issuer: service_provider.issuer, + ) + + perform + end + end + + context 'turned off' do + it 'does not schedule a SocureShadowModeProofingJob' do + stub_vendor_requests + + expect(SocureShadowModeProofingJob).not_to receive(:perform_later) + + perform + end + end + end + it 'determines the UUID and UUID prefix and passes it to the downstream proofing vendors' do uuid_info = { uuid_prefix: service_provider.app_id, From 05697adc49659d1c8a57368f3bd2b480b4671bd9 Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Thu, 22 Aug 2024 09:53:09 -0700 Subject: [PATCH 05/16] Add verified_attributes to resolution result to_h --- app/services/proofing/resolution/result.rb | 1 + spec/features/idv/analytics_spec.rb | 9 ++++++--- spec/jobs/socure_shadow_mode_proofing_job_spec.rb | 1 + .../proofing/mock/resolution_mock_client_spec.rb | 8 ++++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/services/proofing/resolution/result.rb b/app/services/proofing/resolution/result.rb index f8d07ea6398..0978ce995bb 100644 --- a/app/services/proofing/resolution/result.rb +++ b/app/services/proofing/resolution/result.rb @@ -61,6 +61,7 @@ def to_h attributes_requiring_additional_verification, vendor_name: vendor_name, vendor_workflow: vendor_workflow, + verified_attributes: verified_attributes, } end diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index afc18f7c86b..fafb3005ea4 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -70,7 +70,8 @@ can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', - vendor_workflow: nil } + vendor_workflow: nil, + verified_attributes: nil } end let(:base_proofing_results) do @@ -94,7 +95,8 @@ timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired', - vendor_workflow: nil }, + vendor_workflow: nil, + verified_attributes: nil }, state_id: state_id_resolution, threatmetrix: threatmetrix_response, }, @@ -123,7 +125,8 @@ can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', - vendor_workflow: nil }, + vendor_workflow: nil, + verified_attributes: nil }, state_id: state_id_resolution, threatmetrix: threatmetrix_response, }, diff --git a/spec/jobs/socure_shadow_mode_proofing_job_spec.rb b/spec/jobs/socure_shadow_mode_proofing_job_spec.rb index 86793b86c2d..6c82dfd643d 100644 --- a/spec/jobs/socure_shadow_mode_proofing_job_spec.rb +++ b/spec/jobs/socure_shadow_mode_proofing_job_spec.rb @@ -140,6 +140,7 @@ transaction_id: 'a1234b56-e789-0123-4fga-56b7c890d123', vendor_name: 'socure_kyc', vendor_workflow: nil, + verified_attributes: %i[address first_name last_name phone ssn dob].to_set, }, ) end diff --git a/spec/services/proofing/mock/resolution_mock_client_spec.rb b/spec/services/proofing/mock/resolution_mock_client_spec.rb index 5c8b2f9933f..61f23ddb71c 100644 --- a/spec/services/proofing/mock/resolution_mock_client_spec.rb +++ b/spec/services/proofing/mock/resolution_mock_client_spec.rb @@ -28,6 +28,7 @@ can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_workflow: nil, + verified_attributes: nil, ) end end @@ -51,6 +52,7 @@ can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_workflow: nil, + verified_attributes: nil, ) end end @@ -74,6 +76,7 @@ can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_workflow: nil, + verified_attributes: nil, ) end end @@ -97,6 +100,7 @@ can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_workflow: nil, + verified_attributes: nil, ) end end @@ -120,6 +124,7 @@ can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_workflow: nil, + verified_attributes: nil, ) end end @@ -143,6 +148,7 @@ can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_workflow: nil, + verified_attributes: nil, ) end end @@ -166,6 +172,7 @@ can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_workflow: nil, + verified_attributes: nil, ) end end @@ -191,6 +198,7 @@ can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_workflow: nil, + verified_attributes: nil, ) end end From 57555519fabd317ba9432a68da1f7d9199b05951 Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Thu, 22 Aug 2024 12:05:31 -0700 Subject: [PATCH 06/16] Add more detail to resolution proofer logging test --- app/jobs/socure_shadow_mode_proofing_job.rb | 7 + .../socure_shadow_mode_proofing_job_spec.rb | 145 +++++++++++++++++- 2 files changed, 147 insertions(+), 5 deletions(-) diff --git a/app/jobs/socure_shadow_mode_proofing_job.rb b/app/jobs/socure_shadow_mode_proofing_job.rb index 7c83a438c0e..25e47aa2ff6 100644 --- a/app/jobs/socure_shadow_mode_proofing_job.rb +++ b/app/jobs/socure_shadow_mode_proofing_job.rb @@ -48,6 +48,13 @@ def perform( resolution_result: proofing_result.to_h, socure_result: socure_result.to_h, user_id: user.uuid, + pii_like_keypaths: [ + [:errors, :ssn], + [:resolution_result, :context, :stages, :resolution, :errors, :ssn], + [:resolution_result, :context, :stages, :residential_address, :errors, :ssn], + [:resolution_result, :context, :stages, :threatmetrix, :response_body, :first_name], + [:resolution_result, :context, :stages, :state_id, :state_id_jurisdiction], + ], ) end diff --git a/spec/jobs/socure_shadow_mode_proofing_job_spec.rb b/spec/jobs/socure_shadow_mode_proofing_job_spec.rb index 6c82dfd643d..e87eb49185a 100644 --- a/spec/jobs/socure_shadow_mode_proofing_job_spec.rb +++ b/spec/jobs/socure_shadow_mode_proofing_job_spec.rb @@ -39,14 +39,83 @@ FormResponse.new( success: true, errors: {}, - extra: {}, + extra: { + exception: nil, + timed_out: false, + threatmetrix_review_status: 'pass', + context: { + device_profiling_adjudication_reason: 'device_profiling_result_pass', + resolution_adjudication_reason: 'pass_resolution_and_state_id', + should_proof_state_id: true, + stages: { + resolution: { + success: true, + errors: {}, + exception: nil, + timed_out: false, + transaction_id: 'resolution-mock-transaction-id-123', + reference: 'aaa-bbb-ccc', + can_pass_with_additional_verification: false, + attributes_requiring_additional_verification: [], + vendor_name: 'ResolutionMock', + vendor_workflow: nil, + verified_attributes: nil, + }, + residential_address: { + success: true, + errors: {}, + exception: nil, + timed_out: false, + transaction_id: '', + reference: '', + can_pass_with_additional_verification: false, + attributes_requiring_additional_verification: [], + vendor_name: 'ResidentialAddressNotRequired', + vendor_workflow: nil, + verified_attributes: nil, + }, + state_id: { + success: true, + errors: {}, + exception: nil, + mva_exception: nil, + requested_attributes: {}, + timed_out: false, + transaction_id: 'state-id-mock-transaction-id-456', + vendor_name: 'StateIdMock', + verified_attributes: [], + }, + threatmetrix: { + client: nil, + success: true, + errors: {}, + exception: nil, + timed_out: false, + transaction_id: 'ddp-mock-transaction-id-123', + review_status: 'pass', + response_body: { + "fraudpoint.score": '500', + request_id: '1234', + request_result: 'success', + review_status: 'pass', + risk_rating: 'trusted', + summary_risk_score: '-6', + tmx_risk_rating: 'neutral', + tmx_summary_reason_code: ['Identity_Negative_History'], + first_name: '[redacted]', + }, + }, + }, + }, + ssn_is_unique: true, + }, ) end let(:socure_idplus_base_url) { 'https://example.org' } before do - document_capture_session.store_proofing_result(proofing_result) + document_capture_session.store_proofing_result(proofing_result.to_h) allow(IdentityConfig.store).to receive(:socure_idplus_base_url). and_return(socure_idplus_base_url) @@ -124,10 +193,76 @@ :idv_socure_shadow_mode_proofing_result, user_id: user.uuid, resolution_result: { - errors: {}, - extra: {}, - serialize_error_details_only: false, success: true, + errors: {}, + context: { + device_profiling_adjudication_reason: 'device_profiling_result_pass', + resolution_adjudication_reason: 'pass_resolution_and_state_id', + should_proof_state_id: true, + stages: { + residential_address: { + attributes_requiring_additional_verification: [], + can_pass_with_additional_verification: false, + errors: {}, + exception: nil, + reference: '', + success: true, + timed_out: false, + transaction_id: '', + vendor_name: 'ResidentialAddressNotRequired', + vendor_workflow: nil, + verified_attributes: nil, + }, + resolution: { + attributes_requiring_additional_verification: [], + can_pass_with_additional_verification: false, + errors: {}, + exception: nil, + reference: 'aaa-bbb-ccc', + success: true, + timed_out: false, + transaction_id: 'resolution-mock-transaction-id-123', + vendor_name: 'ResolutionMock', + vendor_workflow: nil, + verified_attributes: nil, + }, + state_id: { + errors: {}, + exception: nil, + mva_exception: nil, + requested_attributes: {}, + success: true, + timed_out: false, + transaction_id: 'state-id-mock-transaction-id-456', + vendor_name: 'StateIdMock', + verified_attributes: [], + }, + threatmetrix: { + client: nil, + errors: {}, + exception: nil, + response_body: { + first_name: '[redacted]', + "fraudpoint.score": '500', + request_id: '1234', + request_result: 'success', + review_status: 'pass', + risk_rating: 'trusted', + summary_risk_score: '-6', + tmx_risk_rating: 'neutral', + tmx_summary_reason_code: ['Identity_Negative_History'], + }, + review_status: 'pass', + success: true, + timed_out: false, + transaction_id: 'ddp-mock-transaction-id-123', + }, + }, + }, + exception: nil, + ssn_is_unique: true, + threatmetrix_review_status: 'pass', + timed_out: false, }, socure_result: { attributes_requiring_additional_verification: [], From 46d1c975e0462a02ff7fa226a46429ed3972b55d Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Thu, 22 Aug 2024 12:21:01 -0700 Subject: [PATCH 07/16] Don't log TMX response body ong idv_socure_shadow_mode_proofing_result These are real big and mess with Cloudwatch's ability to parse fields out oflogs. --- app/jobs/socure_shadow_mode_proofing_job.rb | 11 ++++++++++- spec/jobs/socure_shadow_mode_proofing_job_spec.rb | 11 ----------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/app/jobs/socure_shadow_mode_proofing_job.rb b/app/jobs/socure_shadow_mode_proofing_job.rb index 25e47aa2ff6..45ce370308d 100644 --- a/app/jobs/socure_shadow_mode_proofing_job.rb +++ b/app/jobs/socure_shadow_mode_proofing_job.rb @@ -45,7 +45,7 @@ def perform( socure_result = proofer.proof(applicant) analytics.idv_socure_shadow_mode_proofing_result( - resolution_result: proofing_result.to_h, + resolution_result: format_proofing_result_for_logs(proofing_result), socure_result: socure_result.to_h, user_id: user.uuid, pii_like_keypaths: [ @@ -70,6 +70,15 @@ def create_analytics( ) end + def format_proofing_result_for_logs(proofing_result) + proofing_result.to_h.then do |hash| + hash[:context][:stages][:threatmetrix].delete(:response_body) + hash + rescue + hash + end + end + def load_proofing_result(document_capture_session_result_id:) DocumentCaptureSession.new( result_id: document_capture_session_result_id, diff --git a/spec/jobs/socure_shadow_mode_proofing_job_spec.rb b/spec/jobs/socure_shadow_mode_proofing_job_spec.rb index e87eb49185a..bcb5305088b 100644 --- a/spec/jobs/socure_shadow_mode_proofing_job_spec.rb +++ b/spec/jobs/socure_shadow_mode_proofing_job_spec.rb @@ -241,17 +241,6 @@ client: nil, errors: {}, exception: nil, - response_body: { - first_name: '[redacted]', - "fraudpoint.score": '500', - request_id: '1234', - request_result: 'success', - review_status: 'pass', - risk_rating: 'trusted', - summary_risk_score: '-6', - tmx_risk_rating: 'neutral', - tmx_summary_reason_code: ['Identity_Negative_History'], - }, review_status: 'pass', success: true, timed_out: false, From fab11b85f17716e30c586b86a1ea068285862124 Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Thu, 22 Aug 2024 12:38:49 -0700 Subject: [PATCH 08/16] Tweak socure default base URL for dev --- config/application.yml.default | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/application.yml.default b/config/application.yml.default index a9eedc31ed7..b4b01dc0980 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -431,7 +431,7 @@ development: show_unsupported_passkey_platform_authentication_setup: true sign_in_recaptcha_score_threshold: 0.3 skip_encryption_allowed_list: '["urn:gov:gsa:SAML:2.0.profiles:sp:sso:localhost"]' - socure_idplus_base_url: 'https://sandbox.socure.us/api/3.0/EmailAuthScore' + socure_idplus_base_url: 'https://sandbox.socure.us' state_tracking_enabled: true telephony_adapter: test use_dashboard_service_providers: true From dbaf5a81786ba7af119040552574d5180f8ae5d3 Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Mon, 26 Aug 2024 14:48:14 -0700 Subject: [PATCH 09/16] Remove pointless user_id arg to analytics event --- app/jobs/socure_shadow_mode_proofing_job.rb | 7 +------ spec/jobs/socure_shadow_mode_proofing_job_spec.rb | 1 - 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/app/jobs/socure_shadow_mode_proofing_job.rb b/app/jobs/socure_shadow_mode_proofing_job.rb index 45ce370308d..7c564ad1408 100644 --- a/app/jobs/socure_shadow_mode_proofing_job.rb +++ b/app/jobs/socure_shadow_mode_proofing_job.rb @@ -31,12 +31,7 @@ def perform( proofing_result = load_proofing_result(document_capture_session_result_id:) if !proofing_result - analytics.idv_socure_shadow_mode_proofing_result_missing( - # NOTE: user_id in Analytics parlance is *actually* the uuid. Passing it - # here will make sure that properties.user_id is set correctly on - # the logged event - user_id: user.uuid, - ) + analytics.idv_socure_shadow_mode_proofing_result_missing return end diff --git a/spec/jobs/socure_shadow_mode_proofing_job_spec.rb b/spec/jobs/socure_shadow_mode_proofing_job_spec.rb index bcb5305088b..0d35c65fabf 100644 --- a/spec/jobs/socure_shadow_mode_proofing_job_spec.rb +++ b/spec/jobs/socure_shadow_mode_proofing_job_spec.rb @@ -290,7 +290,6 @@ expect(analytics).to have_logged_event( :idv_socure_shadow_mode_proofing_result_missing, - user_id: user.uuid, ) end end From 181ca8008a95a9a20536450d5366a647317c2c14 Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Mon, 26 Aug 2024 14:47:21 -0700 Subject: [PATCH 10/16] Update app/jobs/socure_shadow_mode_proofing_job.rb Co-authored-by: Zach Margolis --- app/jobs/socure_shadow_mode_proofing_job.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/jobs/socure_shadow_mode_proofing_job.rb b/app/jobs/socure_shadow_mode_proofing_job.rb index 7c564ad1408..fc39a3280c2 100644 --- a/app/jobs/socure_shadow_mode_proofing_job.rb +++ b/app/jobs/socure_shadow_mode_proofing_job.rb @@ -66,11 +66,8 @@ def create_analytics( end def format_proofing_result_for_logs(proofing_result) - proofing_result.to_h.then do |hash| - hash[:context][:stages][:threatmetrix].delete(:response_body) - hash - rescue - hash + proofing_result.to_h.tap do |hash| + hash.dig(:context, :stages, :threatmetrix)&.delete(:response_body) end end From c4fbd4060d4a503996c1d9e392b9f3dc7d53f6cf Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Mon, 26 Aug 2024 20:13:29 -0700 Subject: [PATCH 11/16] Clarify comment in spec --- spec/jobs/socure_shadow_mode_proofing_job_spec.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/jobs/socure_shadow_mode_proofing_job_spec.rb b/spec/jobs/socure_shadow_mode_proofing_job_spec.rb index 0d35c65fabf..e14652de17c 100644 --- a/spec/jobs/socure_shadow_mode_proofing_job_spec.rb +++ b/spec/jobs/socure_shadow_mode_proofing_job_spec.rb @@ -275,8 +275,9 @@ end it 'does not squash the error' do - # The Proofer converts errors raised during requests into results - # with the `exception` property set. Any other error + # If the Proofer encounters an error while _making_ a request, that + # will be returned as a Result with the `exception` property set. + # Other errors will be raised as normal. expect { perform }.to raise_error end end From cb276e6a3c2519dec7896638ae4cd6315e474951 Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Mon, 26 Aug 2024 20:16:41 -0700 Subject: [PATCH 12/16] Clarify spec name --- spec/jobs/socure_shadow_mode_proofing_job_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/jobs/socure_shadow_mode_proofing_job_spec.rb b/spec/jobs/socure_shadow_mode_proofing_job_spec.rb index e14652de17c..87741d62317 100644 --- a/spec/jobs/socure_shadow_mode_proofing_job_spec.rb +++ b/spec/jobs/socure_shadow_mode_proofing_job_spec.rb @@ -283,7 +283,7 @@ end end - context 'when document_capture_session_result_id is not valid' do + context 'when document capture session result is not present in redis' do let(:document_capture_session_result_id) { 'some-id-that-is-not-valid' } it 'logs an idv_socure_shadow_mode_proofing_result_missing event' do From 91b17363b962b806d659e099c7a20816229252bd Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Mon, 26 Aug 2024 20:19:07 -0700 Subject: [PATCH 13/16] Clarify spec name --- spec/jobs/socure_shadow_mode_proofing_job_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/jobs/socure_shadow_mode_proofing_job_spec.rb b/spec/jobs/socure_shadow_mode_proofing_job_spec.rb index 87741d62317..3850ca75593 100644 --- a/spec/jobs/socure_shadow_mode_proofing_job_spec.rb +++ b/spec/jobs/socure_shadow_mode_proofing_job_spec.rb @@ -176,7 +176,7 @@ ) end - context 'when document_capture_session_result_id is valid' do + context 'when document capture session result is present in redis' do it 'makes a proofing call' do expect(job.proofer).to receive(:proof).and_call_original perform From fd4dcab045a0924d93a64d05313f8d3db7975bef Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Wed, 28 Aug 2024 09:10:19 -0700 Subject: [PATCH 14/16] Use user.first_email --- app/jobs/resolution_proofing_job.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/jobs/resolution_proofing_job.rb b/app/jobs/resolution_proofing_job.rb index 44401a8914e..74a756c7319 100644 --- a/app/jobs/resolution_proofing_job.rb +++ b/app/jobs/resolution_proofing_job.rb @@ -79,7 +79,7 @@ def perform( document_capture_session_result_id: document_capture_session.result_id, encrypted_arguments:, service_provider_issuer:, - user_email: user_email_for_proofing(user), + user_email: user.first_email, user_uuid: user.uuid, ) end @@ -99,7 +99,7 @@ def make_vendor_proofing_requests( ) result = progressive_proofer.proof( applicant_pii: applicant_pii, - user_email: user_email_for_proofing(user), + user_email: user.first_email, threatmetrix_session_id: threatmetrix_session_id, request_ip: request_ip, ipp_enrollment_in_progress: ipp_enrollment_in_progress, @@ -119,10 +119,6 @@ def make_vendor_proofing_requests( ) end - def user_email_for_proofing(user) - user.confirmed_email_addresses.first.email - end - def log_threatmetrix_info(threatmetrix_result, user) logger_info_hash( name: 'ThreatMetrix', From 87a34bf01898e0f84d1a9b480e354193294ac9ad Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Wed, 28 Aug 2024 09:20:07 -0700 Subject: [PATCH 15/16] Revert "Use user.first_email" This reverts commit fd4dcab045a0924d93a64d05313f8d3db7975bef. --- app/jobs/resolution_proofing_job.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/jobs/resolution_proofing_job.rb b/app/jobs/resolution_proofing_job.rb index 74a756c7319..44401a8914e 100644 --- a/app/jobs/resolution_proofing_job.rb +++ b/app/jobs/resolution_proofing_job.rb @@ -79,7 +79,7 @@ def perform( document_capture_session_result_id: document_capture_session.result_id, encrypted_arguments:, service_provider_issuer:, - user_email: user.first_email, + user_email: user_email_for_proofing(user), user_uuid: user.uuid, ) end @@ -99,7 +99,7 @@ def make_vendor_proofing_requests( ) result = progressive_proofer.proof( applicant_pii: applicant_pii, - user_email: user.first_email, + user_email: user_email_for_proofing(user), threatmetrix_session_id: threatmetrix_session_id, request_ip: request_ip, ipp_enrollment_in_progress: ipp_enrollment_in_progress, @@ -119,6 +119,10 @@ def make_vendor_proofing_requests( ) end + def user_email_for_proofing(user) + user.confirmed_email_addresses.first.email + end + def log_threatmetrix_info(threatmetrix_result, user) logger_info_hash( name: 'ThreatMetrix', From 6af08a838b88431fc584961e00992d53be538a81 Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Fri, 30 Aug 2024 15:14:34 -0700 Subject: [PATCH 16/16] Remove pointless service_provider_issuer let --- spec/jobs/socure_shadow_mode_proofing_job_spec.rb | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/spec/jobs/socure_shadow_mode_proofing_job_spec.rb b/spec/jobs/socure_shadow_mode_proofing_job_spec.rb index 3850ca75593..3353e0e427f 100644 --- a/spec/jobs/socure_shadow_mode_proofing_job_spec.rb +++ b/spec/jobs/socure_shadow_mode_proofing_job_spec.rb @@ -27,8 +27,6 @@ ) end - let(:service_provider_issuer) {} - let(:user) { create(:user) } let(:user_uuid) { user.uuid } @@ -128,7 +126,7 @@ job.perform( document_capture_session_result_id:, encrypted_arguments:, - service_provider_issuer:, + service_provider_issuer: nil, user_email:, user_uuid:, ) @@ -360,15 +358,14 @@ it 'creates an Analytics instance with user and sp configured' do analytics = job.create_analytics( user:, - service_provider_issuer:, + service_provider_issuer: 'some-issuer', ) - expect(analytics.sp).to eql(service_provider_issuer) + expect(analytics.sp).to eql('some-issuer') expect(analytics.user).to eql(user) end end describe '#proofer' do - let(:service_provider_issuer) { 'some-issuer' } it 'returns a configured proofer' do allow(IdentityConfig.store).to receive(:socure_idplus_api_key).and_return('an-api-key') allow(IdentityConfig.store).to receive(:socure_idplus_base_url).and_return('https://example.org')