diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb index c942a596823..df7432436fb 100644 --- a/app/controllers/concerns/idv/verify_info_concern.rb +++ b/app/controllers/concerns/idv/verify_info_concern.rb @@ -212,10 +212,12 @@ def async_state_done(current_async_state) previous_ssn_edit_distance: previous_ssn_edit_distance, pii_like_keypaths: [ [:errors, :ssn], + [:errors, :state_id_jurisdiction], [:proofing_results, :context, :stages, :resolution, :errors, :ssn], [:proofing_results, :context, :stages, :residential_address, :errors, :ssn], [:proofing_results, :context, :stages, :threatmetrix, :response_body, :first_name], [:proofing_results, :context, :stages, :state_id, :state_id_jurisdiction], + [:proofing_results, :context, :stages, :state_id, :errors, :state_id_jurisdiction], [:proofing_results, :biographical_info, :identity_doc_address_state], [:proofing_results, :biographical_info, :state_id_jurisdiction], [:proofing_results, :biographical_info], diff --git a/app/controllers/idv/address_controller.rb b/app/controllers/idv/address_controller.rb index b8b1428fa03..b5902ef6045 100644 --- a/app/controllers/idv/address_controller.rb +++ b/app/controllers/idv/address_controller.rb @@ -42,11 +42,13 @@ def self.step_info def build_address_form Idv::AddressForm.new( - idv_session.updated_user_address || address_from_document, + idv_session.updated_user_address || address_from_document || null_address, ) end def address_from_document + return if idv_session.pii_from_doc.state_id_type == 'passport' + Pii::Address.new( address1: idv_session.pii_from_doc.address1, address2: idv_session.pii_from_doc.address2, @@ -56,6 +58,16 @@ def address_from_document ) end + def null_address + Pii::Address.new( + address1: nil, + address2: nil, + city: nil, + state: nil, + zipcode: nil, + ) + end + def success idv_session.address_edited = address_edited? idv_session.updated_user_address = @address_form.updated_user_address diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 087dab8728a..508c95fdc75 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -1969,6 +1969,7 @@ def idv_doc_auth_submitted_image_upload_form( # @option extra [Boolean] 'OrientationChanged' # @option extra [Boolean] 'PresentationChanged' # @param ["Passport","DriversLicense"] document_type Document capture user flow + # @param [Hash] passport_check_result The results of the Dos API call # The document capture image was uploaded to vendor during the IDV process def idv_doc_auth_submitted_image_upload_vendor( success:, @@ -2018,6 +2019,7 @@ def idv_doc_auth_submitted_image_upload_vendor( acuant_sdk_upgrade_ab_test_bucket: nil, liveness_enabled: nil, document_type: nil, + passport_check_result: nil, **extra ) track_event( @@ -2069,6 +2071,7 @@ def idv_doc_auth_submitted_image_upload_vendor( acuant_sdk_upgrade_ab_test_bucket:, liveness_enabled:, document_type:, + passport_check_result:, **extra, ) end diff --git a/app/services/doc_auth/mock/result_response.rb b/app/services/doc_auth/mock/result_response.rb index ed7dedb2062..4b8c64e2d5b 100644 --- a/app/services/doc_auth/mock/result_response.rb +++ b/app/services/doc_auth/mock/result_response.rb @@ -15,17 +15,18 @@ def initialize(uploaded_file, config, selfie_required = false) @selfie_required = selfie_required super( success: success?, - errors: errors, - pii_from_doc: pii_from_doc, + errors:, + pii_from_doc:, doc_type_supported: id_type_supported?, selfie_live: selfie_live?, selfie_quality_good: selfie_quality_good?, selfie_status: selfie_status, extra: { - doc_auth_result: doc_auth_result, - portrait_match_results: portrait_match_results, + doc_auth_result:, + passport_check_result:, + portrait_match_results:, billed: true, - classification_info: classification_info, + classification_info:, workflow: workflow, liveness_checking_required: @selfie_required, **@response_info.to_h, @@ -46,6 +47,7 @@ def errors passed = file_data.dig('passed_alerts') face_match_result = file_data.dig('portrait_match_results', 'FaceMatchResult') classification_info = file_data.dig('classification_info')&.symbolize_keys + passport_check_result = file_data.dig('passport_check_result', 'PassportCheckResult') # Pass and doc type is ok has_fields = [ doc_auth_result, @@ -54,6 +56,7 @@ def errors passed, face_match_result, classification_info, + passport_check_result, ].any?(&:present?) if has_fields @@ -70,6 +73,10 @@ def errors mock_args[:passed] = passed.map!(&:symbolize_keys) if passed.present? mock_args[:liveness_enabled] = face_match_result ? true : false mock_args[:classification_info] = classification_info if classification_info.present? + if passport_check_result.present? + mock_args[:passport_check_result] = + classification_info + end @response_info = create_response_info(**mock_args) ErrorGenerator.new(config).generate_doc_auth_errors(@response_info) elsif file_data.include?(:general) # general is the key for errors from parsing @@ -132,7 +139,15 @@ def parsed_alerts end def parsed_pii_from_doc - if parsed_data_from_uploaded_file.has_key?('document') + return if !parsed_data_from_uploaded_file.has_key?('document') + + if parsed_data_from_uploaded_file['document']['state_id_type'] == 'passport' + Pii::Passport.new( + **Idp::Constants::MOCK_IDV_APPLICANT.merge( + parsed_data_from_uploaded_file['document'].symbolize_keys, + ).slice(*Pii::Passport.members), + ) + else Pii::StateId.new( **Idp::Constants::MOCK_IDV_APPLICANT.merge( parsed_data_from_uploaded_file['document'].symbolize_keys, @@ -161,6 +176,12 @@ def portrait_match_results &.deep_symbolize_keys end + def passport_check_result + parsed_data_from_uploaded_file.dig('passport_check_result') + &.transform_keys! { |key| key.to_s.camelize } + &.deep_symbolize_keys + end + def classification_info info = parsed_data_from_uploaded_file&.[]('classification_info') || {} info.to_h.symbolize_keys @@ -214,13 +235,14 @@ def parse_uri }.freeze def create_response_info( - doc_auth_result: 'Failed', - passed: [], - failed: DEFAULT_FAILED_ALERTS, - liveness_enabled: false, - image_metrics: DEFAULT_IMAGE_METRICS, - classification_info: nil - ) + doc_auth_result: 'Failed', + passed: [], + failed: DEFAULT_FAILED_ALERTS, + liveness_enabled: false, + image_metrics: DEFAULT_IMAGE_METRICS, + classification_info: nil, + passport_check_result: nil + ) merged_image_metrics = DEFAULT_IMAGE_METRICS.deep_merge(image_metrics) { vendor: 'Mock', @@ -234,6 +256,7 @@ def create_response_info( liveness_enabled: liveness_enabled, classification_info: classification_info, portrait_match_results: selfie_check_performed? ? portrait_match_results : nil, + passport_check_result:, } end end diff --git a/app/services/proofing/mock/state_id_mock_client.rb b/app/services/proofing/mock/id_mock_client.rb similarity index 79% rename from app/services/proofing/mock/state_id_mock_client.rb rename to app/services/proofing/mock/id_mock_client.rb index 3b86f0bff68..377e369d24b 100644 --- a/app/services/proofing/mock/state_id_mock_client.rb +++ b/app/services/proofing/mock/id_mock_client.rb @@ -2,9 +2,9 @@ module Proofing module Mock - class StateIdMockClient + class IdMockClient SUPPORTED_STATE_ID_TYPES = %w[ - drivers_license drivers_permit state_id_card + drivers_license drivers_permit passport state_id_card ].to_set.freeze INVALID_STATE_ID_NUMBER = '00000000' @@ -15,12 +15,14 @@ def proof(applicant) return mva_timeout_result if mva_timeout?(applicant[:state_id_number]) errors = {} - if state_not_supported?(applicant[:state_id_jurisdiction]) + if jurisdiction_not_supported?(applicant) errors[:state_id_jurisdiction] = ['The jurisdiction could not be verified'] elsif invalid_state_id_number?(applicant[:state_id_number]) errors[:state_id_number] = ['The state ID number could not be verified'] elsif invalid_state_id_type?(applicant[:state_id_type]) errors[:state_id_type] = ['The state ID type could not be verified'] + elsif bad_mrz?(applicant) + errors[:mrz] = ['The passport MRZ could not be verified'] end return unverifiable_result(errors) if errors.any? @@ -63,7 +65,10 @@ def mva_timeout?(state_id_number) state_id_number.downcase == TRIGGER_MVA_TIMEOUT end - def state_not_supported?(state_id_jurisdiction) + def jurisdiction_not_supported?(applicant) + return false if applicant[:state_id_type] == 'passport' + + state_id_jurisdiction = applicant[:state_id_jurisdiction] !IdentityConfig.store.aamva_supported_jurisdictions.include? state_id_jurisdiction end @@ -75,6 +80,11 @@ def invalid_state_id_type?(state_id_type) !SUPPORTED_STATE_ID_TYPES.include?(state_id_type) && !state_id_type.nil? end + + def bad_mrz?(applicant) + applicant[:state_id_type] == 'passport' && + applicant[:mrz] == 'bad mrz' + end end end end diff --git a/app/services/proofing/resolution/plugins/aamva_plugin.rb b/app/services/proofing/resolution/plugins/aamva_plugin.rb index 16ed6bc9190..437683b3db3 100644 --- a/app/services/proofing/resolution/plugins/aamva_plugin.rb +++ b/app/services/proofing/resolution/plugins/aamva_plugin.rb @@ -66,7 +66,7 @@ def out_of_aamva_jurisdiction_result def proofer @proofer ||= if IdentityConfig.store.proofer_mock_fallback - Proofing::Mock::StateIdMockClient.new + Proofing::Mock::IdMockClient.new else Proofing::Aamva::Proofer.new( auth_request_timeout: IdentityConfig.store.aamva_auth_request_timeout, diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb index a63467283d2..e8830bd37fa 100644 --- a/spec/features/idv/doc_auth/document_capture_spec.rb +++ b/spec/features/idv/doc_auth/document_capture_spec.rb @@ -187,6 +187,67 @@ end end + context 'with a valid passport', allow_browser_log: true do + let(:fake_dos_api_endpoint) { 'http://fake_dos_api_endpoint/' } + + before do + stub_request(:post, fake_dos_api_endpoint) + .to_return(status: 200, body: '{"response" : "YES"}', headers: {}) + + allow(IdentityConfig.store).to receive(:dos_passport_mrz_endpoint) + .and_return(fake_dos_api_endpoint) + visit_idp_from_oidc_sp_with_ial2 + sign_in_and_2fa_user(@user) + complete_doc_auth_steps_before_document_capture_step + end + + it 'works' do + expect(page).to have_content(t('doc_auth.headings.document_capture')) + expect(page).to have_current_path(idv_document_capture_url) + + expect(page).not_to have_content(t('doc_auth.tips.document_capture_selfie_text1')) + attach_images( + Rails.root.join( + 'spec', 'fixtures', + 'passport_credential.yml' + ), + ) + + submit_images + expect(page).to have_content(t('doc_auth.headings.capture_complete')) + end + end + + context 'with an invalid passport', allow_browser_log: true do + let(:fake_dos_api_endpoint) { 'http://fake_dos_api_endpoint/' } + + before do + stub_request(:post, fake_dos_api_endpoint) + .to_return(status: 200, body: '{}', headers: {}) + + allow(IdentityConfig.store).to receive(:dos_passport_mrz_endpoint) + .and_return(fake_dos_api_endpoint) + visit_idp_from_oidc_sp_with_ial2 + sign_in_and_2fa_user(@user) + complete_doc_auth_steps_before_document_capture_step + end + + it 'fails' do + expect(page).to have_current_path(idv_document_capture_url) + expect(page).not_to have_content(t('doc_auth.tips.document_capture_selfie_text1')) + attach_images( + Rails.root.join( + 'spec', 'fixtures', + 'passport_bad_mrz_credential.yml' + ), + ) + + submit_images + + expect(page).not_to have_content(t('doc_auth.headings.capture_complete')) + end + end + context 'standard desktop flow' do before do visit_idp_from_oidc_sp_with_ial2 @@ -296,6 +357,7 @@ end end end + context 'selfie check' do before do allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true) diff --git a/spec/features/idv/doc_auth/verify_info_step_spec.rb b/spec/features/idv/doc_auth/verify_info_step_spec.rb index 131b034ce74..27f64fd3cf1 100644 --- a/spec/features/idv/doc_auth/verify_info_step_spec.rb +++ b/spec/features/idv/doc_auth/verify_info_step_spec.rb @@ -254,7 +254,7 @@ allow(IdentityConfig.store).to receive(:aamva_supported_jurisdictions).and_return( mock_state_id_jurisdiction, ) - expect_any_instance_of(Proofing::Mock::StateIdMockClient).to receive(:proof).with( + expect_any_instance_of(Proofing::Mock::IdMockClient).to receive(:proof).with( hash_including( **Idp::Constants::MOCK_IDV_APPLICANT, ), @@ -271,7 +271,7 @@ IdentityConfig.store.aamva_supported_jurisdictions - mock_state_id_jurisdiction, ) - expect_any_instance_of(Proofing::Mock::StateIdMockClient).to_not receive(:proof) + expect_any_instance_of(Proofing::Mock::IdMockClient).to_not receive(:proof) complete_ssn_step complete_verify_step diff --git a/spec/features/sp_cost_tracking_spec.rb b/spec/features/sp_cost_tracking_spec.rb index 2bf4a64a083..3d6f92eff6f 100644 --- a/spec/features/sp_cost_tracking_spec.rb +++ b/spec/features/sp_cost_tracking_spec.rb @@ -30,7 +30,7 @@ ) expect_sp_cost_type( 5, 2, 'aamva', - transaction_id: Proofing::Mock::StateIdMockClient::TRANSACTION_ID + transaction_id: Proofing::Mock::IdMockClient::TRANSACTION_ID ) expect_sp_cost_type(6, 2, 'lexis_nexis_address') end diff --git a/spec/fixtures/passport_bad_mrz_credential.yml b/spec/fixtures/passport_bad_mrz_credential.yml new file mode 100644 index 00000000000..da6f2a8b64c --- /dev/null +++ b/spec/fixtures/passport_bad_mrz_credential.yml @@ -0,0 +1,18 @@ +passport_check_result: + # returns the DoS API check result + PassportCheckResult: Fail +doc_auth_result: Failed +document: + first_name: Joe + last_name: Dokes + middle_name: Q + dob: 10/06/1938 + sex: Male + state_id_type: passport + mrz: 'P