Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f0d1d49
resolve merge conflicts
amirbey Jun 27, 2025
7a131cf
re-apply fixes
amirbey Jun 6, 2025
393a0d6
TrueID to document detected id type
amirbey Jun 6, 2025
2e07da0
update spec to have id_doc_type
amirbey Jun 6, 2025
8bd7b76
update image uploads controller to log id_doc_type
amirbey Jun 6, 2025
f0381cb
add id_doc_type to analytics spec
amirbey Jun 6, 2025
4ff33bf
resolve merge conflict
amirbey Jul 1, 2025
299a072
add fields back to pii redact struct and remove validations of unrequ…
amirbey Jun 7, 2025
f74a5c0
add req'd passport fields
amirbey Jun 7, 2025
d3cd898
update spec to test with invalid passport
amirbey Jun 7, 2025
a31f9ce
test docv passport on hybrid mobile
amirbey Jun 7, 2025
4819691
happy linting
amirbey Jun 7, 2025
61be165
pii exists
amirbey Jun 7, 2025
a5c52ff
normailize yaml
amirbey Jun 7, 2025
47369de
remove comments
amirbey Jun 16, 2025
961c034
changelog: Upcoming Features, Document Authentication, DocV with pass…
amirbey Jun 16, 2025
ce948e2
unused env var
amirbey Jun 17, 2025
e53baf9
use to_return_json
amirbey Jun 17, 2025
b54fd87
fix to check passport
amirbey Jun 26, 2025
ba9889b
update spec stub
amirbey Jun 27, 2025
dc25b55
move docv_document_type
amirbey Jun 27, 2025
7cb1c91
remove comment
amirbey Jun 27, 2025
27a1270
always store mrz_status in session result
amirbey Jul 2, 2025
a9529eb
determine_mrz_status does not exist
amirbey Jul 2, 2025
856fdf4
update argument
amirbey Jul 2, 2025
c921552
comment if passport requested
amirbey Jul 2, 2025
5106cc2
update spec stub
amirbey Jul 2, 2025
ee342fb
remove comments
amirbey Jul 2, 2025
ab50d14
rename fixture from pass.json to license_pass.json
amirbey Jul 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/controllers/concerns/idv/doc_auth_vendor_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ def doc_auth_vendor
bucket = choose_non_socure_bucket
elsif resolved_authn_context_result.facial_match?
bucket = ab_test_bucket(:DOC_AUTH_SELFIE_VENDOR)
elsif idv_session.passport_allowed
bucket = ab_test_bucket(:DOC_AUTH_PASSPORT_VENDOR)
else
bucket = ab_test_bucket(:DOC_AUTH_VENDOR)
end
Expand All @@ -25,6 +27,7 @@ def doc_auth_vendor
DocAuthRouter.doc_auth_vendor_for_bucket(
bucket,
selfie: resolved_authn_context_result.facial_match?,
passport_allowed: idv_session.passport_allowed,
)
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def show
redirect_url: idv_hybrid_mobile_socure_document_capture_update_url,
language: I18n.locale,
liveness_checking_required: resolved_authn_context_result.facial_match?,
passport_requested: document_capture_session.passport_requested?,
)
timer = JobHelpers::Timer.new
document_response = timer.time('vendor_request') do
Expand Down
1 change: 1 addition & 0 deletions app/controllers/idv/socure/document_capture_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def show
redirect_url: idv_socure_document_capture_update_url,
language: I18n.locale,
liveness_checking_required: resolved_authn_context_result.facial_match?,
passport_requested: document_capture_session.passport_requested?,
)
timer = JobHelpers::Timer.new
document_response = timer.time('vendor_request') do
Expand Down
14 changes: 8 additions & 6 deletions app/controllers/idv/welcome_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ class WelcomeController < ApplicationController
before_action :confirm_step_allowed
before_action :confirm_not_rate_limited
before_action :cancel_previous_in_person_enrollments, only: :show
before_action :update_doc_auth_vendor
before_action :update_passport_allowed,
only: :show,
if: -> { IdentityConfig.store.doc_auth_passports_enabled }
before_action :update_doc_auth_vendor

