diff --git a/app/controllers/idv/otp_delivery_method_controller.rb b/app/controllers/idv/otp_delivery_method_controller.rb index 51cbbbcd48b..9655d5e8ecc 100644 --- a/app/controllers/idv/otp_delivery_method_controller.rb +++ b/app/controllers/idv/otp_delivery_method_controller.rb @@ -67,7 +67,9 @@ def render_new_with_error_message def send_phone_confirmation_otp_and_handle_result save_delivery_preference result = send_phone_confirmation_otp - analytics.idv_phone_confirmation_otp_sent(**result.to_h) + analytics.idv_phone_confirmation_otp_sent( + **result.to_h.merge(adapter: Telephony.config.adapter), + ) irs_attempts_api_tracker.idv_phone_otp_sent( phone_number: @idv_phone, diff --git a/app/controllers/users/two_factor_authentication_controller.rb b/app/controllers/users/two_factor_authentication_controller.rb index eeb01398869..d6e69a867a3 100644 --- a/app/controllers/users/two_factor_authentication_controller.rb +++ b/app/controllers/users/two_factor_authentication_controller.rb @@ -219,6 +219,7 @@ def track_events(otp_delivery_preference:) context: context, otp_delivery_preference: otp_delivery_preference, resend: params.dig(:otp_delivery_selection_form, :resend), + adapter: Telephony.config.adapter, telephony_response: @telephony_result.to_h, success: @telephony_result.success?, ) diff --git a/app/forms/add_user_email_form.rb b/app/forms/add_user_email_form.rb index 6ff9243be84..3912d26071a 100644 --- a/app/forms/add_user_email_form.rb +++ b/app/forms/add_user_email_form.rb @@ -1,6 +1,7 @@ class AddUserEmailForm include ActiveModel::Model include FormAddEmailValidator + include ActionView::Helpers::TranslationHelper attr_reader :email diff --git a/app/forms/idv/api_image_upload_form.rb b/app/forms/idv/api_image_upload_form.rb index b792c826bd2..adcf3da3024 100644 --- a/app/forms/idv/api_image_upload_form.rb +++ b/app/forms/idv/api_image_upload_form.rb @@ -199,15 +199,6 @@ def as_readable(image_key) end end - def track_event(event, attributes = {}) - if analytics.present? - analytics.track_event( - event, - attributes, - ) - end - end - def update_analytics(client_response) add_costs(client_response) update_funnel(client_response) @@ -219,13 +210,16 @@ def update_analytics(client_response) ).merge(native_camera_ab_test_data), ) pii_from_doc = client_response.pii_from_doc || {} - store_encrypted_images_if_required + stored_image_result = store_encrypted_images_if_required irs_attempts_api_tracker.idv_document_upload_submitted( success: client_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], + document_front_image_filename: stored_image_result&.front_filename, + document_back_image_filename: stored_image_result&.back_filename, + document_image_encryption_key: stored_image_result&.encryption_key, first_name: pii_from_doc[:first_name], last_name: pii_from_doc[:last_name], date_of_birth: pii_from_doc[:dob], @@ -239,7 +233,9 @@ def store_encrypted_images_if_required encrypted_document_storage_writer.encrypt_and_write_document( front_image: front_image_bytes, + front_image_content_type: front.content_type, back_image: back_image_bytes, + back_image_content_type: back.content_type, ) end diff --git a/app/forms/idv/inherited_proofing/base_form.rb b/app/forms/idv/inherited_proofing/base_form.rb index 63559f6575a..16e144690ef 100644 --- a/app/forms/idv/inherited_proofing/base_form.rb +++ b/app/forms/idv/inherited_proofing/base_form.rb @@ -11,23 +11,9 @@ def model_name def namespaced_model_name self.to_s.gsub('::', '') end - - def fields - @fields ||= required_fields + optional_fields - end - - def required_fields - raise NotImplementedError, - 'Override this method and return an Array of required field names as Symbols' - end - - def optional_fields - raise NotImplementedError, - 'Override this method and return an Array of optional field names as Symbols' - end end - private_class_method :namespaced_model_name, :required_fields, :optional_fields + private_class_method :namespaced_model_name attr_reader :payload_hash @@ -35,16 +21,12 @@ def initialize(payload_hash:) raise ArgumentError, 'payload_hash is blank?' if payload_hash.blank? raise ArgumentError, 'payload_hash is not a Hash' unless payload_hash.is_a? Hash - self.class.attr_accessor(*self.class.fields) - @payload_hash = payload_hash.dup populate_field_data end def submit - validate - FormResponse.new( success: valid?, errors: errors, diff --git a/app/forms/idv/inherited_proofing/va/form.rb b/app/forms/idv/inherited_proofing/va/form.rb index 5f8a0874c47..97878154d4a 100644 --- a/app/forms/idv/inherited_proofing/va/form.rb +++ b/app/forms/idv/inherited_proofing/va/form.rb @@ -2,18 +2,34 @@ module Idv module InheritedProofing module Va class Form < Idv::InheritedProofing::BaseForm - class << self - def required_fields - @required_fields ||= %i[first_name last_name birth_date ssn address_street address_zip] - end - - def optional_fields - @optional_fields ||= %i[phone address_street2 address_city address_state - address_country] - end - end + REQUIRED_FIELDS = %i[first_name + last_name + birth_date + ssn + address_street + address_zip].freeze + OPTIONAL_FIELDS = %i[phone + address_street2 + address_city + address_state + address_country + service_error].freeze + FIELDS = (REQUIRED_FIELDS + OPTIONAL_FIELDS).freeze + + attr_accessor(*FIELDS) + validate :add_service_error, if: :service_error? + validates(*REQUIRED_FIELDS, presence: true, unless: :service_error?) - validates(*required_fields, presence: true) + def submit + extra = {} + extra = { service_error: service_error } if service_error? + + FormResponse.new( + success: validate, + errors: errors, + extra: extra, + ) + end def user_pii raise 'User PII is invalid' unless valid? @@ -30,6 +46,22 @@ def user_pii user_pii[:zipcode] = address_zip user_pii end + + def service_error? + service_error.present? + end + + private + + def add_service_error + errors.add( + :service_provider, + # Use a "safe" error message for the model in case it's displayed + # to the user at any point. + I18n.t('inherited_proofing.errors.service_provider.communication'), + type: :service_error, + ) + end end end end diff --git a/app/javascript/packages/document-capture/context/acuant.tsx b/app/javascript/packages/document-capture/context/acuant.tsx index d4add292131..d81d64c4f1a 100644 --- a/app/javascript/packages/document-capture/context/acuant.tsx +++ b/app/javascript/packages/document-capture/context/acuant.tsx @@ -193,8 +193,8 @@ const getActualAcuantCamera = (): AcuantCameraInterface => { }; function AcuantContextProvider({ - sdkSrc = '/acuant/11.7.0/AcuantJavascriptWebSdk.min.js', - cameraSrc = '/acuant/11.7.0/AcuantCamera.min.js', + sdkSrc = '/acuant/11.7.1/AcuantJavascriptWebSdk.min.js', + cameraSrc = '/acuant/11.7.1/AcuantCamera.min.js', credentials = null, endpoint = null, glareThreshold = DEFAULT_ACCEPTABLE_GLARE_SCORE, diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index b603263bd25..a3d6500717d 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -621,6 +621,12 @@ def idv_inherited_proofing_get_started_visited(flow_path:, step:, **extra) ) end + # Retry retrieving the user PII in the case where the first attempt fails + # in the agreement step, and the user initiates a "retry". + def idv_inherited_proofing_redo_retrieve_user_info_submitted(**extra) + track_event('IdV: inherited proofing retry retrieve user information submitted', **extra) + end + # @param [String] flow_path Document capture path ("hybrid" or "standard") # The user visited the in person proofing location step def idv_in_person_location_visited(flow_path:, **extra) @@ -1146,7 +1152,7 @@ def idv_phone_confirmation_otp_rate_limit_sends(proofing_components: nil, **extr # @param [Boolean] success # @param [Hash] errors - # @param ["sms","voice"] otp_delivery_preference which chaennel the OTP was delivered by + # @param ["sms","voice"] otp_delivery_preference which channel the OTP was delivered by # @param [String] country_code country code of phone number # @param [String] area_code area code of phone number # @param [Boolean] rate_limit_exceeded whether or not the rate limit was exceeded by this attempt @@ -1180,13 +1186,14 @@ def idv_phone_confirmation_otp_resent( # @param [Boolean] success # @param [Hash] errors - # @param ["sms","voice"] otp_delivery_preference which chaennel the OTP was delivered by + # @param ["sms","voice"] otp_delivery_preference which channel the OTP was delivered by # @param [String] country_code country code of phone number # @param [String] area_code area code of phone number # @param [Boolean] rate_limit_exceeded whether or not the rate limit was exceeded by this attempt # @param [String] phone_fingerprint the hmac fingerprint of the phone number formatted as e164 # @param [Hash] telephony_response response from Telephony gem # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [:test, :pinpoint] adapter which adapter the OTP was delivered with # The user requested an OTP to confirm their phone during the IDV phone step def idv_phone_confirmation_otp_sent( success:, @@ -1197,6 +1204,7 @@ def idv_phone_confirmation_otp_sent( rate_limit_exceeded:, phone_fingerprint:, telephony_response:, + adapter:, proofing_components: nil, **extra ) @@ -1210,6 +1218,7 @@ def idv_phone_confirmation_otp_sent( rate_limit_exceeded: rate_limit_exceeded, phone_fingerprint: phone_fingerprint, telephony_response: telephony_response, + adapter: adapter, proofing_components: proofing_components, **extra, ) @@ -2644,6 +2653,7 @@ def sms_opt_in_visit( # @param ["sms","voice"] otp_delivery_preference the channel used to send the message # @param [Boolean] resend # @param [Hash] telephony_response + # @param [:test, :pinpoint] adapter which adapter the OTP was delivered with # @param [Boolean] success # A phone one-time password send was attempted def telephony_otp_sent( @@ -2654,6 +2664,7 @@ def telephony_otp_sent( otp_delivery_preference:, resend:, telephony_response:, + adapter:, success:, **extra ) @@ -2667,6 +2678,7 @@ def telephony_otp_sent( otp_delivery_preference: otp_delivery_preference, resend: resend, telephony_response: telephony_response, + adapter: adapter, success: success, **extra, }, diff --git a/app/services/encrypted_document_storage/document_writer.rb b/app/services/encrypted_document_storage/document_writer.rb index 68c1f1c3496..6dbadc84114 100644 --- a/app/services/encrypted_document_storage/document_writer.rb +++ b/app/services/encrypted_document_storage/document_writer.rb @@ -1,21 +1,25 @@ module EncryptedDocumentStorage class DocumentWriter - def encrypt_and_write_document(front_image:, back_image:) + def encrypt_and_write_document( + front_image:, + front_image_content_type:, + back_image:, + back_image_content_type: + ) key = SecureRandom.bytes(32) encrypted_front_image = aes_cipher.encrypt(front_image, key) encrypted_back_image = aes_cipher.encrypt(back_image, key) - front_image_uuid = SecureRandom.uuid - back_image_uiid = SecureRandom.uuid + front_filename = build_filename_for_content_type(front_image_content_type) + back_filename = build_filename_for_content_type(back_image_content_type) - storage.write_image(encrypted_image: encrypted_front_image, name: front_image_uuid) - storage.write_image(encrypted_image: encrypted_back_image, name: back_image_uiid) + storage.write_image(encrypted_image: encrypted_front_image, name: front_filename) + storage.write_image(encrypted_image: encrypted_back_image, name: back_filename) WriteDocumentResult.new( - front_uuid: front_image_uuid, - back_uuid: back_image_uiid, - front_encryption_key: Base64.strict_encode64(key), - back_encryption_key: Base64.strict_encode64(key), + front_filename: front_filename, + back_filename: back_filename, + encryption_key: Base64.strict_encode64(key), ) end @@ -32,5 +36,11 @@ def storage def aes_cipher @aes_cipher ||= Encryption::AesCipher.new end + + # @return [String] A new, unique S3 key for an image of the given content type. + def build_filename_for_content_type(content_type) + ext = Rack::Mime::MIME_TYPES.rassoc(content_type)&.first + "#{SecureRandom.uuid}#{ext}" + end end end diff --git a/app/services/encrypted_document_storage/local_storage.rb b/app/services/encrypted_document_storage/local_storage.rb index eb9a29da0b1..e55be4b1092 100644 --- a/app/services/encrypted_document_storage/local_storage.rb +++ b/app/services/encrypted_document_storage/local_storage.rb @@ -1,5 +1,11 @@ module EncryptedDocumentStorage class LocalStorage + # Used in tests to verify results + def read_image(name:) + filepath = tmp_document_storage_dir.join(name) + File.read(filepath) + end + def write_image(encrypted_image:, name:) FileUtils.mkdir_p(tmp_document_storage_dir) filepath = tmp_document_storage_dir.join(name) diff --git a/app/services/encrypted_document_storage/write_document_result.rb b/app/services/encrypted_document_storage/write_document_result.rb index ea8fb247fa6..4e9a21e1107 100644 --- a/app/services/encrypted_document_storage/write_document_result.rb +++ b/app/services/encrypted_document_storage/write_document_result.rb @@ -1,9 +1,8 @@ module EncryptedDocumentStorage WriteDocumentResult = Struct.new( - :front_uuid, - :back_uuid, - :front_encryption_key, - :back_encryption_key, + :front_filename, + :back_filename, + :encryption_key, keyword_init: true, ) end diff --git a/app/services/flow/base_flow.rb b/app/services/flow/base_flow.rb index 8bcf66618be..6f5779794e7 100644 --- a/app/services/flow/base_flow.rb +++ b/app/services/flow/base_flow.rb @@ -1,5 +1,7 @@ module Flow class BaseFlow + include Failure + attr_accessor :flow_session attr_reader :steps, :actions, :current_user, :current_sp, :params, :request, :json, :http_status, :controller diff --git a/app/services/flow/base_step.rb b/app/services/flow/base_step.rb index f138d0d15df..c6f55650d25 100644 --- a/app/services/flow/base_step.rb +++ b/app/services/flow/base_step.rb @@ -1,6 +1,7 @@ module Flow class BaseStep include Rails.application.routes.url_helpers + include Failure def initialize(flow, name) @flow = flow @@ -51,13 +52,6 @@ def form_submit FormResponse.new(success: true) end - def failure(message, extra = nil) - flow_session[:error_message] = message - form_response_params = { success: false, errors: { message: message } } - form_response_params[:extra] = extra unless extra.nil? - FormResponse.new(**form_response_params) - end - def flow_params params[@name] end diff --git a/app/services/flow/failure.rb b/app/services/flow/failure.rb new file mode 100644 index 00000000000..c84beedae71 --- /dev/null +++ b/app/services/flow/failure.rb @@ -0,0 +1,12 @@ +module Flow + module Failure + private + + def failure(message, extra = nil) + flow_session[:error_message] = message + form_response_params = { success: false, errors: { message: message } } + form_response_params[:extra] = extra unless extra.nil? + FormResponse.new(**form_response_params) + end + end +end diff --git a/app/services/gpo_confirmation_exporter.rb b/app/services/gpo_confirmation_exporter.rb index da7d4375bd4..b56a7829e23 100644 --- a/app/services/gpo_confirmation_exporter.rb +++ b/app/services/gpo_confirmation_exporter.rb @@ -22,7 +22,7 @@ def run def make_psv(csv) csv << make_header_row(confirmations.size) confirmations.each do |confirmation| - csv << make_entry_row(confirmation.entry) + csv << make_entry_row(confirmation) end end @@ -30,9 +30,11 @@ def make_header_row(num_entries) [HEADER_ROW_ID, num_entries] end - def make_entry_row(entry) + def make_entry_row(confirmation) now = current_date - due = now + OTP_MAX_VALID_DAYS.days + due = confirmation.created_at + OTP_MAX_VALID_DAYS.days + + entry = confirmation.entry service_provider = ServiceProvider.find_by(issuer: entry[:issuer]) [ diff --git a/app/services/idv/actions/inherited_proofing/redo_retrieve_user_info_action.rb b/app/services/idv/actions/inherited_proofing/redo_retrieve_user_info_action.rb new file mode 100644 index 00000000000..a9fd7ed2c2e --- /dev/null +++ b/app/services/idv/actions/inherited_proofing/redo_retrieve_user_info_action.rb @@ -0,0 +1,19 @@ +module Idv + module Actions + module InheritedProofing + class RedoRetrieveUserInfoAction < Idv::Steps::InheritedProofing::VerifyWaitStepShow + class << self + def analytics_submitted_event + :idv_inherited_proofing_redo_retrieve_user_info_submitted + end + end + + def call + enqueue_job unless api_call_already_in_progress? + + super + end + end + end + end +end diff --git a/app/services/idv/data_url_image.rb b/app/services/idv/data_url_image.rb index 073f80e39d6..5fa71a7a27c 100644 --- a/app/services/idv/data_url_image.rb +++ b/app/services/idv/data_url_image.rb @@ -10,6 +10,11 @@ def initialize(data_url) @data = data end + # @return [String] + def content_type + @header.split(';', 2).first + end + # @return [String] def read if base64_encoded? diff --git a/app/services/idv/flows/inherited_proofing_flow.rb b/app/services/idv/flows/inherited_proofing_flow.rb index 94a559c537c..2fc56d71103 100644 --- a/app/services/idv/flows/inherited_proofing_flow.rb +++ b/app/services/idv/flows/inherited_proofing_flow.rb @@ -18,7 +18,9 @@ class InheritedProofingFlow < Flow::BaseFlow { name: :secure_account }, ].freeze - ACTIONS = {}.freeze + ACTIONS = { + redo_retrieve_user_info: Idv::Actions::InheritedProofing::RedoRetrieveUserInfoAction, + }.freeze attr_reader :idv_session diff --git a/app/services/idv/inherited_proofing/va/mocks/service.rb b/app/services/idv/inherited_proofing/va/mocks/service.rb index fc1ed38269f..e1150a9528d 100644 --- a/app/services/idv/inherited_proofing/va/mocks/service.rb +++ b/app/services/idv/inherited_proofing/va/mocks/service.rb @@ -36,7 +36,7 @@ class Service }.freeze ERROR_HASH = { - errors: 'InheritedProofing::Errors::MHVIdentityDataNotFoundError', + service_error: 'the server responded with status 401', }.freeze def initialize(service_provider_data) diff --git a/app/services/idv/inherited_proofing/va/service.rb b/app/services/idv/inherited_proofing/va/service.rb index fc7e70cd91e..7e7c76a7e28 100644 --- a/app/services/idv/inherited_proofing/va/service.rb +++ b/app/services/idv/inherited_proofing/va/service.rb @@ -17,12 +17,35 @@ def initialize(service_provider_data) def execute raise 'The provided auth_code is blank?' if auth_code.blank? - response = request - payload_to_hash decrypt_payload(response) + begin + response = request + return payload_to_hash decrypt_payload(response) if response.status == 200 + + service_error(not_200_service_error(response.status)) + rescue => error + service_error(error.message) + end end private + def service_error(message) + { service_error: message } + end + + def not_200_service_error(http_status) + # Under certain circumstances, Faraday may return a nil http status. + # https://lostisland.github.io/faraday/middleware/raise-error + if http_status.blank? + http_status = 'unavailable' + http_status_description = 'unavailable' + else + http_status_description = Rack::Utils::HTTP_STATUS_CODES[http_status] + end + "The service provider API returned an http status other than 200: " \ + "#{http_status} (#{http_status_description})" + end + def request connection.get(request_uri) { |req| req.headers = request_headers } end diff --git a/app/services/idv/steps/inherited_proofing/agreement_step.rb b/app/services/idv/steps/inherited_proofing/agreement_step.rb index c3b125b24c0..b49e3d7ec7d 100644 --- a/app/services/idv/steps/inherited_proofing/agreement_step.rb +++ b/app/services/idv/steps/inherited_proofing/agreement_step.rb @@ -2,6 +2,8 @@ module Idv module Steps module InheritedProofing class AgreementStep < VerifyBaseStep + include UserPiiJobInitiator + delegate :controller, :idv_session, to: :@flow STEP_INDICATOR_STEP = :getting_started @@ -24,28 +26,6 @@ def form_submit def consent_form_params params.require(:inherited_proofing).permit(:ial2_consent_given) end - - def enqueue_job - return if api_call_already_in_progress? - - doc_capture_session = create_document_capture_session( - inherited_proofing_verify_step_document_capture_session_uuid_key, - ) - - doc_capture_session.create_proofing_session - - InheritedProofingJob.perform_later( - controller.inherited_proofing_service_provider, - controller.inherited_proofing_service_provider_data, - doc_capture_session.uuid, - ) - end - - def api_call_already_in_progress? - DocumentCaptureSession.find_by( - uuid: flow_session['inherited_proofing_verify_step_document_capture_session_uuid'], - )&.in_progress? - end end end end diff --git a/app/services/idv/steps/inherited_proofing/user_pii_job_initiator.rb b/app/services/idv/steps/inherited_proofing/user_pii_job_initiator.rb new file mode 100644 index 00000000000..3d39f3b0599 --- /dev/null +++ b/app/services/idv/steps/inherited_proofing/user_pii_job_initiator.rb @@ -0,0 +1,35 @@ +module Idv + module Steps + module InheritedProofing + module UserPiiJobInitiator + private + + def enqueue_job + return if api_call_already_in_progress? + + create_document_capture_session( + inherited_proofing_verify_step_document_capture_session_uuid_key, + ).tap do |doc_capture_session| + doc_capture_session.create_proofing_session + + InheritedProofingJob.perform_later( + controller.inherited_proofing_service_provider, + controller.inherited_proofing_service_provider_data, + doc_capture_session.uuid, + ) + end + end + + def api_call_already_in_progress? + DocumentCaptureSession.find_by( + uuid: flow_session[inherited_proofing_verify_step_document_capture_session_uuid_key], + ).present? + end + + def delete_async + flow_session.delete(inherited_proofing_verify_step_document_capture_session_uuid_key) + end + end + end + end +end diff --git a/app/services/idv/steps/inherited_proofing/verify_wait_step_show.rb b/app/services/idv/steps/inherited_proofing/verify_wait_step_show.rb index 45df7920b8c..611c8d8731d 100644 --- a/app/services/idv/steps/inherited_proofing/verify_wait_step_show.rb +++ b/app/services/idv/steps/inherited_proofing/verify_wait_step_show.rb @@ -2,6 +2,7 @@ module Idv module Steps module InheritedProofing class VerifyWaitStepShow < VerifyBaseStep + include UserPiiJobInitiator include UserPiiManagable include Idv::InheritedProofing::ServiceProviderForms delegate :controller, :idv_session, to: :@flow @@ -21,17 +22,15 @@ def call private def process_async_state(current_async_state) + return if current_async_state.in_progress? + if current_async_state.none? mark_step_incomplete(:agreement) - elsif current_async_state.in_progress? - nil elsif current_async_state.missing? flash[:error] = I18n.t('idv.failure.timeout') - # Need to add path to error pages once they exist - # LG-7257 - # This method overrides VerifyBaseStep#process_async_state: - # See the VerifyBaseStep#process_async_state "elsif current_async_state.missing?" - # logic as to what is typically needed/performed when hitting this logic path. + delete_async + mark_step_incomplete(:agreement) + @flow.analytics.idv_proofing_resolution_result_missing elsif current_async_state.done? async_state_done(current_async_state) end @@ -54,11 +53,16 @@ def async_state_done(_current_async_state) ) form_response = form.submit + delete_async + if form_response.success? inherited_proofing_save_user_pii_to_session!(form.user_pii) mark_step_complete(:verify_wait) + elsif throttle.throttled? + idv_failure(form_response) else - mark_step_incomplete(:agreement) + mark_step_complete(:agreement) + idv_failure(form_response) end form_response @@ -76,6 +80,34 @@ def document_capture_session def api_job_result document_capture_session.load_proofing_result end + + # Base class overrides + + def throttle + @throttle ||= Throttle.new( + user: current_user, + throttle_type: :inherited_proofing, + ) + end + + def idv_failure_log_throttled + @flow.analytics.throttler_rate_limit_triggered( + throttle_type: throttle.throttle_type, + step_name: self.class.name, + ) + end + + def throttled_url + idv_inherited_proofing_errors_failure_url(flow: :inherited_proofing) + end + + def exception_url + idv_inherited_proofing_errors_failure_url(flow: :inherited_proofing) + end + + def warning_url + idv_inherited_proofing_errors_no_information_url(flow: :inherited_proofing) + end end end end diff --git a/app/services/idv/steps/verify_base_step.rb b/app/services/idv/steps/verify_base_step.rb index 7a0e912f9f6..9865bac8fbb 100644 --- a/app/services/idv/steps/verify_base_step.rb +++ b/app/services/idv/steps/verify_base_step.rb @@ -78,26 +78,41 @@ def throttle def idv_failure(result) throttle.increment! if result.extra.dig(:proofing_results, :exception).blank? if throttle.throttled? - @flow.irs_attempts_api_tracker.idv_verification_rate_limited - @flow.analytics.throttler_rate_limit_triggered( - throttle_type: :idv_resolution, - step_name: self.class.name, - ) - redirect_to idv_session_errors_failure_url + idv_failure_log_throttled + redirect_to throttled_url elsif result.extra.dig(:proofing_results, :exception).present? - @flow.analytics.idv_doc_auth_exception_visited( - step_name: self.class.name, - remaining_attempts: throttle.remaining_count, - ) + idv_failure_log_error redirect_to exception_url else - @flow.analytics.idv_doc_auth_warning_visited( - step_name: self.class.name, - remaining_attempts: throttle.remaining_count, - ) + idv_failure_log_warning redirect_to warning_url end - result + end + + def idv_failure_log_throttled + @flow.irs_attempts_api_tracker.idv_verification_rate_limited + @flow.analytics.throttler_rate_limit_triggered( + throttle_type: :idv_resolution, + step_name: self.class.name, + ) + end + + def idv_failure_log_error + @flow.analytics.idv_doc_auth_exception_visited( + step_name: self.class.name, + remaining_attempts: throttle.remaining_count, + ) + end + + def idv_failure_log_warning + @flow.analytics.idv_doc_auth_warning_visited( + step_name: self.class.name, + remaining_attempts: throttle.remaining_count, + ) + end + + def throttled_url + idv_session_errors_failure_url end def exception_url diff --git a/app/services/irs_attempts_api/tracker_events.rb b/app/services/irs_attempts_api/tracker_events.rb index 891994bb98f..4f8302a7e95 100644 --- a/app/services/irs_attempts_api/tracker_events.rb +++ b/app/services/irs_attempts_api/tracker_events.rb @@ -89,6 +89,9 @@ def idv_document_upload_rate_limited # @param [String] document_number # @param [String] document_issued # @param [String] document_expiration + # @param [String] document_front_image_filename Filename in S3 w/ encrypted data for the front. + # @param [String] document_back_image_filename Filename in S3 w/ encrypted data for the back. + # @param [String] document_image_encryption_key Base64-encoded AES key used for images. # @param [String] first_name # @param [String] last_name # @param [String] date_of_birth @@ -101,6 +104,9 @@ def idv_document_upload_submitted( document_number: nil, document_issued: nil, document_expiration: nil, + document_front_image_filename: nil, + document_back_image_filename: nil, + document_image_encryption_key: nil, first_name: nil, last_name: nil, date_of_birth: nil, @@ -114,6 +120,9 @@ def idv_document_upload_submitted( document_number: document_number, document_issued: document_issued, document_expiration: document_expiration, + document_front_image_filename: document_front_image_filename, + document_back_image_filename: document_back_image_filename, + document_image_encryption_key: document_image_encryption_key, first_name: first_name, last_name: last_name, date_of_birth: date_of_birth, diff --git a/app/services/proofing/base.rb b/app/services/proofing/base.rb deleted file mode 100644 index 3a76306ef59..00000000000 --- a/app/services/proofing/base.rb +++ /dev/null @@ -1,100 +0,0 @@ -require 'set' - -module Proofing - class Base - @vendor_name = nil - @required_attributes = [] - @optional_attributes = [] - @stage = nil - - class << self - attr_reader :proofer - - def vendor_name(name = nil) - @vendor_name = name || @vendor_name - end - - def required_attributes(*required_attributes) - return @required_attributes || [] if required_attributes.empty? - @required_attributes = required_attributes - end - - def optional_attributes(*optional_attributes) - return @optional_attributes || [] if optional_attributes.empty? - @optional_attributes = optional_attributes - end - - def attributes - [*required_attributes, *optional_attributes] - end - - def stage(stage = nil) - @stage = stage || @stage - end - - def proof(sym = nil, &block) - @proofer = sym || block - end - end - - def proof(applicant) - vendor_applicant = restrict_attributes(applicant) - validate_attributes(vendor_applicant) - result = Proofing::Result.new - execute_proof(proofer, vendor_applicant, result) - result - rescue => exception - NewRelic::Agent.notice_error(exception) - Proofing::Result.new(exception: exception) - end - - private - - def execute_proof(proofer, *args) - if proofer.is_a? Symbol - send(proofer, *args) - else - instance_exec(*args, &proofer) - end - end - - def restrict_attributes(applicant) - applicant.select { |attribute| attributes.include?(attribute) } - end - - def validate_attributes(applicant) - empty_attributes = applicant.select { |_, attribute| blank?(attribute) }.keys - missing_attributes = attributes - applicant.keys - bad_attributes = (empty_attributes | missing_attributes) - optional_attributes - raise error_message(bad_attributes) if bad_attributes.any? - end - - def error_message(required_attributes) - "Required attributes #{required_attributes.join(', ')} are not present" - end - - def required_attributes - self.class.required_attributes - end - - def optional_attributes - self.class.optional_attributes - end - - def attributes - self.class.attributes - end - - def stage - self.class.stage - end - - def proofer - self.class.proofer - end - - def blank?(val) - !val || val.to_s.empty? - end - end -end diff --git a/app/services/proofing/lexis_nexis/ddp/proofer.rb b/app/services/proofing/lexis_nexis/ddp/proofer.rb index 7a78f1164a7..cc338d2b1f8 100644 --- a/app/services/proofing/lexis_nexis/ddp/proofer.rb +++ b/app/services/proofing/lexis_nexis/ddp/proofer.rb @@ -1,41 +1,75 @@ module Proofing module LexisNexis module Ddp - class Proofer < LexisNexis::Proofer - vendor_name 'lexisnexis:ddp' - - required_attributes :threatmetrix_session_id, - :state_id_number, - :first_name, - :last_name, - :dob, - :ssn, - :address1, - :city, - :state, - :zipcode, - :request_ip - - optional_attributes :address2, :phone, :email, :uuid_prefix - - stage :resolution - - proof do |applicant, result| - proof_applicant(applicant, result) + class Proofer + class << self + def required_attributes + [:threatmetrix_session_id, + :state_id_number, + :first_name, + :last_name, + :dob, + :ssn, + :address1, + :city, + :state, + :zipcode, + :request_ip] + end + + def vendor_name + 'lexisnexis' + end + + def optional_attributes + [:address2, :phone, :email, :uuid_prefix] + end + + def stage + :resolution + end + end + + Config = RedactedStruct.new( + :instant_verify_workflow, + :phone_finder_workflow, + :account_id, + :base_url, + :username, + :password, + :request_mode, + :request_timeout, + :org_id, + :api_key, + keyword_init: true, + allowed_members: [ + :instant_verify_workflow, + :phone_finder_workflow, + :base_url, + :request_mode, + :request_timeout, + ], + ) + + attr_reader :config + + def initialize(attrs) + @config = Config.new(attrs) end def send_verification_request(applicant) VerificationRequest.new(config: config, applicant: applicant).send end - def proof_applicant(applicant, result) + def proof(applicant) response = send_verification_request(applicant) - process_response(response, result) + process_response(response) end private - def process_response(response, result) + def process_response(response) + result = Proofing::Result.new body = response.response_body result.response_body = body result.transaction_id = body['request_id'] @@ -44,6 +78,7 @@ def process_response(response, result) result.review_status = review_status result.add_error(:request_result, request_result) unless request_result == 'success' result.add_error(:review_status, review_status) unless review_status == 'pass' + result end end end diff --git a/app/services/proofing/lexis_nexis/instant_verify/proofer.rb b/app/services/proofing/lexis_nexis/instant_verify/proofer.rb index 3c177a5bc36..6eb80530694 100644 --- a/app/services/proofing/lexis_nexis/instant_verify/proofer.rb +++ b/app/services/proofing/lexis_nexis/instant_verify/proofer.rb @@ -5,7 +5,7 @@ class Proofer attr_reader :config def initialize(config) - @config = LexisNexis::Proofer::Config.new(config) + @config = LexisNexis::Ddp::Proofer::Config.new(config) end def proof(applicant) diff --git a/app/services/proofing/lexis_nexis/phone_finder/proofer.rb b/app/services/proofing/lexis_nexis/phone_finder/proofer.rb index 269b6b2d1bf..f0d981a43dc 100644 --- a/app/services/proofing/lexis_nexis/phone_finder/proofer.rb +++ b/app/services/proofing/lexis_nexis/phone_finder/proofer.rb @@ -5,7 +5,7 @@ class Proofer attr_reader :config def initialize(config) - @config = LexisNexis::Proofer::Config.new(config) + @config = LexisNexis::Ddp::Proofer::Config.new(config) end def proof(applicant) diff --git a/app/services/proofing/lexis_nexis/proofer.rb b/app/services/proofing/lexis_nexis/proofer.rb deleted file mode 100644 index 07cba134baa..00000000000 --- a/app/services/proofing/lexis_nexis/proofer.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'redacted_struct' - -module Proofing - module LexisNexis - class Proofer < Proofing::Base - Config = RedactedStruct.new( - :instant_verify_workflow, - :phone_finder_workflow, - :account_id, - :base_url, - :username, - :password, - :request_mode, - :request_timeout, - :org_id, - :api_key, - keyword_init: true, - allowed_members: [ - :instant_verify_workflow, - :phone_finder_workflow, - :base_url, - :request_mode, - :request_timeout, - ], - ) - - attr_reader :config - - def initialize(**attrs) - @config = Config.new(**attrs) - end - - def proof_applicant(applicant, result) - response = send_verification_request(applicant) - result.transaction_id = response.conversation_id - result.reference = response.reference - return if response.verification_status == 'passed' - - response.verification_errors.each do |key, error_message| - result.add_error(key, error_message) - end - end - - private - - def send_verification_request - raise NotImplementedError, "#{__method__} should be defined by a subclass" - end - end - end -end diff --git a/app/services/proofing/mock/ddp_mock_client.rb b/app/services/proofing/mock/ddp_mock_client.rb index 9a5bd8c77e8..f67314779cd 100644 --- a/app/services/proofing/mock/ddp_mock_client.rb +++ b/app/services/proofing/mock/ddp_mock_client.rb @@ -1,27 +1,38 @@ module Proofing module Mock - class DdpMockClient < Proofing::Base - vendor_name 'DdpMock' + class DdpMockClient + class << self + def vendor_name + 'DdpMock' + end - required_attributes :threatmetrix_session_id, - :state_id_number, - :first_name, - :last_name, - :dob, - :ssn, - :address1, - :city, - :state, - :zipcode, - :request_ip + def required_attributes + %I[threatmetrix_session_id + state_id_number + first_name + last_name + dob + ssn + address1 + city + state + zipcode + request_ip] + end - optional_attributes :address2, :phone, :email, :uuid_prefix + def optional_attributes + %I[address2 phone email uuid_prefix] + end - stage :resolution + def stage + :resolution + end + end TRANSACTION_ID = 'ddp-mock-transaction-id-123' - proof do |applicant, result| + def proof(applicant) + result = Proofing::Result.new result.transaction_id = TRANSACTION_ID response_body = File.read( @@ -36,6 +47,8 @@ class DdpMockClient < Proofing::Base result.response_body = JSON.parse(response_body).tap do |json_body| json_body['review_status'] = status end + + result end def review_status(session_id:) diff --git a/app/services/proofing/mock/resolution_mock_client.rb b/app/services/proofing/mock/resolution_mock_client.rb index 8677731c2f7..10e2cb71ce5 100644 --- a/app/services/proofing/mock/resolution_mock_client.rb +++ b/app/services/proofing/mock/resolution_mock_client.rb @@ -1,6 +1,6 @@ module Proofing module Mock - class ResolutionMockClient < Proofing::Base + class ResolutionMockClient UNVERIFIABLE_ZIP_CODE = '00000' NO_CONTACT_SSN = /000-?00-?0000/ TRANSACTION_ID = 'resolution-mock-transaction-id-123' diff --git a/app/services/proofing/mock/state_id_mock_client.rb b/app/services/proofing/mock/state_id_mock_client.rb index 23924635cff..7f01945df57 100644 --- a/app/services/proofing/mock/state_id_mock_client.rb +++ b/app/services/proofing/mock/state_id_mock_client.rb @@ -2,7 +2,7 @@ module Proofing module Mock - class StateIdMockClient < Proofing::Base + class StateIdMockClient SUPPORTED_STATE_ID_TYPES = %w[ drivers_license drivers_permit state_id_card ].to_set.freeze diff --git a/app/services/proofing/result.rb b/app/services/proofing/result.rb index f3c366a0ccf..e07d9eb4328 100644 --- a/app/services/proofing/result.rb +++ b/app/services/proofing/result.rb @@ -4,13 +4,13 @@ class Result attr_accessor :context, :transaction_id, :reference, :review_status, :response_body def initialize( - errors: {}, - context: {}, - exception: nil, - transaction_id: nil, - reference: nil, - response_body: nil - ) + errors: {}, + context: {}, + exception: nil, + transaction_id: nil, + reference: nil, + response_body: nil + ) @errors = errors @context = context @exception = exception diff --git a/app/services/throttle.rb b/app/services/throttle.rb index df0e89c150e..c0fc4bf7e2b 100644 --- a/app/services/throttle.rb +++ b/app/services/throttle.rb @@ -49,6 +49,10 @@ class Throttle max_attempts: IdentityConfig.store.phone_confirmation_max_attempts, attempt_window: IdentityConfig.store.phone_confirmation_max_attempt_window_in_minutes, }, + inherited_proofing: { + max_attempts: IdentityConfig.store.inherited_proofing_max_attempts, + attempt_window: IdentityConfig.store.inherited_proofing_max_attempt_window_in_minutes, + }, }.with_indifferent_access.freeze def initialize(throttle_type:, user: nil, target: nil) diff --git a/app/validators/form_add_email_validator.rb b/app/validators/form_add_email_validator.rb index b1539350f3a..9882e837ba2 100644 --- a/app/validators/form_add_email_validator.rb +++ b/app/validators/form_add_email_validator.rb @@ -13,10 +13,22 @@ module FormAddEmailValidator mx_with_fallback: !ENV['RAILS_OFFLINE'], ban_disposable_email: true, } + validate :validate_domain end private + def validate_domain + return unless email.present? && errors.blank? + domain = Mail::Address.new(email).domain + + if domain && !domain.ascii_only? + errors.add(:email, t('valid_email.validations.email.invalid'), type: :domain) + end + rescue Mail::Field::IncompleteParseError + errors.add(:email, t('valid_email.validations.email.invalid'), type: :domain) + end + def downcase_and_strip self.email = email&.downcase&.strip end diff --git a/app/validators/form_email_validator.rb b/app/validators/form_email_validator.rb index 814e474a55a..9b822eff5ef 100644 --- a/app/validators/form_email_validator.rb +++ b/app/validators/form_email_validator.rb @@ -11,10 +11,22 @@ module FormEmailValidator mx_with_fallback: !ENV['RAILS_OFFLINE'], ban_disposable_email: true, } + validate :validate_domain end private + def validate_domain + return unless email.present? && errors.blank? + domain = Mail::Address.new(email).domain + + if domain && !domain.ascii_only? + errors.add(:email, t('valid_email.validations.email.invalid'), type: :domain) + end + rescue Mail::Field::IncompleteParseError + errors.add(:email, t('valid_email.validations.email.invalid'), type: :domain) + end + def downcase_and_strip self.email = email&.downcase&.strip end diff --git a/app/views/idv/inherited_proofing_errors/warning.html.erb b/app/views/idv/inherited_proofing_errors/warning.html.erb index b06fa1930bb..58044bd9f77 100644 --- a/app/views/idv/inherited_proofing_errors/warning.html.erb +++ b/app/views/idv/inherited_proofing_errors/warning.html.erb @@ -8,7 +8,7 @@ <%= t('inherited_proofing.errors.cannot_retrieve.info') %>
- <%= link_to t('inherited_proofing.buttons.try_again'), root_url, class: 'usa-button usa-button--big usa-button--wide' %> + <%= button_to t('inherited_proofing.buttons.try_again'), idv_inherited_proofing_step_path(step: :redo_retrieve_user_info), method: :put, class: 'usa-button usa-button--big usa-button--wide' %> <%= render( 'shared/troubleshooting_options', diff --git a/config/application.yml.default b/config/application.yml.default index 91df708da7d..355f85ebf3d 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -127,6 +127,8 @@ in_person_completion_survey_url: 'https://login.gov' include_slo_in_saml_metadata: false inherited_proofing_enabled: false inherited_proofing_va_base_url: 'https://staging-api.va.gov' +inherited_proofing_max_attempts: 2 +inherited_proofing_max_attempt_window_in_minutes: 1 va_inherited_proofing_mock_enabled: false irs_attempt_api_audience: 'https://irs.gov' irs_attempt_api_auth_tokens: '' diff --git a/config/locales/inherited_proofing/en.yml b/config/locales/inherited_proofing/en.yml index 6914473751b..c25dad3569c 100644 --- a/config/locales/inherited_proofing/en.yml +++ b/config/locales/inherited_proofing/en.yml @@ -27,6 +27,8 @@ en: info: We are temporarily having trouble retrieving your information. Please try again. title: Couldn’t retrieve information + service_provider: + communication: 'communication was unsuccessful' headings: lets_go: How verifying your identity works retrieval: We are retrieving your information from My HealtheVet diff --git a/config/locales/inherited_proofing/es.yml b/config/locales/inherited_proofing/es.yml index 228bc52bc0a..dc6d924f0a4 100644 --- a/config/locales/inherited_proofing/es.yml +++ b/config/locales/inherited_proofing/es.yml @@ -28,6 +28,8 @@ es: info: Estamos teniendo problemas temporalmente para obtener su información. Inténtelo de nuevo. title: to be implemented + service_provider: + communication: 'la comunicacion no tuvo exito' headings: lets_go: to be implemented retrieval: Estamos recuperando su información de My HealtheVet diff --git a/config/locales/inherited_proofing/fr.yml b/config/locales/inherited_proofing/fr.yml index 209686a40aa..0ff9ad3878e 100644 --- a/config/locales/inherited_proofing/fr.yml +++ b/config/locales/inherited_proofing/fr.yml @@ -29,6 +29,8 @@ fr: info: Nous avons temporairement des difficultés à récupérer vos informations. Veuillez réessayer. title: to be implemented + service_provider: + communication: 'la communication a échoué' headings: lets_go: to be implemented retrieval: Nous récupérons vos informations depuis My HealtheVet. diff --git a/lib/identity_config.rb b/lib/identity_config.rb index e3a734946ae..d6214abc001 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -206,6 +206,8 @@ def self.build_store(config_map) config.add(:include_slo_in_saml_metadata, type: :boolean) config.add(:inherited_proofing_enabled, type: :boolean) config.add(:inherited_proofing_va_base_url, type: :string) + config.add(:inherited_proofing_max_attempts, type: :integer) + config.add(:inherited_proofing_max_attempt_window_in_minutes, type: :integer) config.add(:va_inherited_proofing_mock_enabled, type: :boolean) config.add(:irs_attempt_api_audience) config.add(:irs_attempt_api_auth_tokens, type: :comma_separated_string_list) diff --git a/public/acuant/11.5.0/AcuantCamera.min.js b/public/acuant/11.5.0/AcuantCamera.min.js deleted file mode 100644 index ba5ade42ad3..00000000000 --- a/public/acuant/11.5.0/AcuantCamera.min.js +++ /dev/null @@ -1 +0,0 @@ -var AcuantCameraUI=function(){"use strict";let e=null,t=null,a=null,i=null,n={start:function(n,s,p){m=s,p&&(u=p);AcuantCamera.isCameraSupported?r||(r=!0,S(),function(n){var r=0,s=(new Date).getTime();i=document.getElementById("acuant-camera"),i&&i.addEventListener("acuantcameracreated",(()=>{e=document.getElementById("acuant-player"),t=document.getElementById("acuant-ui-canvas"),a=t.getContext("2d"),t.setAttribute("role","img"),e.addEventListener("play",A,0)}));AcuantCamera.start((e=>{!function(e,t){if(t>=3)return!0;{let t=(new Date).getTime()-e;return t