Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions app/forms/idv/api_image_upload_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def submit

client_response = nil
doc_pii_response = nil
passport_response = nil

if form_response.success?
client_response = post_images_to_client
Expand All @@ -46,13 +47,19 @@ def submit

if client_response.success?
doc_pii_response = validate_pii_from_doc(client_response)

if doc_pii_response.success? &&
doc_pii_response.pii_from_doc[:state_id_type] == 'passport'
passport_response = validate_mrz(client_response)
end
end
end

response = determine_response(
form_response: form_response,
client_response: client_response,
doc_pii_response: doc_pii_response,
form_response:,
client_response:,
doc_pii_response:,
passport_response:,
)

failed_fingerprints = store_failed_images(client_response, doc_pii_response)
Expand Down Expand Up @@ -157,6 +164,22 @@ def validate_pii_from_doc(client_response)
response
end

def validate_mrz(client_response)
response = DocAuth::Dos::Requests::MrzRequest.new(mrz: client_response.pii_from_doc.mrz).fetch

analytics.idv_dos_passport_verification(
document_type:,
remaining_submit_attempts:,
submit_attempts:,
user_id: user_uuid,
response: response.extra[:response],
success: response.success?,
)

response.extra.merge!(extra_attributes)
response
end

def doc_side_classification(client_response)
side_info = {}.merge(client_response&.extra&.[](:classification_info) || {})
side_info.transform_keys(&:downcase).symbolize_keys
Expand Down Expand Up @@ -227,13 +250,15 @@ def processed_selfie_attempts_data
{ selfie_attempts: past_selfie_count + processed_selfie_count }
end

def determine_response(form_response:, client_response:, doc_pii_response:)
def determine_response(form_response:, client_response:, doc_pii_response:, passport_response:)
# image validation failed
return form_response unless form_response.success?

# doc_pii validation failed
return doc_pii_response if doc_pii_response.present? && !doc_pii_response.success?

return passport_response if passport_response.present? && !passport_response.success?

client_response
end

Expand Down
3 changes: 2 additions & 1 deletion app/forms/idv/doc_pii_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ class DocPiiForm
validate :dob_valid?
validate :state_id_or_passport

attr_reader :first_name, :last_name, :dob, :state_id_type, :attention_with_barcode
attr_reader :first_name, :last_name, :dob, :attention_with_barcode,
:jurisdiction, :state_id_number, :state_id_expiration, :state_id_type
alias_method :attention_with_barcode?, :attention_with_barcode

def initialize(pii:, attention_with_barcode: false)
Expand Down
29 changes: 29 additions & 0 deletions app/services/analytics_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2363,6 +2363,35 @@ def idv_doc_auth_welcome_visited(
)
end

# User's passport information submitted to DoS for validation
# @param [Boolean] success Whether the validation succeeded
# @param [String] response The raw verdict from DoS
# @param [Integer] submit_attempts Times that user has tried submitting document capture
# @param [Integer] remaining_submit_attempts how many attempts the user has left before
# we rate limit them.
# @param [String] user_id
# @param [String] document_type The document type (should always be 'Passport' here)
def idv_dos_passport_verification(
success:,
response:,
submit_attempts:,
remaining_submit_attempts:,
user_id:,
document_type:,
**extra
)
track_event(
:idv_dos_passport_verification,
success:,
response:,
submit_attempts:,
remaining_submit_attempts:,
user_id:,
document_type:,
**extra,
)
end

# User submitted IDV password confirm page
# @param [Boolean] success
# @param [Boolean] fraud_review_pending
Expand Down
1 change: 1 addition & 0 deletions app/services/doc_auth/dos/requests/mrz_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def handle_http_response(response)
vendor: 'DoS',
correlation_id_sent: correlation_id,
correlation_id_received: response.headers['X-Correlation-ID'],
response: result[:response],
}.compact
case result[:response]
when 'YES'
Expand Down
14 changes: 14 additions & 0 deletions lib/idp/constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,20 @@ module Vendors
same_address_as_id: 'true',
}.freeze