def show
idv_session.proofing_started_at ||= Time.zone.now.iso8601
Expand Down Expand Up @@ -85,9 +85,12 @@ def update_doc_auth_vendor
end

def update_passport_allowed
return if !IdentityConfig.store.doc_auth_passports_enabled
return if resolved_authn_context_result.facial_match?
return if doc_auth_vendor == Idp::Constants::Vendors::SOCURE
if !IdentityConfig.store.doc_auth_passports_enabled ||
resolved_authn_context_result.facial_match?
idv_session.passport_allowed = nil
return
end

idv_session.passport_allowed ||= begin
if dos_passport_api_healthy?(analytics:)
(ab_test_bucket(:DOC_AUTH_PASSPORT) == :passport_allowed)
Expand All @@ -96,8 +99,7 @@ def update_passport_allowed
end

def passport_status
if resolved_authn_context_result.facial_match? ||
doc_auth_vendor == Idp::Constants::Vendors::SOCURE
if resolved_authn_context_result.facial_match?
idv_session.passport_allowed = nil
end

Expand Down
24 changes: 9 additions & 15 deletions app/forms/idv/api_image_upload_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def submit

client_response = nil
doc_pii_response = nil
passport_response = nil
mrz_response = nil

if form_response.success?
client_response = post_images_to_client
Expand All @@ -48,7 +48,7 @@ def submit
if client_response.success?
doc_pii_response = validate_pii_from_doc(client_response)
if doc_pii_response.success? && passport_submittal
passport_response = validate_mrz(client_response)
mrz_response = validate_mrz(client_response)
end
end
end
Expand All @@ -57,13 +57,12 @@ def submit
form_response:,
client_response:,
doc_pii_response:,
passport_response:,
mrz_response:,
)

# Store PII and MRZ status after all validations are complete
if client_response&.success? && doc_pii_response&.success?
mrz_status = determine_mrz_status(passport_response)
store_pii(client_response, mrz_status)
store_pii(client_response, mrz_response)
end

# if there is no client_response, there was no submission attempt
Expand Down Expand Up @@ -199,6 +198,7 @@ def validate_pii_from_doc(client_response)
side_classification = doc_side_classification(client_response)
response_with_classification =
response.to_h.merge(side_classification)
.merge(id_doc_type: client_response.pii_from_doc.id_doc_type)

analytics.idv_doc_auth_submitted_pii_validation(**response_with_classification)

Expand Down Expand Up @@ -282,14 +282,14 @@ def processed_selfie_attempts_data
{ selfie_attempts: past_selfie_count + processed_selfie_count }
end

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

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

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

client_response
end
Expand Down Expand Up @@ -492,8 +492,8 @@ def update_funnel(client_response)
end
end

def store_pii(client_response, mrz_status = nil)
document_capture_session.store_result_from_response(client_response, mrz_status:)
def store_pii(client_response, mrz_response)
document_capture_session.store_result_from_response(client_response, mrz_response:)
end

def user_id
Expand Down Expand Up @@ -584,11 +584,5 @@ def store_failed_images(client_response, doc_pii_response)
def image_resubmission_check?
IdentityConfig.store.doc_auth_check_failed_image_resubmission_enabled
end

