Skip to content
22 changes: 20 additions & 2 deletions app/forms/idv/document_capture_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 28 additions & 43 deletions app/javascript/packs/image-preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
43 changes: 27 additions & 16 deletions app/services/acuant/acuant_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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:,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can drop the instance ID here, right?

liveness_checking_enabled: nil, instance_id: nil)
document = create_document
return failure(document.errors.first, document.to_h) unless document.success?

Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this line is uncovered. I think it makes sense for us to drop in a unit test for what happens if posting the selfie fails.

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)
Expand Down Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions app/services/acuant/responses/response_with_pii.rb
Original file line number Diff line number Diff line change
@@ -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
34 changes: 23 additions & 11 deletions app/services/doc_auth_mock/doc_auth_mock_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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:)
Expand All @@ -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
Expand All @@ -39,31 +41,42 @@ 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__)

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)
Expand All @@ -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'),
)
Expand Down
22 changes: 19 additions & 3 deletions app/services/idv/steps/doc_auth_base_step.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
16 changes: 11 additions & 5 deletions app/services/idv/steps/document_capture_step.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions app/views/idv/doc_auth/_document_capture_notices.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<% if flow_session[:notice].present? %>
<div>
<%= flow_session[:notice].html_safe %>
</div>
<% end %>
Loading