MOCK_IDV_APPLICANT_WITH_PASSPORT = MOCK_IDV_APPLICANT.select do |field, _value|
%i[first_name middle_name last_name dob sex].include?(field)
end.merge(
state_id_type: 'passport',
mrz:
'P<UTOSAMPLE<<COMPANY<<<<<<<<<<<<<<<<<<<<<<<<ACU1234P<5UTO0003067F4003065<<<<<<<<<<<<<<02',
birth_place: 'Birthplace',
passport_expiration: (DateTime.now.utc + 10.years).to_s,
issuing_country_code: 'USA',
passport_issued: (DateTime.new.utc - 1.year).to_s,
nationality_code: 'USA',
document_number: nil,
).freeze

MOCK_IPP_APPLICANT_SAME_ADDRESS_AS_ID_FALSE = MOCK_IPP_APPLICANT.merge(
same_address_as_id: 'false',
).freeze
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1044,4 +1044,4 @@
}
]
}


116 changes: 116 additions & 0 deletions spec/forms/idv/api_image_upload_form_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,122 @@
end
end

context 'Passport MRZ validation fails' do
let(:passport_pii_response) do
DocAuth::Response.new(
success: true,
errors: {},
extra: {},
pii_from_doc: Pii::Passport.new(**Idp::Constants::MOCK_IDV_APPLICANT_WITH_PASSPORT),
)
end

let(:failed_passport_mrz_response) do
DocAuth::Response.new(
success: false,
errors: { passport: 'invalid MRZ' },
extra: {
vendor: 'DoS',
correlation_id_sent: 'something',
correlation_id_received: 'something else',
response: 'NO',
},
)
end

let(:response) { form.submit }

before do
allow_any_instance_of(described_class)
.to receive(:post_images_to_client)
.and_return(passport_pii_response)

allow_any_instance_of(DocAuth::Dos::Requests::MrzRequest)
.to receive(:fetch)
.and_return(failed_passport_mrz_response)
end

it 'is not successful' do
expect(response.success?).to eq(false)
end

it 'includes remaining_submit_attempts' do
expect(response.extra[:remaining_submit_attempts]).to be_a_kind_of(Numeric)
end

it 'includes mrz errors' do
expect(response.errors).to eq({ passport: 'invalid MRZ' })
end

it 'logs the check event' do
response

expect(fake_analytics).to have_logged_event(
:idv_dos_passport_verification,
success: false,
response: 'NO',
submit_attempts: 1,
remaining_submit_attempts: 3,
user_id: document_capture_session.user.uuid,
document_type: document_type,
)
end
end

context 'Passport MRZ validation succeeds' do
let(:passport_pii_response) do
DocAuth::Response.new(
success: true,
errors: {},
extra: {},
pii_from_doc: Pii::Passport.new(**Idp::Constants::MOCK_IDV_APPLICANT_WITH_PASSPORT),
)
end

let(:successful_passport_mrz_response) do
DocAuth::Response.new(
success: true,
errors: {},
extra: {
vendor: 'DoS',
correlation_id_sent: 'something',
correlation_id_received: 'something else',
response: 'YES',
},
)
end

let(:response) { form.submit }

before do
allow_any_instance_of(described_class)
.to receive(:post_images_to_client)
.and_return(passport_pii_response)

allow_any_instance_of(DocAuth::Dos::Requests::MrzRequest)
.to receive(:fetch)
.and_return(successful_passport_mrz_response)
end

it 'is successful' do
expect(response.success?).to eq(true)
end

it 'logs the check event' do
response

expect(fake_analytics).to have_logged_event(
:idv_dos_passport_verification,
success: true,
response: 'YES',
submit_attempts: 1,
remaining_submit_attempts: 3,
user_id: document_capture_session.user.uuid,
document_type: document_type,
)
end
end

describe 'image source' do
let(:source) { nil }
let(:front_image_metadata) do
Expand Down