-
Notifications
You must be signed in to change notification settings - Fork 166
LG-10427 prevent doc image resubmission validation #9177
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
252e55c
e3eeaaf
e830693
4dd960a
6ef091a
ade4e97
b858847
f893126
7d7de5a
fbc7f95
0afd669
9695888
2944912
e348fe0
2703c19
53256b2
28c2a4f
48d2bf6
d5a1c5c
2f26c4e
3c4afc6
25ea516
64fb0b8
e224903
f7636d5
11ac7c2
ae28a46
4363619
1ef09cc
ca56d6d
aa69851
0b89255
8f01682
41cf775
7be6ffe
0b5e64c
f1625f0
2790ebb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ class ApiImageUploadForm | |
| validates_presence_of :document_capture_session | ||
|
|
||
| validate :validate_images | ||
| validate :validate_duplicate_images, if: :image_resubmission_check? | ||
| validate :limit_if_rate_limited | ||
|
|
||
| def initialize(params, service_provider:, analytics: nil, | ||
|
|
@@ -41,8 +42,9 @@ def submit | |
| doc_pii_response: doc_pii_response, | ||
| ) | ||
|
|
||
| failed_fingerprints = store_failed_images(client_response, doc_pii_response) | ||
| response.extra[:failed_image_fingerprints] = failed_fingerprints | ||
| track_event(response) | ||
|
|
||
| response | ||
| end | ||
|
|
||
|
|
@@ -92,7 +94,6 @@ def post_images_to_client | |
| client_response: response, | ||
| vendor_request_time_in_ms: timer.results['vendor_request'], | ||
| ) | ||
|
|
||
| response | ||
| end | ||
|
|
||
|
|
@@ -122,7 +123,8 @@ def validate_pii_from_doc(client_response) | |
| end | ||
|
|
||
| def extra_attributes | ||
| return @extra_attributes if defined?(@extra_attributes) | ||
| return @extra_attributes if defined?(@extra_attributes) && | ||
| @extra_attributes&.dig('attempts') == attempts | ||
| @extra_attributes = { | ||
| attempts: attempts, | ||
| remaining_attempts: remaining_attempts, | ||
|
|
@@ -131,17 +133,26 @@ def extra_attributes | |
| flow_path: params[:flow_path], | ||
| } | ||
|
|
||
| @extra_attributes[:front_image_fingerprint] = front_image_fingerprint | ||
| @extra_attributes[:back_image_fingerprint] = back_image_fingerprint | ||
| @extra_attributes.merge!(getting_started_ab_test_analytics_bucket) | ||
| @extra_attributes | ||
| end | ||
|
|
||
| def front_image_fingerprint | ||
| return @front_image_fingerprint if @front_image_fingerprint | ||
| if readable?(:front) | ||
| @extra_attributes[:front_image_fingerprint] = | ||
| @front_image_fingerprint = | ||
| Digest::SHA256.urlsafe_base64digest(front_image_bytes) | ||
| end | ||
| end | ||
|
|
||
| def back_image_fingerprint | ||
| return @back_image_fingerprint if @back_image_fingerprint | ||
| if readable?(:back) | ||
| @extra_attributes[:back_image_fingerprint] = | ||
| @back_image_fingerprint = | ||
| Digest::SHA256.urlsafe_base64digest(back_image_bytes) | ||
| end | ||
|
|
||
| @extra_attributes.merge!(getting_started_ab_test_analytics_bucket) | ||
| end | ||
|
|
||
| def remaining_attempts | ||
|
|
@@ -199,8 +210,31 @@ def validate_images | |
| end | ||
| end | ||
|
|
||
| def validate_duplicate_images | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🙌🏻 |
||
| capture_result = document_capture_session&.load_result | ||
| return unless capture_result | ||
| error_sides = [] | ||
| if capture_result&.failed_front_image?(front_image_fingerprint) | ||
| errors.add( | ||
| :front, t('doc_auth.errors.doc.resubmit_failed_image'), type: :duplicate_image | ||
| ) | ||
| error_sides << 'front' | ||
| end | ||
|
|
||
| if capture_result&.failed_back_image?(back_image_fingerprint) | ||
| errors.add( | ||
| :back, t('doc_auth.errors.doc.resubmit_failed_image'), type: :duplicate_image | ||
| ) | ||
| error_sides << 'back' | ||
| end | ||
| unless error_sides.empty? | ||
| analytics.idv_doc_auth_failed_image_resubmitted( | ||
| side: error_sides.length == 2 ? 'both' : error_sides[0], **extra_attributes, | ||
|
eileen-nava marked this conversation as resolved.
Outdated
|
||
| ) | ||
| end | ||
| end | ||
|
|
||
| def limit_if_rate_limited | ||
| return unless document_capture_session | ||
| return unless rate_limited? | ||
|
|
||
| errors.add(:limit, t('errors.doc_auth.rate_limited_heading'), type: :rate_limited) | ||
|
|
@@ -367,5 +401,53 @@ def track_event(response) | |
| failure_reason: response.errors&.except(:hints)&.presence, | ||
| ) | ||
| end | ||
|
|
||
| ## | ||
| # Store failed image fingerprints in document_capture_session_result | ||
|
eileen-nava marked this conversation as resolved.
Outdated
|
||
| # when client_response is not successful and not a network error | ||
| # ( http status except handled status 438, 439, 440 ) or doc_pii_response is not successful. | ||
| # @param [Object] client_response | ||
| # @param [Object] doc_pii_response | ||
| # @return [Object] latest failed fingerprints | ||
| def store_failed_images(client_response, doc_pii_response) | ||
| unless image_resubmission_check? | ||
| return { | ||
| front: [], | ||
| back: [], | ||
| } | ||
| end | ||
| # doc auth failed due to non network error or doc_pii is not valid | ||
| if client_response && !client_response.success? && !client_response.network_error? | ||
| errors_hash = client_response.errors&.to_h || {} | ||
| ## assume both sides' error presents or both sides' error missing | ||
| failed_front_fingerprint = extra_attributes[:front_image_fingerprint] | ||
| failed_back_fingerprint = extra_attributes[:back_image_fingerprint] | ||
| ## not both sides' error present nor both sides' error missing | ||
| ## equivalent to: only one side error presents | ||
| only_one_side_error = errors_hash[:front]&.present? ^ errors_hash[:back]&.present? | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for leaving this comment. At first the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @eileen-nava , have to google to find out this XOR logic operator. |
||
| if only_one_side_error | ||
| ## find which side is missing | ||
| failed_front_fingerprint = nil unless errors_hash[:front]&.present? | ||
| failed_back_fingerprint = nil unless errors_hash[:back]&.present? | ||
| end | ||
| document_capture_session. | ||
| store_failed_auth_image_fingerprint(failed_front_fingerprint, failed_back_fingerprint) | ||
| elsif doc_pii_response && !doc_pii_response.success? | ||
| document_capture_session.store_failed_auth_image_fingerprint( | ||
| extra_attributes[:front_image_fingerprint], | ||
| extra_attributes[:back_image_fingerprint], | ||
| ) | ||
| end | ||
| # retrieve updated data from session | ||
| captured_result = document_capture_session&.load_result | ||
| { | ||
| front: captured_result&.failed_front_image_fingerprints || [], | ||
| back: captured_result&.failed_back_image_fingerprints || [], | ||
| } | ||
| end | ||
|
|
||
| def image_resubmission_check? | ||
| IdentityConfig.store.doc_auth_check_failed_image_resubmission_enabled | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -62,6 +62,16 @@ interface ImageAnalyticsPayload { | |
| * capture functionality | ||
| */ | ||
| acuantCaptureMode?: AcuantCaptureMode; | ||
|
|
||
| /** | ||
| * Fingerprint of the image, base64 encoded SHA-256 digest | ||
|
eileen-nava marked this conversation as resolved.
Outdated
|
||
| */ | ||
| fingerprint: string | null; | ||
|
|
||
| /** | ||
| * | ||
| */ | ||
| failedImageResubmission: boolean; | ||
| } | ||
|
|
||
| interface AcuantImageAnalyticsPayload extends ImageAnalyticsPayload { | ||
|
|
@@ -75,6 +85,7 @@ interface AcuantImageAnalyticsPayload extends ImageAnalyticsPayload { | |
| sharpnessScoreThreshold: number; | ||
| isAssessedAsBlurry: boolean; | ||
| assessment: AcuantImageAssessment; | ||
| isAssessedAsUnsupported: boolean; | ||
| } | ||
|
|
||
| interface AcuantCaptureProps { | ||
|
|
@@ -178,6 +189,31 @@ export function getNormalizedAcuantCaptureFailureMessage( | |
| } | ||
| } | ||
|
|
||
| function getFingerPrint(file: File): Promise<string | null> { | ||
| return new Promise((resolve) => { | ||
| const reader = new FileReader(); | ||
| reader.onload = () => { | ||
| const dataBuffer = reader.result; | ||
| window.crypto.subtle | ||
| .digest('SHA-256', dataBuffer as ArrayBuffer) | ||
| .then((arrayBuffer) => { | ||
| const digestArray = new Uint8Array(arrayBuffer); | ||
| const strDigest = digestArray.reduce( | ||
| (data, byte) => data + String.fromCharCode(byte), | ||
| '', | ||
| ); | ||
| const base64String = window.btoa(strDigest); | ||
| const urlSafeBase64String = base64String | ||
| .replace(/\+/g, '-') | ||
| .replace(/\//g, '_') | ||
| .replace(/=+$/, ''); | ||
| resolve(urlSafeBase64String); | ||
| }) | ||
| .catch(() => null); | ||
| }; | ||
| reader.readAsArrayBuffer(file); | ||
| }); | ||
| } | ||
| function getImageDimensions(file: File): Promise<{ width: number | null; height: number | null }> { | ||
| let objectURL: string; | ||
| return file.type.indexOf('image/') === 0 | ||
|
|
@@ -196,6 +232,22 @@ function getImageDimensions(file: File): Promise<{ width: number | null; height: | |
| : Promise.resolve({ width: null, height: null }); | ||
| } | ||
|
|
||
| function getImageMetadata( | ||
| file: File, | ||
| ): Promise<{ width: number | null; height: number | null; fingerprint: string | null }> { | ||
| const dimension = getImageDimensions(file); | ||
| const fingerprint = getFingerPrint(file); | ||
| return new Promise<{ width: number | null; height: number | null; fingerprint: string | null }>( | ||
| function (resolve) { | ||
| Promise.all([dimension, fingerprint]) | ||
| .then((results) => { | ||
| resolve({ width: results[0].width, height: results[0].height, fingerprint: results[1] }); | ||
| }) | ||
| .catch(() => ({ width: null, height: null, fingerprint: null })); | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Pauses default focus trap behaviors for a single tick. If a focus transition occurs during this | ||
| * tick, the focus trap's deactivation will be overridden to prevent any default focus return, in | ||
|
|
@@ -289,6 +341,7 @@ function AcuantCapture( | |
| onResetFailedCaptureAttempts, | ||
| failedSubmissionAttempts, | ||
| forceNativeCamera, | ||
| failedSubmissionImageFingerprints, | ||
| } = useContext(FailedCaptureAttemptsContext); | ||
|
|
||
| const hasCapture = !isError && (isReady ? isCameraSupported : isMobile); | ||
|
|
@@ -330,17 +383,19 @@ function AcuantCapture( | |
| */ | ||
| async function onUpload(nextValue: File | null) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @dawei-nava I tested locally and attempted to resubmit a yml file that had already failed. Inline errors did display that said However, I was still able to click "submit" and proceed to the next error screen when there were inline errors displaying above both upload zones. Is it possible to prevent the user from clicking submit and proceeding when there are inline errors present above the upload zones? Thanks!
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I saw the same behavior. I also saw that if I reused both the same "front" and "back" image, the inline error only showed on the "back" image upload section. I'm not sure but I think we'd want it to display in both sections, or if only one, the "front" section first. In addition, when I tried again with just a failure on the "front" image upload ( The combination of these two makes me wonder if something is broken with the front upload section, but I'm going to test on
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm...I'm seeing the second issue in
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @night-jellyfish , that's because the mock client you cannot really testing two different files when upload image.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @eileen-nava, that's the original intention, seems there is a issue with ordering/extra stuff. Should work now.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @dawei-nava Thanks! I am logging off now but will look at this tomorrow. 👍🏻
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the info, Dawei! I pulled the updates and tested again with
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interestingly, if I try again but use
So I'm seeing different results with different types of errors / yml files. Maybe this is a yml issue?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @night-jellyfish Looks like it classifes only as a back side error. It alerts it is duplicate submission for the back only. Note: Sometimes it has cached/not compiled javascript files, run
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @eileen-nava issue should have been resolved. |
||
| let analyticsPayload: ImageAnalyticsPayload | undefined; | ||
| let hasFailed = false; | ||
| if (nextValue) { | ||
| const { width, height } = await getImageDimensions(nextValue); | ||
|
|
||
| const { width, height, fingerprint } = await getImageMetadata(nextValue); | ||
| hasFailed = failedSubmissionImageFingerprints[name]?.includes(fingerprint); | ||
| analyticsPayload = getAddAttemptAnalyticsPayload({ | ||
| width, | ||
| height, | ||
| fingerprint, | ||
| mimeType: nextValue.type, | ||
| source: 'upload', | ||
| size: nextValue.size, | ||
| failedImageResubmission: hasFailed, | ||
| }); | ||
|
|
||
| trackEvent(`IdV: ${name} image added`, analyticsPayload); | ||
| } | ||
|
|
||
|
|
@@ -472,6 +527,8 @@ function AcuantCapture( | |
| isAssessedAsBlurry, | ||
| assessment, | ||
| size: getDecodedBase64ByteSize(nextCapture.image.data), | ||
| fingerprint: null, | ||
| failedImageResubmission: false, | ||
| }); | ||
|
|
||
| trackEvent(`IdV: ${name} image added`, analyticsPayload); | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want to be sure I understand the need for this update to the guard statement. Were you trying to ensure that the rate limiter didn't get stale attempts data?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@eileen-nava, yes, because the timing of the extra_attributes called, it may cache stale data ( the attempts).