diff --git a/app/forms/idv/document_capture_form.rb b/app/forms/idv/document_capture_form.rb index 24262f5fb9f..2bffc1ee36b 100644 --- a/app/forms/idv/document_capture_form.rb +++ b/app/forms/idv/document_capture_form.rb @@ -2,12 +2,24 @@ module Idv class DocumentCaptureForm include ActiveModel::Model - ATTRIBUTES = %i[front_image front_image_data_url back_image back_image_data_url].freeze + ATTRIBUTES = %i[front_image front_image_data_url + back_image back_image_data_url + selfie_image selfie_image_data_url].freeze - attr_accessor :front_image, :front_image_data_url, :back_image, :back_image_data_url + attr_accessor :front_image, :front_image_data_url, + :back_image, :back_image_data_url, + :selfie_image, :selfie_image_data_url + + attr_reader :liveness_checking_enabled validate :front_image_or_image_data_url_presence validate :back_image_or_image_data_url_presence + validate :selfie_image_or_image_data_url_presence + + def initialize(**args) + @liveness_checking_enabled = args.delete(:liveness_checking_enabled) + super + end def self.model_name ActiveModel::Name.new(self, nil, 'Image') @@ -31,6 +43,12 @@ def back_image_or_image_data_url_presence errors.add(:back_image, :blank) end + def selfie_image_or_image_data_url_presence + return unless liveness_checking_enabled + return if selfie_image.present? || selfie_image_data_url.present? + errors.add(:selfie_image, :blank) + end + def consume_params(params) params.each do |key, value| raise_invalid_image_parameter_error(key) unless ATTRIBUTES.include?(key.to_sym) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index df7f0eaf36f..dda1b242f49 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -57,6 +57,11 @@ def user_verifying_identity? sp_session && sp_session[:ial2] && multiple_factors_enabled? end + def liveness_checking_enabled? + FeatureManagement.liveness_checking_enabled? && + (sp_session[:issuer].blank? || sp_session[:ial2_strict]) + end + def sign_up_or_idv_no_js_link if user_signing_up? destroy_user_path diff --git a/app/javascript/packs/image-preview.js b/app/javascript/packs/image-preview.js index 25b96c64d02..0dbbb3b709c 100644 --- a/app/javascript/packs/image-preview.js +++ b/app/javascript/packs/image-preview.js @@ -26,52 +26,37 @@ function imagePreview() { }); } -function frontImagePreview() { - $('#doc_auth_front_image').on('change', function(event) { - $('.simple_form .alert-error').hide(); - $('.simple_form .alert-notice').hide(); - const { files } = event.target; - const image = files[0]; - const reader = new FileReader(); - reader.onload = function(file) { - const img = new Image(); - img.onload = function () { - const displayWidth = '460'; - const ratio = (this.height / this.width); - img.width = displayWidth; - img.height = (displayWidth * ratio); - $('#front_target').html(img); - }; - img.src = file.target.result; - $('#front_target').html(img); - }; - reader.readAsDataURL(image); - }); -} +document.addEventListener('DOMContentLoaded', imagePreview); -function backImagePreview() { - $('#doc_auth_back_image').on('change', function(event) { - $('.simple_form .alert-error').hide(); - $('.simple_form .alert-notice').hide(); - const { files } = event.target; - const image = files[0]; - const reader = new FileReader(); - reader.onload = function(file) { - const img = new Image(); - img.onload = function () { - const displayWidth = '460'; - const ratio = (this.height / this.width); - img.width = displayWidth; - img.height = (displayWidth * ratio); - $('#back_target').html(img); +function imagePreviewFunction(imageId, imageTarget) { + return function() { + $(imageId).on('change', function(event) { + $('.simple_form .alert-error').hide(); + $('.simple_form .alert-notice').hide(); + const { files } = event.target; + const image = files[0]; + const reader = new FileReader(); + reader.onload = function(file) { + const img = new Image(); + img.onload = function () { + const displayWidth = '460'; + const ratio = (this.height / this.width); + img.width = displayWidth; + img.height = (displayWidth * ratio); + $(imageTarget).html(img); + }; + img.src = file.target.result; + $(imageTarget).html(img); }; - img.src = file.target.result; - $('#back_target').html(img); - }; - reader.readAsDataURL(image); - }); + reader.readAsDataURL(image); + }); + }; } -document.addEventListener('DOMContentLoaded', imagePreview); +const frontImagePreview = imagePreviewFunction('#doc_auth_front_image', '#front_target'); +const backImagePreview = imagePreviewFunction('#doc_auth_back_image', '#back_target'); +const selfieImagePreview = imagePreviewFunction('#doc_auth_selfie_image', '#selfie_target'); + document.addEventListener('DOMContentLoaded', frontImagePreview); document.addEventListener('DOMContentLoaded', backImagePreview); +document.addEventListener('DOMContentLoaded', selfieImagePreview); diff --git a/app/services/acuant/acuant_client.rb b/app/services/acuant/acuant_client.rb index 1e30e391a86..ba93cf64a6e 100644 --- a/app/services/acuant/acuant_client.rb +++ b/app/services/acuant/acuant_client.rb @@ -20,7 +20,22 @@ def post_back_image(image:, instance_id:) ).fetch end - def post_images(front_image:, back_image:, instance_id: nil) + def post_selfie(image:, instance_id:) + get_face_image_response = Requests::GetFaceImageRequest.new(instance_id: instance_id).fetch + return get_face_image_response unless get_face_image_response.success? + + facial_match_response = Requests::FacialMatchRequest.new( + selfie_image: image, + document_face_image: get_face_image_response.image, + ).fetch + liveness_response = Requests::LivenessRequest.new(image: image).fetch + + merge_facial_match_and_liveness_response(facial_match_response, liveness_response) + end + + # rubocop:disable Metrics/AbcSize + def post_images(front_image:, back_image:, selfie_image:, + liveness_checking_enabled: nil, instance_id: nil) document = create_document return failure(document.errors.first, document.to_h) unless document.success? @@ -29,26 +44,22 @@ def post_images(front_image:, back_image:, instance_id: nil) back_response = post_back_image(image: back_image, instance_id: instance_id) response = merge_post_responses(front_response, back_response) - check_results(response, instance_id) + results = check_results(response, instance_id) + + if results.success? && liveness_checking_enabled + pii = results.pii_from_doc + selfie_response = post_selfie(image: selfie_image, instance_id: instance_id) + Acuant::Responses::ResponseWithPii.new(selfie_response, pii) + else + results + end end + # rubocop:enable Metrics/AbcSize def get_results(instance_id:) Requests::GetResultsRequest.new(instance_id: instance_id).fetch end - def post_selfie(instance_id:, image:) - get_face_image_response = Requests::GetFaceImageRequest.new(instance_id: instance_id).fetch - return get_face_image_response unless get_face_image_response.success? - - facial_match_response = Requests::FacialMatchRequest.new( - selfie_image: image, - document_face_image: get_face_image_response.image, - ).fetch - liveness_response = Requests::LivenessRequest.new(image: image).fetch - - merge_facial_match_and_liveness_response(facial_match_response, liveness_response) - end - private def merge_post_responses(front_response, back_response) @@ -80,7 +91,7 @@ def check_results(post_response, instance_id) def fetch_doc_auth_results(instance_id) results_response = get_results(instance_id: instance_id) - handle_document_verification_failure(results_response) unless results_response.success? + return handle_document_verification_failure(results_response) unless results_response.success? results_response end diff --git a/app/services/acuant/responses/response_with_pii.rb b/app/services/acuant/responses/response_with_pii.rb new file mode 100644 index 00000000000..bcea99335f4 --- /dev/null +++ b/app/services/acuant/responses/response_with_pii.rb @@ -0,0 +1,19 @@ +module Acuant + module Responses + class ResponseWithPii < Acuant::Response + def initialize(acuant_response, pii) + super( + success: acuant_response.success?, + errors: acuant_response.errors, + exception: acuant_response.exception, + extra: acuant_response.extra, + ) + @pii = pii + end + + def pii_from_doc + @pii + end + end + end +end diff --git a/app/services/doc_auth_mock/doc_auth_mock_client.rb b/app/services/doc_auth_mock/doc_auth_mock_client.rb index 67aa737c55e..ce1f0a84222 100644 --- a/app/services/doc_auth_mock/doc_auth_mock_client.rb +++ b/app/services/doc_auth_mock/doc_auth_mock_client.rb @@ -5,6 +5,7 @@ class << self attr_reader :response_mocks attr_accessor :last_uploaded_front_image attr_accessor :last_uploaded_back_image + attr_accessor :last_uploaded_selfie_image end def self.mock_response!(method:, response:) @@ -16,6 +17,7 @@ def self.reset! @response_mocks = {} @last_uploaded_front_image = nil @last_uploaded_back_image = nil + @last_uploaded_selfie_image = nil end def create_document @@ -39,18 +41,35 @@ def post_back_image(image:, instance_id:) Acuant::Response.new(success: true) end - def post_images(front_image:, back_image:) + def post_selfie(image:, instance_id:) + return mocked_response_for_method(__method__) if method_mocked?(__method__) + + self.class.last_uploaded_selfie_image = image + Acuant::Response.new(success: true) + end + + # rubocop:disable Metrics/AbcSize + def post_images(front_image:, back_image:, selfie_image:, + liveness_checking_enabled: nil, instance_id: nil) return mocked_response_for_method(__method__) if method_mocked?(__method__) document = create_document return document unless document.success? - instance_id = create_document.instance_id + instance_id ||= create_document.instance_id front_response = post_front_image(image: front_image, instance_id: instance_id) back_response = post_back_image(image: back_image, instance_id: instance_id) response = merge_post_responses(front_response, back_response) - check_results(response, instance_id) + results = check_results(response, instance_id) + if results.success? && liveness_checking_enabled + pii = results.pii_from_doc + selfie_response = post_selfie(image: selfie_image, instance_id: instance_id) + Acuant::Responses::ResponseWithPii.new(selfie_response, pii) + else + results + end end + # rubocop:enable Metrics/AbcSize def get_results(instance_id:) return mocked_response_for_method(__method__) if method_mocked?(__method__) @@ -58,12 +77,6 @@ def get_results(instance_id:) ResultResponseBuilder.new(self.class.last_uploaded_back_image).call end - def post_selfie(instance_id:, image:) - return mocked_response_for_method(__method__) if method_mocked?(__method__) - - Acuant::Response.new(success: true) - end - private def method_mocked?(method_name) @@ -86,13 +99,12 @@ def check_results(post_response, instance_id) def fetch_doc_auth_results(instance_id) results_response = get_results(instance_id: instance_id) - handle_document_verification_failure(results_response) unless results_response.success? + return handle_document_verification_failure(results_response) unless results_response.success? results_response end def handle_document_verification_failure(get_results_response) - mark_step_incomplete(:front_image) extra = get_results_response.to_h.merge( notice: I18n.t('errors.doc_auth.general_info'), ) diff --git a/app/services/idv/steps/doc_auth_base_step.rb b/app/services/idv/steps/doc_auth_base_step.rb index 99fbb75711b..3c74fef5b7b 100644 --- a/app/services/idv/steps/doc_auth_base_step.rb +++ b/app/services/idv/steps/doc_auth_base_step.rb @@ -29,6 +29,13 @@ def back_image DataUrlImage.new(flow_params[:back_image_data_url]) end + def selfie_image + return nil unless liveness_checking_enabled? + uploaded_image = flow_params[:selfie_image] + return uploaded_image if uploaded_image.present? + DataUrlImage.new(flow_params[:selfie_image_data_url]) + end + def doc_auth_client @doc_auth_client ||= begin case doc_auth_vendor @@ -124,9 +131,11 @@ def post_images result = doc_auth_client.post_images( front_image: front_image.read, back_image: back_image.read, + selfie_image: selfie_image&.read, + liveness_checking_enabled: liveness_checking_enabled?, ) - add_cost(:acuant_front_image) - add_cost(:acuant_back_image) + # DP: should these cost recordings happen in the doc_auth_client? + add_costs result end @@ -162,6 +171,12 @@ def add_cost(token) Db::ProofingCost::AddUserProofingCost.call(user_id, token) end + def add_costs + add_cost(:acuant_front_image) + add_cost(:acuant_back_image) + add_cost(:acuant_selfie) if liveness_checking_enabled? + end + def sp_session session.fetch(:sp, {}) end @@ -171,9 +186,10 @@ def mark_selfie_step_complete_unless_liveness_checking_is_enabled end def mark_document_capture_or_image_upload_steps_complete - if Figaro.env.document_capture_step_enabled == 'true' + if FeatureManagement.document_capture_step_enabled? mark_step_complete(:front_image) mark_step_complete(:back_image) + mark_step_complete(:selfie) mark_step_complete(:mobile_front_image) mark_step_complete(:mobile_back_image) else diff --git a/app/services/idv/steps/document_capture_step.rb b/app/services/idv/steps/document_capture_step.rb index 95fa6d841a3..bd24f190d2a 100644 --- a/app/services/idv/steps/document_capture_step.rb +++ b/app/services/idv/steps/document_capture_step.rb @@ -15,15 +15,21 @@ def call def handle_document_verification_failure(response) mark_step_incomplete(:document_capture) - extra = response.to_h.merge( - notice: I18n.t('errors.doc_auth.general_info'), - ) + notice = if liveness_checking_enabled? + { notice: I18n.t('errors.doc_auth.document_capture_info_with_selfie_html') } + else + { notice: I18n.t('errors.doc_auth.document_capture_info_html') } + end + extra = response.to_h.merge(notice) failure(response.errors.first, extra) end def form_submit - Idv::DocumentCaptureForm.new.submit(permit(:front_image, :front_image_data_url, - :back_image, :back_image_data_url)) + Idv::DocumentCaptureForm. + new(liveness_checking_enabled: liveness_checking_enabled?). + submit(permit(:front_image, :front_image_data_url, + :back_image, :back_image_data_url, + :selfie_image, :selfie_image_data_url)) end end end diff --git a/app/views/idv/doc_auth/_document_capture_notices.html.erb b/app/views/idv/doc_auth/_document_capture_notices.html.erb new file mode 100644 index 00000000000..04c3ce8b5bb --- /dev/null +++ b/app/views/idv/doc_auth/_document_capture_notices.html.erb @@ -0,0 +1,5 @@ +<% if flow_session[:notice].present? %> +
+ <%= flow_session[:notice].html_safe %> +
+<% end %> diff --git a/app/views/idv/doc_auth/document_capture.html.erb b/app/views/idv/doc_auth/document_capture.html.erb index 8dce08dba36..89429f56bf1 100644 --- a/app/views/idv/doc_auth/document_capture.html.erb +++ b/app/views/idv/doc_auth/document_capture.html.erb @@ -17,6 +17,16 @@ <%= render 'idv/doc_auth/error_messages', flow_session: flow_session %> +

+ <% if liveness_checking_enabled? %> + <%= t('doc_auth.headings.document_capture_with_selfie_html') %> + <% else %> + <%= t('doc_auth.headings.document_capture_html') %> + <% end %> +

+ +<%= render 'idv/doc_auth/document_capture_notices', flow_session: flow_session %> + <%= simple_form_for( :doc_auth, url: url_for, @@ -24,50 +34,62 @@ html: { autocomplete: 'off', role: 'form', class: 'mt2' } ) do |f| %> <%# ---- Front Image ----- %> - <%= render 'idv/doc_auth/front_of_state_id_image' %> - -

- <%= t('doc_auth.headings.upload_front') %> -

- - <%= accordion('totp-info', t('doc_auth.tips.title_html'), - wrapper_css: 'my2 col-12 fs-16p') do %> - <%= render 'idv/doc_auth/tips_and_sample' %> -
- <%= image_tag(asset_url('state-id-sample-front.jpg'), height: 338, width: 450) %> -
- <% end %> - - <%= f.input :front_image_data_url, as: :hidden %> - <%= render 'idv/doc_auth/notices', flow_session: flow_session %> - <%= f.input :front_image, label: false, as: :file, required: true, wrapper_class: 'mt3 sm-col-8' %> -
+
+
+

+ <%= t('doc_auth.headings.upload_front_html') %> +

+ + <%= accordion('totp-info', t('doc_auth.tips.title_html'), + wrapper_css: 'my2 col-12 fs-16p') do %> + <%= render 'idv/doc_auth/tips_and_sample' %> +
+ <%= image_tag(asset_url('state-id-sample-front.jpg'), height: 338, width: 450) %> +
+ <% end %> + + <%= f.input :front_image_data_url, as: :hidden %> + <%= f.input :front_image, label: false, as: :file, required: true, wrapper_class: 'mt3 sm-col-8' %> +
+
<%# ---- Back Image ----- %> - <%= render 'idv/doc_auth/back_of_state_id_image' %> -

- <%= t('doc_auth.headings.upload_back') %> -

+
+
+

+ <%= t('doc_auth.headings.upload_back_html') %> +

+ + <%= accordion('totp-info', t('doc_auth.tips.title_html'), + wrapper_css: 'my2 col-12 fs-16p') do %> + <%= render 'idv/doc_auth/tips_and_sample' %> +
+ <%= image_tag(asset_url('state-id-sample-back.jpg'), height: 338, width: 450) %> +
+ <% end %> + + <%= f.input :back_image_data_url, as: :hidden %> + <%= f.input :back_image, label: false, as: :file, required: true, wrapper_class: 'mt3 sm-col-8' %> +
+
- <%= accordion('totp-info', t('doc_auth.tips.title_html'), - wrapper_css: 'my2 col-12 fs-16p') do %> - <%= render 'idv/doc_auth/tips_and_sample' %> -
- <%= image_tag(asset_url('state-id-sample-back.jpg'), height: 338, width: 450) %> + <%# ---- Selfie ----- %> + <% if liveness_checking_enabled? %> +
+
+

+ <%= t('doc_auth.headings.selfie') %> +

+ + <%= f.input :selfie_image_data_url, as: :hidden %> + <%= f.input :selfie_image, label: false, as: :file, required: true, wrapper_class: 'mt3 sm-col-8' %> +
<% end %> - <%= f.input :back_image_data_url, as: :hidden %> - <%= render 'idv/doc_auth/notices', flow_session: flow_session %> - <%= f.input :back_image, label: false, as: :file, required: true, wrapper_class: 'mt3 sm-col-8' %> -
- - <%# ---- Selfie ----- %> - - - <%# ---- Selfie ----- %> + <%# ---- Submit ----- %>
<%= render 'idv/doc_auth/submit_with_spinner' %> @@ -79,3 +101,4 @@ <%= render 'idv/doc_auth/start_over_or_cancel' %> <%= javascript_pack_tag 'image-preview' %>
+<%= javascript_pack_tag 'document-capture' %> diff --git a/config/locales/doc_auth/en.yml b/config/locales/doc_auth/en.yml index 29f25e08c0a..9c41b17eafc 100644 --- a/config/locales/doc_auth/en.yml +++ b/config/locales/doc_auth/en.yml @@ -22,6 +22,9 @@ en: headings: address: Mailing Address capture_complete: We have verified your state issued ID + document_capture_html: Upload your state‑issued ID + document_capture_with_selfie_html: Upload your state‑issued ID and + a photo of you selfie: Take a selfie. ssn: Please enter your social security number. take_pic_back: Take a photo of the back of your ID @@ -30,8 +33,10 @@ en: text_message: We sent a message to your phone upload: How would you like to upload your state issued ID? upload_back: Upload an image of the back of your state issued ID + upload_back_html: Upload an image of the back of your state‑issued ID upload_from_phone: Take a photo with a mobile phone to upload your ID upload_front: Upload an image of the front of your state issued ID + upload_front_html: Upload an image of the front of your state‑issued ID verify: Please verify your information welcome: We need to verify your identity info: diff --git a/config/locales/doc_auth/es.yml b/config/locales/doc_auth/es.yml index fa31b60c9bf..b9842a945b3 100644 --- a/config/locales/doc_auth/es.yml +++ b/config/locales/doc_auth/es.yml @@ -23,6 +23,9 @@ es: headings: address: Dirección de envio capture_complete: Hemos verificado la identificación emitida por su estado + document_capture_html: Cargue su identificación emitida por el estado + document_capture_with_selfie_html: Cargue su identificación emitida por el estado + y una foto suya selfie: Toma una selfie. ssn: Por favor ingrese su número de seguro social. take_pic_back: Toma una foto de la parte posterior de tu identificación @@ -31,9 +34,13 @@ es: text_message: Enviamos un mensaje a su teléfono upload: "¿Cómo te gustaría subir tu identificación emitida por el estado?" upload_back: Cargue una foto del dorso de su identificación emitida por el estado. + upload_back_html: Cargue una foto del dorso de su identificación emitida por + el estado. upload_from_phone: Tome una foto con un teléfono móvil para cargar su identificación upload_front: Cargue una foto del frente de su identificación emitida por el estado. + upload_front_html: Cargue una foto del frente de su identificación emitida por + el verify: Por favor verifica tu información welcome: Nosotros necesitamos verificar tu identidad info: diff --git a/config/locales/doc_auth/fr.yml b/config/locales/doc_auth/fr.yml index f4c68e77c47..fe5460999c7 100644 --- a/config/locales/doc_auth/fr.yml +++ b/config/locales/doc_auth/fr.yml @@ -23,6 +23,9 @@ fr: headings: address: Adresse mail capture_complete: Nous avons vérifié votre ID émis par l'état + document_capture_html: Téléchargez votre pièce d'identité délivrée par l'État + document_capture_with_selfie_html: Téléchargez votre pièce d'identité officielle + et une photo de vous selfie: Prendre un selfie. ssn: S'il vous plaît entrez votre numéro de sécurité sociale. take_pic_back: Prenez une photo au verso de votre identifiant @@ -32,10 +35,14 @@ fr: upload: Comment voudriez-vous télécharger votre ID émis par l'état? upload_back: S'il vous plaît télécharger une photo du dos de votre ID émis par l'état. + upload_back_html: S'il vous plaît télécharger une photo du dos de votre ID émis + par l'état. upload_from_phone: Prenez une photo avec un téléphone portable pour télécharger votre pièce d'identité upload_front: Veuillez télécharger une photo du recto de votre identifiant émis par l'État. + upload_front_html: Veuillez télécharger une photo du recto de votre identifiant + émis par l'État. verify: S'il vous plaît vérifier vos informations welcome: Nous devons vérifier votre identité info: diff --git a/config/locales/errors/en.yml b/config/locales/errors/en.yml index 7a8dbea23fc..43cd9a9fcec 100644 --- a/config/locales/errors/en.yml +++ b/config/locales/errors/en.yml @@ -24,6 +24,26 @@ en: to validate a document. Please try again later. consent_form: Before you can continue, you must give us permission. Please check the box below and then click continue. + document_capture_info_html: |- +
Please check that:
+ + document_capture_info_with_selfie_html: |- +
Please check that:
+ general_error: We could not read or verify your ID. Try to take and upload new images of both the front and back of your ID. Make sure everything can be easily read, including the barcode. diff --git a/config/locales/errors/es.yml b/config/locales/errors/es.yml index 9d972e3de6b..9f648048df4 100644 --- a/config/locales/errors/es.yml +++ b/config/locales/errors/es.yml @@ -24,6 +24,26 @@ es: intentar validar un documento. Por favor, inténtelo de nuevo más tarde. consent_form: Antes de continuar, debe darnos permiso. Marque la casilla a continuación y luego haga clic en continuar. + document_capture_info_html: |- +
Por favor verifique que:
+ + document_capture_info_with_selfie_html: |- +
Por favor verifique que:
+ general_error: No pudimos leer ni verificar su identificación. Intenta tomar y subir nuevas imágenes de la parte delantera y trasera de tu ID. Asegúrese de que todo se pueda leer fácilmente, incluido el código de barras. diff --git a/config/locales/errors/fr.yml b/config/locales/errors/fr.yml index 6d84b17fa8d..b1113b43214 100644 --- a/config/locales/errors/fr.yml +++ b/config/locales/errors/fr.yml @@ -24,6 +24,26 @@ fr: pouvez essayer de valider un document. Veuillez réessayer plus tard. consent_form: Avant de pouvoir continuer, vous devez nous donner la permission. Veuillez cocher la case ci-dessous puis cliquez sur continuer. + document_capture_info_html: |- +
Veuillez vérifier que:
+ + document_capture_info_with_selfie_html: |- +
Veuillez vérifier que:
+ general_error: Nous n'avons pas pu lire ni vérifier votre identité. Essayez de prendre et de télécharger de nouvelles images des deux côtés de votre ID. Assurez-vous que tout peut être facilement lu, y compris le code à barres. diff --git a/lib/feature_management.rb b/lib/feature_management.rb index dc4c89a53da..137f26a0cfe 100644 --- a/lib/feature_management.rb +++ b/lib/feature_management.rb @@ -112,6 +112,10 @@ def self.doc_capture_polling_enabled? Figaro.env.doc_capture_polling_enabled == 'true' end + def self.document_capture_step_enabled? + Figaro.env.document_capture_step_enabled == 'true' + end + def self.hide_phone_mfa_signup? Figaro.env.hide_phone_mfa_signup == 'true' end diff --git a/spec/features/idv/doc_auth/document_capture_step_spec.rb b/spec/features/idv/doc_auth/document_capture_step_spec.rb index 177707dcf22..d2994280e28 100644 --- a/spec/features/idv/doc_auth/document_capture_step_spec.rb +++ b/spec/features/idv/doc_auth/document_capture_step_spec.rb @@ -7,9 +7,12 @@ let(:max_attempts) { Figaro.env.acuant_max_attempts.to_i } let(:user) { user_with_2fa } + let(:liveness_enabled) { 'false' } before do allow(Figaro.env).to receive(:document_capture_step_enabled). and_return(document_capture_step_enabled) + allow(Figaro.env).to receive(:liveness_checking_enabled). + and_return(liveness_enabled) sign_in_and_2fa_user(user) complete_doc_auth_steps_before_document_capture_step end @@ -25,104 +28,193 @@ context 'when the step is enabled' do let(:document_capture_step_enabled) { 'true' } - it 'is on the correct_page' do - expect(current_path).to eq(idv_doc_auth_document_capture_step) - expect(page).to have_content(t('doc_auth.headings.upload_front')) - expect(page).to have_content(t('doc_auth.headings.upload_back')) - end + context 'when liveness checking is enabled' do + let(:liveness_enabled) { 'true' } - it 'displays tips and sample images' do - expect(page).to have_content(I18n.t('doc_auth.tips.text1')) - expect(page).to have_css('img[src*=state-id-sample-front]') - end + it 'is on the correct_page' do + expect(current_path).to eq(idv_doc_auth_document_capture_step) + expect(page).to have_content(render_html_string(t('doc_auth.headings.upload_front_html'))) + expect(page).to have_content(render_html_string(t('doc_auth.headings.upload_back_html'))) + expect(page).to have_content(t('doc_auth.headings.selfie')) + end - it 'proceeds to the next page with valid info' do - attach_images - click_idv_continue + it 'displays tips and sample images' do + expect(page).to have_content(I18n.t('doc_auth.tips.text1')) + expect(page).to have_css('img[src*=state-id-sample-front]') + end - expect(page).to have_current_path(next_step) - end + it 'proceeds to the next page with valid info' do + attach_images + click_idv_continue - it 'allows the use of a base64 encoded data url representation of the image' do - attach_front_image_data_url - attach_back_image_data_url - click_idv_continue - - expect(page).to have_current_path(next_step) - expect(DocAuthMock::DocAuthMockClient.last_uploaded_front_image).to eq( - doc_auth_front_image_data_url_data, - ) - expect(DocAuthMock::DocAuthMockClient.last_uploaded_back_image).to eq( - doc_auth_back_image_data_url_data, - ) - end + expect(page).to have_current_path(next_step) + end - it 'does not proceed to the next page with invalid info' do - mock_general_doc_auth_client_error(:create_document) - attach_images - click_idv_continue + it 'allows the use of a base64 encoded data url representation of the image' do + attach_front_image_data_url + attach_back_image_data_url + attach_selfie_image_data_url + click_idv_continue - expect(page).to have_current_path(idv_doc_auth_document_capture_step) - end + expect(page).to have_current_path(next_step) + expect(DocAuthMock::DocAuthMockClient.last_uploaded_front_image).to eq( + doc_auth_front_image_data_url_data, + ) + expect(DocAuthMock::DocAuthMockClient.last_uploaded_back_image).to eq( + doc_auth_back_image_data_url_data, + ) + expect(DocAuthMock::DocAuthMockClient.last_uploaded_selfie_image).to eq( + doc_auth_selfie_image_data_url_data, + ) + end - it 'offers in person option on failure' do - enable_in_person_proofing + it 'does not proceed to the next page with invalid info' do + mock_general_doc_auth_client_error(:create_document) + attach_images + click_idv_continue - expect(page).to_not have_link(t('in_person_proofing.opt_in_link'), - href: idv_in_person_welcome_step) + expect(page).to have_current_path(idv_doc_auth_document_capture_step) + end - mock_general_doc_auth_client_error(:create_document) - attach_images - click_idv_continue + it 'offers in person option on failure' do + enable_in_person_proofing - expect(page).to have_link(t('in_person_proofing.opt_in_link'), - href: idv_in_person_welcome_step) - end + expect(page).to_not have_link(t('in_person_proofing.opt_in_link'), + href: idv_in_person_welcome_step) - it 'throttles calls to acuant and allows retry after the attempt window' do - allow(Figaro.env).to receive(:acuant_max_attempts).and_return(max_attempts) - max_attempts.times do + mock_general_doc_auth_client_error(:create_document) attach_images click_idv_continue - expect(page).to have_current_path(next_step) - click_on t('doc_auth.buttons.start_over') - complete_doc_auth_steps_before_document_capture_step + expect(page).to have_link(t('in_person_proofing.opt_in_link'), + href: idv_in_person_welcome_step) end - attach_images - click_idv_continue + it 'throttles calls to acuant and allows retry after the attempt window' do + allow(Figaro.env).to receive(:acuant_max_attempts).and_return(max_attempts) + max_attempts.times do + attach_images + click_idv_continue - expect(page).to have_current_path(idv_session_errors_throttled_path) + expect(page).to have_current_path(next_step) + click_on t('doc_auth.buttons.start_over') + complete_doc_auth_steps_before_document_capture_step + end - Timecop.travel(Figaro.env.acuant_attempt_window_in_minutes.to_i.minutes.from_now) do - sign_in_and_2fa_user(user) - complete_doc_auth_steps_before_document_capture_step attach_images click_idv_continue - expect(page).to have_current_path(next_step) + expect(page).to have_current_path(idv_session_errors_throttled_path) + + Timecop.travel(Figaro.env.acuant_attempt_window_in_minutes.to_i.minutes.from_now) do + sign_in_and_2fa_user(user) + complete_doc_auth_steps_before_document_capture_step + attach_images + click_idv_continue + + expect(page).to have_current_path(next_step) + end + end + + it 'catches network connection errors on post_front_image' do + DocAuthMock::DocAuthMockClient.mock_response!( + method: :post_front_image, + response: Acuant::Response.new( + success: false, + errors: [I18n.t('errors.doc_auth.acuant_network_error')], + ), + ) + + attach_images + click_idv_continue + + expect(page).to have_current_path(idv_doc_auth_document_capture_step) + expect(page).to have_content(I18n.t('errors.doc_auth.acuant_network_error')) end end - it 'catches network connection errors on post_front_image' do - DocAuthMock::DocAuthMockClient.mock_response!( - method: :post_front_image, - response: Acuant::Response.new( - success: false, - errors: [I18n.t('errors.doc_auth.acuant_network_error')], - ), - ) + context 'when liveness checking is not enabled' do + let(:liveness_enabled) { 'false' } + + it 'is on the correct_page, but does not show the selfie upload option' do + expect(current_path).to eq(idv_doc_auth_document_capture_step) + expect(page).to have_content(render_html_string(t('doc_auth.headings.upload_front_html'))) + expect(page).to have_content(render_html_string(t('doc_auth.headings.upload_back_html'))) + expect(page).not_to have_content(t('doc_auth.headings.selfie')) + end + + it 'proceeds to the next page with valid info' do + attach_images(liveness_enabled: false) + click_idv_continue + + expect(page).to have_current_path(next_step) + end + + it 'allows the use of a base64 encoded data url representation of the image' do + attach_front_image_data_url + attach_back_image_data_url + click_idv_continue + + expect(page).to have_current_path(next_step) + expect(DocAuthMock::DocAuthMockClient.last_uploaded_front_image).to eq( + doc_auth_front_image_data_url_data, + ) + expect(DocAuthMock::DocAuthMockClient.last_uploaded_back_image).to eq( + doc_auth_back_image_data_url_data, + ) + expect(DocAuthMock::DocAuthMockClient.last_uploaded_selfie_image).to be_nil + end + + it 'throttles calls to acuant and allows retry after the attempt window' do + allow(Figaro.env).to receive(:acuant_max_attempts).and_return(max_attempts) + max_attempts.times do + attach_images(liveness_enabled: false) + click_idv_continue + + expect(page).to have_current_path(next_step) + click_on t('doc_auth.buttons.start_over') + complete_doc_auth_steps_before_document_capture_step + end + + attach_images(liveness_enabled: false) + click_idv_continue - attach_images - click_idv_continue + expect(page).to have_current_path(idv_session_errors_throttled_path) + + Timecop.travel(Figaro.env.acuant_attempt_window_in_minutes.to_i.minutes.from_now) do + sign_in_and_2fa_user(user) + complete_doc_auth_steps_before_document_capture_step + attach_images(liveness_enabled: false) + click_idv_continue + + expect(page).to have_current_path(next_step) + end + end - expect(page).to have_current_path(idv_doc_auth_document_capture_step) - expect(page).to have_content(I18n.t('errors.doc_auth.acuant_network_error')) + it 'catches network connection errors on post_front_image' do + DocAuthMock::DocAuthMockClient.mock_response!( + method: :post_front_image, + response: Acuant::Response.new( + success: false, + errors: [I18n.t('errors.doc_auth.acuant_network_error')], + ), + ) + + attach_images(liveness_enabled: false) + click_idv_continue + + expect(page).to have_current_path(idv_doc_auth_document_capture_step) + expect(page).to have_content(I18n.t('errors.doc_auth.acuant_network_error')) + end end end def next_step - idv_doc_auth_selfie_step + idv_doc_auth_ssn_step + end + + def render_html_string(html_string) + rendered = Nokogiri::HTML.parse(html_string).text + strip_nbsp(rendered) end end diff --git a/spec/forms/idv/document_capture_form_spec.rb b/spec/forms/idv/document_capture_form_spec.rb new file mode 100644 index 00000000000..00b61f9d8a2 --- /dev/null +++ b/spec/forms/idv/document_capture_form_spec.rb @@ -0,0 +1,128 @@ +require 'rails_helper' + +describe Idv::DocumentCaptureForm do + let(:liveness_enabled) { false } + let(:subject) { Idv::DocumentCaptureForm.new(liveness_checking_enabled: liveness_enabled) } + let(:front_image_data) { 'abc' } + let(:front_image_data_url) { 'data:image/jpeg;base64,abc' } + let(:back_image_data) { 'def' } + let(:back_image_data_url) { 'data:image/jpeg;base64,def' } + let(:selfie_image_data) { 'ghi' } + let(:selfie_image_data_url) { 'data:image/jpeg;base64,ghi' } + + describe '#submit' do + context 'when liveness checking is not enabled' do + context 'when the form has front and back images' do + it 'returns a successful form response' do + result = subject.submit(front_image: front_image_data, + back_image: back_image_data) + + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(true) + expect(result.errors).to be_empty + end + end + + context 'when the form has a front and back data_urls' do + it 'returns a successful form response' do + result = subject.submit(front_image: front_image_data_url, + back_image: back_image_data_url) + + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(true) + expect(result.errors).to be_empty + end + end + end + + context 'when liveness checking is enabled' do + let(:liveness_enabled) { true } + + context 'when the form has front, back, and selfie images' do + it 'returns a successful form response' do + result = subject.submit(front_image: front_image_data, + back_image: back_image_data, + selfie_image: selfie_image_data) + + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(true) + expect(result.errors).to be_empty + end + end + + context 'when the form only has front and back images' do + it 'returns a successful form response' do + result = subject.submit(front_image: front_image_data, + back_image: back_image_data) + + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(subject.errors).to include(:selfie_image) + end + end + + context 'when the form has a front, back, and selfie data_urls' do + it 'returns a successful form response' do + result = subject.submit(front_image: front_image_data_url, + back_image: back_image_data_url, + selfie_image: front_image_data_url) + + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(true) + expect(result.errors).to be_empty + end + end + + context 'when the form only has a front and back data_urls' do + it 'returns a successful form response' do + result = subject.submit(front_image: front_image_data_url, + back_image: back_image_data_url) + + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(subject.errors).to include(:selfie_image) + end + end + end + + context 'when the form has invalid attributes' do + it 'raises an error' do + expect do + subject.submit(front_image: front_image_data, + back_image: back_image_data_url, + foo: 1) + end.to raise_error(ArgumentError, 'foo is an invalid image attribute') + end + end + end + + describe 'presence validations' do + context 'when liveness checking is not enabled' do + it 'is invalid when image and data_url attributes are not present' do + result = subject.submit({}) + + expect(subject).to_not be_valid + expect(subject.errors).to include(:front_image) + expect(subject.errors).to include(:back_image) + expect(subject.errors).not_to include(:selfie_image) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + end + end + + context 'when liveness checking is enabled' do + let(:liveness_enabled) { true } + + it 'is invalid when image and data_url attributes are not present' do + result = subject.submit({}) + + expect(subject).to_not be_valid + expect(subject.errors).to include(:front_image) + expect(subject.errors).to include(:back_image) + expect(subject.errors).to include(:selfie_image) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + end + end + end +end diff --git a/spec/services/acuant/acuant_client_spec.rb b/spec/services/acuant/acuant_client_spec.rb index e5631c7fcb3..a7973cd26f9 100644 --- a/spec/services/acuant/acuant_client_spec.rb +++ b/spec/services/acuant/acuant_client_spec.rb @@ -48,29 +48,96 @@ end describe '#post_images' do - it 'sends an upload image request for the front image' do - instance_id = 'this-is-a-test-instance-id' - url = URI.join( + let(:liveness_enabled) { false } + let(:instance_id) { 'this-is-a-test-instance-id' } + let(:image_upload_url) do + URI.join( Figaro.env.acuant_assure_id_url, "/AssureIDService/Document/#{instance_id}/Image" ) - stub_request(:post, url).with(query: { side: 0, light: 0 }).to_return(body: '', status: 201) - stub_request(:post, url).with(query: { side: 1, light: 0 }).to_return(body: '', status: 201) - results_url = URI.join( + end + let(:front_image_query) { { query: { side: 0, light: 0 } } } + let(:back_image_query) { { query: { side: 1, light: 0 } } } + let(:results_url) do + URI.join( Figaro.env.acuant_assure_id_url, "/AssureIDService/Document/#{instance_id}" ) + end + + before do + # DL image upload stubs + stub_request(:post, image_upload_url).with(front_image_query).to_return(body: '', status: 201) + stub_request(:post, image_upload_url).with(back_image_query).to_return(body: '', status: 201) stub_request(:get, results_url).to_return(body: AcuantFixtures.get_results_response_success) allow(subject).to receive(:create_document).and_return( OpenStruct.new('success?' => true, instance_id: instance_id), ) + end - result = subject.post_images( - front_image: DocAuthImageFixtures.document_front_image, - back_image: DocAuthImageFixtures.document_back_image, - instance_id: instance_id, - ) + context 'with liveness checking enabled' do + let(:get_face_image_url) do + URI.join( + Figaro.env.acuant_assure_id_url, + "/AssureIDService/Document/#{instance_id}/Field/Image?key=Photo", + ) + end + let(:facial_match_url) { URI.join(Figaro.env.acuant_facial_match_url, '/api/v1/facematch') } + let(:liveness_url) { URI.join(Figaro.env.acuant_passlive_url, '/api/v1/liveness') } + let(:liveness_enabled) { true } - expect(result.success?).to eq(true) + it 'sends an upload image request for the front, back, and selfie images' do + # Selfie stubs + stub_request(:get, get_face_image_url). + to_return(body: AcuantFixtures.get_face_image_response) + stub_request(:post, facial_match_url). + to_return(body: AcuantFixtures.facial_match_response_success) + stub_request(:post, liveness_url). + to_return(body: AcuantFixtures.liveness_response_success) + + result = subject.post_images( + front_image: DocAuthImageFixtures.document_front_image, + back_image: DocAuthImageFixtures.document_back_image, + selfie_image: DocAuthImageFixtures.selfie_image, + liveness_checking_enabled: liveness_enabled, + instance_id: instance_id, + ) + + expect(result.success?).to eq(true) + expect(result.class).to eq(Acuant::Responses::ResponseWithPii) + end + end + + context 'with liveness checking disabled' do + it 'sends an upload image request for the front and back DL images' do + result = subject.post_images( + front_image: DocAuthImageFixtures.document_front_image, + back_image: DocAuthImageFixtures.document_back_image, + selfie_image: DocAuthImageFixtures.selfie_image, + liveness_checking_enabled: liveness_enabled, + instance_id: instance_id, + ) + + expect(result.success?).to eq(true) + expect(result.class).to eq(Acuant::Responses::GetResultsResponse) + end + end + + context 'when the results return failure' do + it 'returns a FormResponse with failure' do + url = URI.join(Figaro.env.acuant_assure_id_url, "/AssureIDService/Document/#{instance_id}") + stub_request(:get, url).to_return(body: AcuantFixtures.get_results_response_failure) + + result = subject.post_images( + front_image: DocAuthImageFixtures.document_front_image, + back_image: DocAuthImageFixtures.document_back_image, + selfie_image: DocAuthImageFixtures.selfie_image, + liveness_checking_enabled: liveness_enabled, + instance_id: instance_id, + ) + + expect(result.success?).to eq(false) + expect(result).to be_kind_of(FormResponse) + end end end @@ -157,6 +224,7 @@ expect(result.success?).to eq(true) expect(result.errors).to eq([]) + expect(result.class).to eq(Acuant::Response) expect(get_face_stub).to have_been_requested expect(facial_match_stub).to have_been_requested expect(liveness_stub).to have_been_requested diff --git a/spec/services/acuant_mock/acuant_mock_client_spec.rb b/spec/services/acuant_mock/acuant_mock_client_spec.rb index b1f7ce1120f..b5daaca5cdf 100644 --- a/spec/services/acuant_mock/acuant_mock_client_spec.rb +++ b/spec/services/acuant_mock/acuant_mock_client_spec.rb @@ -109,4 +109,32 @@ expect(described_class.new.create_document).to_not eq('Create doc test') end + + context 'when checking results gives a failure' do + let(:instance_id) { '1234567890' } + before do + DocAuthMock::DocAuthMockClient.mock_response!( + method: :get_results, + response: OpenStruct.new( + 'success?' => false, + instance_id: instance_id, + errors: [ + { back_image: 'blurry' }, + ], + ), + ) + end + + it 'returns a failure response if the results failed' do + post_images_response = client.post_images( + instance_id: instance_id, + front_image: DocAuthImageFixtures.document_front_image, + back_image: DocAuthImageFixtures.document_back_image, + selfie_image: nil, + ) + + expect(post_images_response.success?).to eq(false) + expect(post_images_response).to be_kind_of(FormResponse) + end + end end diff --git a/spec/support/features/doc_auth_helper.rb b/spec/support/features/doc_auth_helper.rb index e1fafa6731b..7d0e40c8d6d 100644 --- a/spec/support/features/doc_auth_helper.rb +++ b/spec/support/features/doc_auth_helper.rb @@ -188,9 +188,10 @@ def mock_general_doc_auth_client_error(method) ) end - def attach_images + def attach_images(liveness_enabled: true) attach_file 'doc_auth_front_image', 'app/assets/images/logo.png' attach_file 'doc_auth_back_image', 'app/assets/images/logo.png' + attach_file 'doc_auth_selfie_image', 'app/assets/images/logo.png' if liveness_enabled end def attach_front_image_data_url @@ -214,7 +215,19 @@ def doc_auth_back_image_data_url end def doc_auth_back_image_data_url_data - Base64.decode64(doc_auth_front_image_data_url.split(',').last) + Base64.decode64(doc_auth_back_image_data_url.split(',').last) + end + + def attach_selfie_image_data_url + page.find('#doc_auth_selfie_image_data_url', visible: false).set(doc_auth_selfie_image_data_url) + end + + def doc_auth_selfie_image_data_url + File.read('spec/support/fixtures/doc_auth_selfie_image_data_url.data') + end + + def doc_auth_selfie_image_data_url_data + Base64.decode64(doc_auth_selfie_image_data_url.split(',').last) end def attach_image diff --git a/spec/support/features/step_tags_helper.rb b/spec/support/features/step_tags_helper.rb index 8131446fd35..48c4e2f1123 100644 --- a/spec/support/features/step_tags_helper.rb +++ b/spec/support/features/step_tags_helper.rb @@ -3,5 +3,10 @@ module StripTagsHelper def strip_tags(*args) ActionController::Base.helpers.strip_tags(*args) end + + def strip_nbsp(text) + nbsp = Nokogiri::HTML.parse(' ').text + text.gsub(/ /, ' ').gsub(/#{nbsp}/, ' ') + end end end diff --git a/spec/support/fixtures/doc_auth_selfie_image_data_url.data b/spec/support/fixtures/doc_auth_selfie_image_data_url.data new file mode 100644 index 00000000000..ca0ca47272b --- /dev/null +++ b/spec/support/fixtures/doc_auth_selfie_image_data_url.data @@ -0,0 +1 @@ +data:image/jpeg;base64,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