Skip to content

Commit

Permalink
LG-13963: 👤 Socure shadow mode background job (#11139)
Browse files Browse the repository at this point in the history
* Add Socure configs to IdentityConfig

* Add failure_message_when_negated to HaveLoggedEventMatcher

* 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

* Make ResolutionProofingJob schedule Socure KYC call

- When flag is enabled, invoke Socure KYC as well

* Add verified_attributes to resolution result to_h

* Add more detail to resolution proofer logging test

* 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.

* Tweak socure default base URL for dev

* Remove pointless user_id arg to analytics event

* Update app/jobs/socure_shadow_mode_proofing_job.rb

Co-authored-by: Zach Margolis <[email protected]>

* Clarify comment in spec

* Clarify spec name

* Clarify spec name

* Use user.first_email

* Revert "Use user.first_email"

This reverts commit fd4dcab.

* Remove pointless service_provider_issuer let

---------

Co-authored-by: Zach Margolis <[email protected]>
  • Loading branch information
matthinz and zachmargolis authored Aug 30, 2024
1 parent 524983b commit a52223a
Show file tree
Hide file tree
Showing 11 changed files with 630 additions and 6 deletions.
16 changes: 15 additions & 1 deletion app/jobs/resolution_proofing_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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',
Expand Down
117 changes: 117 additions & 0 deletions app/jobs/socure_shadow_mode_proofing_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# 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
return
end

applicant = build_applicant(encrypted_arguments:, user_email:)

socure_result = proofer.proof(applicant)

analytics.idv_socure_shadow_mode_proofing_result(
resolution_result: format_proofing_result_for_logs(proofing_result),
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

def create_analytics(
user:,
service_provider_issuer:
)
Analytics.new(
user:,
request: nil,
sp: service_provider_issuer,
session: {},
)
end

def format_proofing_result_for_logs(proofing_result)
proofing_result.to_h.tap do |hash|
hash.dig(:context, :stages, :threatmetrix)&.delete(:response_body)
end
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
22 changes: 22 additions & 0 deletions app/services/analytics_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4204,6 +4204,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
Expand Down
1 change: 1 addition & 0 deletions app/services/proofing/resolution/result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def to_h
attributes_requiring_additional_verification,
vendor_name: vendor_name,
vendor_workflow: vendor_workflow,
verified_attributes: verified_attributes,
}
end

Expand Down
7 changes: 5 additions & 2 deletions config/application.yml.default
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,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
Expand Down Expand Up @@ -338,6 +339,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_enabled: false
socure_webhook_secret_key: ''
socure_webhook_secret_key_queue: '[]'
Expand Down Expand Up @@ -432,8 +436,7 @@ development:
sign_in_recaptcha_percent_tested: 100
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'
state_tracking_enabled: true
telephony_adapter: test
use_dashboard_service_providers: true
Expand Down
4 changes: 4 additions & 0 deletions lib/identity_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,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)
Expand Down Expand Up @@ -360,6 +361,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)
Expand Down
9 changes: 6 additions & 3 deletions spec/features/idv/analytics_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,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
Expand All @@ -95,7 +96,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,
},
Expand Down Expand Up @@ -124,7 +126,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,
},
Expand Down
62 changes: 62 additions & 0 deletions spec/jobs/resolution_proofing_job_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit a52223a

Please sign in to comment.