def determine_mrz_status(passport_response)
return :not_processed unless passport_submittal && passport_response
return :pass if passport_response.success?
:failed
end
end
end
1 change: 1 addition & 0 deletions app/forms/idv/doc_pii_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def submit
extra: {
pii_like_keypaths: self.class.pii_like_keypaths(document_type: id_doc_type),
attention_with_barcode: attention_with_barcode?,
id_doc_type:,
id_issued_status: pii_from_doc[:state_id_issued].present? ? 'present' : 'missing',
id_expiration_status: pii_from_doc[:state_id_expiration].present? ? 'present' : 'missing',
passport_issued_status: pii_from_doc[:passport_issued].present? ? 'present' : 'missing',
Expand Down
12 changes: 2 additions & 10 deletions app/forms/idv/doc_pii_passport.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,22 @@ module Idv
class DocPiiPassport
include ActiveModel::Model

validates :birth_place,
:passport_issued,
:nationality_code,
:mrz,
validates :mrz,
presence: { message: proc { I18n.t('doc_auth.errors.general.no_liveness') } }

validates :issuing_country_code,
:nationality_code,
inclusion: {
in: 'USA', message: proc { I18n.t('doc_auth.errors.general.no_liveness') }
}

validate :passport_expired?

attr_reader :birth_place, :passport_expiration, :passport_issued,
:issuing_country_code, :nationality_code, :mrz
attr_reader :passport_expiration, :issuing_country_code, :mrz

def initialize(pii:)
@pii_from_doc = pii
@birth_place = pii[:birth_place]
@passport_expiration = pii[:passport_expiration]
@passport_issued = pii[:passport_issued]
@issuing_country_code = pii[:issuing_country_code]
@nationality_code = pii[:nationality_code]
@mrz = pii[:mrz]
end

Expand Down
74 changes: 68 additions & 6 deletions app/jobs/socure_docv_results_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ def perform(document_capture_session_uuid:, async: true, docv_transaction_token_
last_doc_auth_result = docv_result_response.extra_attributes.dig(:decision, :value)
document_capture_session.update!(last_doc_auth_result:) if last_doc_auth_result

mrz_response = nil

if docv_result_response.success?
doc_pii_response = Idv::DocPiiForm.new(pii: docv_result_response.pii_from_doc.to_h).submit
log_pii_validation(doc_pii_response:)
Expand All @@ -46,10 +48,27 @@ def perform(document_capture_session_uuid:, async: true, docv_transaction_token_
record_attempt(docv_result_response:, doc_pii_response:)
return
end

if document_capture_session.passport_requested?
mrz_response = validate_mrz(doc_pii_response)
unless mrz_response.success?
document_capture_session.store_failed_auth_data(
doc_auth_success: true,
selfie_status: docv_result_response.selfie_status,
errors: { passport: 'failed' },
front_image_fingerprint: nil,
back_image_fingerprint: nil,
passport_image_fingerprint: nil,
selfie_image_fingerprint: nil,
mrz_status: :failed,
)
return
end
end
end

record_attempt(docv_result_response:, doc_pii_response:)
document_capture_session.store_result_from_response(docv_result_response)
document_capture_session.store_result_from_response(docv_result_response, mrz_response:)
end

private
Expand Down Expand Up @@ -149,8 +168,8 @@ def attempts_api_tracker
def log_verification_request(docv_result_response:, vendor_request_time_in_ms:)
analytics.idv_socure_verification_data_requested(
**docv_result_response.to_h.merge(
submit_attempts: rate_limiter&.attempts,
remaining_submit_attempts: rate_limiter&.remaining_count,
submit_attempts:,
remaining_submit_attempts:,
vendor_request_time_in_ms:,
async:,
pii_like_keypaths: [[:pii]],
Expand All @@ -162,8 +181,8 @@ def log_verification_request(docv_result_response:, vendor_request_time_in_ms:)
def log_pii_validation(doc_pii_response:)
analytics.idv_doc_auth_submitted_pii_validation(
**doc_pii_response.to_h.merge(
submit_attempts: rate_limiter&.attempts,
remaining_submit_attempts: rate_limiter&.remaining_count,
submit_attempts:,
remaining_submit_attempts:,
flow_path: nil,
liveness_checking_required: nil,
),
Expand All @@ -172,7 +191,7 @@ def log_pii_validation(doc_pii_response:)

def socure_document_verification_result
DocAuth::Socure::Requests::DocvResultRequest.new(
customer_user_id: document_capture_session&.user&.uuid,
customer_user_id: user_uuid,
document_capture_session_uuid:,
docv_transaction_token_override:,
user_email: document_capture_session&.user&.last_sign_in_email_address&.email,
Expand Down Expand Up @@ -212,4 +231,47 @@ def doc_escrow_name
def doc_escrow_key
Base64.strict_encode64(SecureRandom.bytes(32))
end

def validate_mrz(doc_pii_response)
id_type = doc_pii_response.extra[:id_doc_type]
unless id_type == 'passport'
return DocAuth::Response.new(
success: false,
errors: { passport: "Cannot validate MRZ for id type: #{id_type}" },
)
end

mrz_client = Rails.env.development? ?
DocAuth::Mock::DosPassportApiClient.new :
DocAuth::Dos::Requests::MrzRequest.new(mrz: doc_pii_response.pii_from_doc[:mrz])
response = mrz_client.fetch

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

response
end

def document_type
@document_type ||= document_capture_session.passport_requested? \
? 'Passport' : 'DriversLicense'
end

def user_uuid
@user_uuid ||= document_capture_session.user&.uuid
end

def submit_attempts
rate_limiter&.attempts
end

def remaining_submit_attempts
rate_limiter&.remaining_count
end
end
13 changes: 10 additions & 3 deletions app/models/document_capture_session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def load_result

# @param doc_auth_response [DocAuth::Response]
# @param mrz_status [Symbol, nil] MRZ validation status for passport documents
def store_result_from_response(doc_auth_response, mrz_status: nil)
def store_result_from_response(doc_auth_response, mrz_response: nil)
session_result = load_result || DocumentCaptureSessionResult.new(
id: generate_result_id,
)
Expand All @@ -32,7 +32,7 @@ def store_result_from_response(doc_auth_response, mrz_status: nil)
session_result.doc_auth_success = doc_auth_response.doc_auth_success?
session_result.selfie_status = doc_auth_response.selfie_status
session_result.errors = doc_auth_response.errors
session_result.mrz_status = mrz_status if mrz_status
session_result.mrz_status = determine_mrz_status(mrz_response)

EncryptedRedisStructStorage.store(
session_result,
Expand All @@ -45,7 +45,7 @@ def store_result_from_response(doc_auth_response, mrz_status: nil)
def store_failed_auth_data(front_image_fingerprint:, back_image_fingerprint:,
passport_image_fingerprint:, selfie_image_fingerprint:,
doc_auth_success:, selfie_status:,
errors: nil)
errors: nil, mrz_status: :not_processed)
session_result = load_result || DocumentCaptureSessionResult.new(
id: generate_result_id,
)
Expand All @@ -60,6 +60,7 @@ def store_failed_auth_data(front_image_fingerprint:, back_image_fingerprint:,
session_result.add_failed_selfie_image!(selfie_image_fingerprint) if selfie_status == :fail

session_result.errors = errors
session_result.mrz_status = mrz_status

EncryptedRedisStructStorage.store(
session_result,
Expand Down Expand Up @@ -120,4 +121,10 @@ def passport_requested?
def generate_result_id
self.result_id = SecureRandom.uuid
end

def determine_mrz_status(mrz_response)
return :not_processed unless mrz_response

mrz_response.success? ? :pass : :failed
end
end
3 changes: 3 additions & 0 deletions app/services/analytics_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2141,6 +2141,7 @@ def idv_doc_auth_submitted_image_upload_vendor(
# @param [Integer] remaining_submit_attempts (previously called "remaining_attempts")
# @param ["hybrid","standard"] flow_path Document capture user flow
# @param [Boolean] liveness_checking_required Whether or not the selfie is required
# @param [String] id_doc_type Document type detected by the vendor
# @param ["present","missing"] id_issued_status Status of state_id_issued field presence
# @param ["present","missing"] id_expiration_status Status of state_id_expiration field presence
# @param ["present","missing"] passport_issued_status Status of passport_issued field presence
Expand All @@ -2160,6 +2161,7 @@ def idv_doc_auth_submitted_pii_validation(
flow_path:,
liveness_checking_required:,
attention_with_barcode:,
id_doc_type:,
id_issued_status:,
id_expiration_status:,
passport_issued_status:,
Expand All @@ -2183,6 +2185,7 @@ def idv_doc_auth_submitted_pii_validation(
error_details:,
user_id:,
attention_with_barcode:,
id_doc_type:,
id_issued_status:,
id_expiration_status:,
passport_issued_status:,
Expand Down
2 changes: 1 addition & 1 deletion app/services/doc_auth/mock/dos_passport_api_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module DocAuth
module Mock
class DosPassportApiClient
def initialize(mock_client_response)
def initialize(mock_client_response = nil)
@mock_client_response = mock_client_response
end

Expand Down
Loading