diff --git a/Gemfile b/Gemfile index 17262773376..8c3c853be75 100644 --- a/Gemfile +++ b/Gemfile @@ -74,7 +74,7 @@ gem 'rqrcode' gem 'ruby-progressbar' gem 'ruby-saml' gem 'safe_target_blank', '>= 1.0.2' -gem 'saml_idp', github: '18F/saml_idp', tag: '0.23.0-18f' +gem 'saml_idp', github: '18F/saml_idp', tag: '0.23.3-18f' gem 'scrypt' gem 'simple_form', '>= 5.0.2' gem 'stringex', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 9fe38995467..e27bf114bc0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -36,10 +36,10 @@ GIT GIT remote: https://github.com/18F/saml_idp.git - revision: 23b69117593e9b9217910af1dd627febd8d18cf4 - tag: 0.23.0-18f + revision: 752085a6f88cd3ce75ecc7a64afe064a0e4f9e35 + tag: 0.23.3-18f specs: - saml_idp (0.23.0.pre.18f) + saml_idp (0.23.3.pre.18f) activesupport builder faraday diff --git a/app/controllers/idv/enter_password_controller.rb b/app/controllers/idv/enter_password_controller.rb index 101d3fb0cdb..64768ea7ec8 100644 --- a/app/controllers/idv/enter_password_controller.rb +++ b/app/controllers/idv/enter_password_controller.rb @@ -129,7 +129,13 @@ def confirm_current_password def init_profile idv_session.create_profile_from_applicant_with_password( password, - resolved_authn_context_result.enhanced_ipp?, + is_enhanced_ipp: resolved_authn_context_result.enhanced_ipp?, + proofing_components: ProofingComponents.new( + user: current_user, + idv_session:, + session:, + user_session:, + ).to_h, ) if idv_session.verify_by_mail? current_user.send_email_to_all_addresses(:verify_by_mail_letter_requested) diff --git a/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb b/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb index 6dfd7980241..243c4e5d598 100644 --- a/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb +++ b/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb @@ -4,11 +4,10 @@ module Idv module HybridMobile module Socure class DocumentCaptureController < ApplicationController - include Idv::AvailabilityConcern + include AvailabilityConcern include DocumentCaptureConcern include Idv::HybridMobile::HybridMobileConcern include RenderConditionConcern - include DocumentCaptureConcern check_or_render_not_found -> { IdentityConfig.store.socure_docv_enabled } before_action :check_valid_document_capture_session, except: [:update] @@ -50,10 +49,16 @@ def show end def update + return if wait_for_result? + result = handle_stored_result( user: document_capture_session.user, store_in_session: false, ) + # TODO: new analytics event? + analytics.idv_doc_auth_document_capture_submitted( + **result.to_h.merge(analytics_arguments), + ) if result.success? redirect_to idv_hybrid_mobile_capture_complete_url @@ -61,6 +66,47 @@ def update redirect_to idv_hybrid_mobile_socure_document_capture_url end end + + private + + def wait_for_result? + return false if stored_result.present? + + # If the stored_result is nil, the job fetching the results has not completed. + analytics.idv_doc_auth_document_capture_polling_wait_visited(**analytics_arguments) + if wait_timed_out? + # flash[:error] = I18n.t('errors.doc_auth.polling_timeout') + # TODO: redirect to try again page LG-14873/14952/15059 + render plain: 'Technical difficulties!!!', status: :ok + else + @refresh_interval = + IdentityConfig.store.doc_auth_socure_wait_polling_refresh_max_seconds + render 'idv/socure/document_capture/wait' + end + + true + end + + def wait_timed_out? + if session[:socure_docv_wait_polling_started_at].nil? + session[:socure_docv_wait_polling_started_at] = Time.zone.now.to_s + return false + end + start = DateTime.parse(session[:socure_docv_wait_polling_started_at]) + timeout_period = + IdentityConfig.store.doc_auth_socure_wait_polling_timeout_minutes.minutes || 2.minutes + start + timeout_period < Time.zone.now + end + + def analytics_arguments + { + flow_path: 'hybrid', + step: 'socure_document_capture', + analytics_id: 'Doc Auth', + liveness_checking_required: false, + selfie_check_required: false, + } + end end end end diff --git a/app/controllers/idv/socure/document_capture_controller.rb b/app/controllers/idv/socure/document_capture_controller.rb index b93ec65334c..1f764f64856 100644 --- a/app/controllers/idv/socure/document_capture_controller.rb +++ b/app/controllers/idv/socure/document_capture_controller.rb @@ -62,27 +62,13 @@ def show end def update + return if wait_for_result? + clear_future_steps! idv_session.redo_document_capture = nil # done with this redo # Not used in standard flow, here for data consistency with hybrid flow. document_capture_session.confirm_ocr - # If the stored_result is nil, the job fetching the results has not completed. - if stored_result.nil? - analytics.idv_doc_auth_document_capture_polling_wait_visited(**analytics_arguments) - if wait_timed_out? - # flash[:error] = I18n.t('errors.doc_auth.polling_timeout') - # TODO: redirect to try again page LG-14873/14952/15059 - render plain: 'Technical difficulties!!!', status: :ok - else - @refresh_interval = - IdentityConfig.store.doc_auth_socure_wait_polling_refresh_max_seconds - render 'idv/socure/document_capture/wait' - end - - return - end - result = handle_stored_result # TODO: new analytics event? analytics.idv_doc_auth_document_capture_submitted(**result.to_h.merge(analytics_arguments)) @@ -121,6 +107,24 @@ def self.step_info private + def wait_for_result? + return false if stored_result.present? + + # If the stored_result is nil, the job fetching the results has not completed. + analytics.idv_doc_auth_document_capture_polling_wait_visited(**analytics_arguments) + if wait_timed_out? + # flash[:error] = I18n.t('errors.doc_auth.polling_timeout') + # TODO: redirect to try again page LG-14873/14952/15059 + render plain: 'Technical difficulties!!!', status: :ok + else + @refresh_interval = + IdentityConfig.store.doc_auth_socure_wait_polling_refresh_max_seconds + render 'idv/socure/document_capture/wait' + end + + true + end + def wait_timed_out? if idv_session.socure_docv_wait_polling_started_at.nil? idv_session.socure_docv_wait_polling_started_at = Time.zone.now.to_s @@ -128,7 +132,7 @@ def wait_timed_out? end start = DateTime.parse(idv_session.socure_docv_wait_polling_started_at) timeout_period = - IdentityConfig.store.doc_auth_socure_wait_polling_timeout_minutes.minutes || 5.minutes + IdentityConfig.store.doc_auth_socure_wait_polling_timeout_minutes.minutes || 2.minutes start + timeout_period < Time.zone.now end diff --git a/app/controllers/saml_idp_controller.rb b/app/controllers/saml_idp_controller.rb index a945f66259f..55083feb324 100644 --- a/app/controllers/saml_idp_controller.rb +++ b/app/controllers/saml_idp_controller.rb @@ -136,6 +136,13 @@ def capture_analytics if result.success? && saml_request.signed? analytics_payload[:cert_error_details] = saml_request.cert_errors + + # analytics to determine if turning on SHA256 validation will break + # existing partners + if certs_different? + analytics_payload[:certs_different] = true + analytics_payload[:sha256_matching_cert] = sha256_alg_matching_cert_serial + end end analytics.saml_auth(**analytics_payload) @@ -147,6 +154,21 @@ def matching_cert_serial nil end + def sha256_alg_matching_cert + # if sha256_alg_matching_cert is nil, fallback to the "first" cert + saml_request.sha256_validation_matching_cert || + saml_request_service_provider&.ssl_certs&.first + rescue SamlIdp::XMLSecurity::SignedDocument::ValidationError + end + + def sha256_alg_matching_cert_serial + sha256_alg_matching_cert&.serial&.to_s + end + + def certs_different? + encryption_cert != sha256_alg_matching_cert + end + def log_external_saml_auth_request return unless external_saml_request? @@ -158,6 +180,8 @@ def log_external_saml_auth_request force_authn: saml_request&.force_authn?, final_auth_request: sp_session[:final_auth_request], service_provider: saml_request&.issuer, + request_signed: saml_request.signed?, + matching_cert_serial:, unknown_authn_contexts:, user_fully_authenticated: user_fully_authenticated?, ) diff --git a/app/controllers/users/reset_passwords_controller.rb b/app/controllers/users/reset_passwords_controller.rb index 49d1b6374f8..e609bd5b0df 100644 --- a/app/controllers/users/reset_passwords_controller.rb +++ b/app/controllers/users/reset_passwords_controller.rb @@ -3,8 +3,6 @@ module Users class ResetPasswordsController < Devise::PasswordsController include AuthorizationCountConcern - include AbTestingConcern - before_action :store_sp_metadata_in_session, only: [:edit] before_action :store_token_in_session, only: [:edit] @@ -44,13 +42,7 @@ def edit def update self.resource = user_matching_token(user_params[:reset_password_token]) - @reset_password_form = ResetPasswordForm.new( - user: resource, - log_password_matches_existing: ab_test_bucket( - :LOG_PASSWORD_RESET_MATCHES_EXISTING, - user: resource, - ) == :log, - ) + @reset_password_form = ResetPasswordForm.new(user: resource) result = @reset_password_form.submit(user_params) diff --git a/app/controllers/users/webauthn_platform_recommended_controller.rb b/app/controllers/users/webauthn_platform_recommended_controller.rb index 039d8acb56a..7c8395dfdcc 100644 --- a/app/controllers/users/webauthn_platform_recommended_controller.rb +++ b/app/controllers/users/webauthn_platform_recommended_controller.rb @@ -15,12 +15,22 @@ def new def create analytics.webauthn_platform_recommended_submitted(opted_to_add: opted_to_add?) + store_webauthn_platform_recommended_in_session if opted_to_add? current_user.update(webauthn_platform_recommended_dismissed_at: Time.zone.now) redirect_to dismiss_redirect_path end private + def store_webauthn_platform_recommended_in_session + user_session[:webauthn_platform_recommended] = + if in_account_creation_flow? + :account_creation + else + :authentication + end + end + def opted_to_add? params[:add_method].present? end diff --git a/app/controllers/users/webauthn_setup_controller.rb b/app/controllers/users/webauthn_setup_controller.rb index 66a4694a668..b67f5b04275 100644 --- a/app/controllers/users/webauthn_setup_controller.rb +++ b/app/controllers/users/webauthn_setup_controller.rb @@ -161,6 +161,7 @@ def process_valid_webauthn(form) def analytics_properties { in_account_creation_flow: user_session[:in_account_creation_flow] || false, + webauthn_platform_recommended: user_session[:webauthn_platform_recommended], attempts: mfa_attempts_count, } end diff --git a/app/forms/reset_password_form.rb b/app/forms/reset_password_form.rb index 35d8ff890d1..adcf084577e 100644 --- a/app/forms/reset_password_form.rb +++ b/app/forms/reset_password_form.rb @@ -5,14 +5,11 @@ class ResetPasswordForm include FormPasswordValidator attr_accessor :reset_password_token - private attr_reader :log_password_matches_existing - alias_method :log_password_matches_existing?, :log_password_matches_existing validate :valid_token - def initialize(user:, log_password_matches_existing: false) + def initialize(user:) @user = user - @log_password_matches_existing = log_password_matches_existing @reset_password_token = @user.reset_password_token @validate_confirmation = true @active_profile = user.active_profile @@ -50,8 +47,6 @@ def handle_valid_password end def update_user - @password_matches_existing = user.valid_password?(password) if log_password_matches_existing? - attributes = { password: password } ActiveRecord::Base.transaction do @@ -92,7 +87,6 @@ def extra_analytics_attributes { user_id: user.uuid, profile_deactivated: active_profile.present?, - password_matches_existing: @password_matches_existing, pending_profile_invalidated: pending_profile.present?, pending_profile_pending_reasons: (pending_profile&.pending_reasons || [])&.join(','), } diff --git a/app/helpers/script_helper.rb b/app/helpers/script_helper.rb index 104e3cec3fe..ae35373dc69 100644 --- a/app/helpers/script_helper.rb +++ b/app/helpers/script_helper.rb @@ -14,14 +14,26 @@ def render_javascript_pack_once_tags(...) javascript_packs_tag_once(...) return if @scripts.blank? concat javascript_assets_tag + crossorigin = local_crossorigin_sources?.presence @scripts.each do |name, (url_params, attributes)| asset_sources.get_sources(name).each do |source| - concat javascript_include_tag( - UriService.add_params(source, url_params), + integrity = asset_sources.get_integrity(source) + + if attributes[:preload_links_header] != false + AssetPreloadLinker.append( + headers: response.headers, + as: :script, + url: source, + crossorigin:, + integrity:, + ) + end + + concat tag.script( + src: UriService.add_params(source, url_params), **attributes, - crossorigin: local_crossorigin_sources? ? true : nil, - integrity: asset_sources.get_integrity(source), - nopush: false, + crossorigin:, + integrity:, ) end end diff --git a/app/helpers/stylesheet_helper.rb b/app/helpers/stylesheet_helper.rb index 2becc651754..c607bdac62c 100644 --- a/app/helpers/stylesheet_helper.rb +++ b/app/helpers/stylesheet_helper.rb @@ -13,7 +13,13 @@ def stylesheet_tag_once(*names) def render_stylesheet_once_tags(*names) stylesheet_tag_once(*names) if names.present? return if @stylesheets.blank? - safe_join(@stylesheets.map { |stylesheet| stylesheet_link_tag(stylesheet, nopush: false) }) + safe_join( + @stylesheets.map do |stylesheet| + url = stylesheet_path(stylesheet) + AssetPreloadLinker.append(headers: response.headers, as: :style, url:) + tag.link(rel: :stylesheet, href: url) + end, + ) end end # rubocop:enable Rails/HelperInstanceVariable diff --git a/app/javascript/packages/analytics/Makefile b/app/javascript/packages/analytics/Makefile index 211d2567d23..9edd73fe61a 100644 --- a/app/javascript/packages/analytics/Makefile +++ b/app/javascript/packages/analytics/Makefile @@ -1,4 +1,4 @@ -DAP_SHA ?= 7c14bb3 +DAP_SHA ?= eebc4d1 digital-analytics-program.js: digital-analytics-program-$(DAP_SHA).js digital-analytics-program.patch patch -p1 $^ --output $@ diff --git a/app/javascript/packages/analytics/digital-analytics-program.patch b/app/javascript/packages/analytics/digital-analytics-program.patch index a2652e478f5..3700721caba 100644 --- a/app/javascript/packages/analytics/digital-analytics-program.patch +++ b/app/javascript/packages/analytics/digital-analytics-program.patch @@ -1,8 +1,2 @@ -73a74 -> GA4Object.defer = true; -785d785 -< var piiRegex = []; -900c900 -< piiRegex.forEach(function (pii) { ---- -> window.piiRegex.forEach(function (pii) { +87a88 +> GA4Object.defer = true; diff --git a/app/jobs/resolution_proofing_job.rb b/app/jobs/resolution_proofing_job.rb index 44401a8914e..b8cd4e08a95 100644 --- a/app/jobs/resolution_proofing_job.rb +++ b/app/jobs/resolution_proofing_job.rb @@ -76,7 +76,7 @@ def perform( if IdentityConfig.store.idv_socure_shadow_mode_enabled SocureShadowModeProofingJob.perform_later( - document_capture_session_result_id: document_capture_session.result_id, + document_capture_session_result_id: document_capture_session&.result_id, encrypted_arguments:, service_provider_issuer:, user_email: user_email_for_proofing(user), diff --git a/app/jobs/socure_docv_results_job.rb b/app/jobs/socure_docv_results_job.rb index 20221f23d68..ad3fa7bca04 100644 --- a/app/jobs/socure_docv_results_job.rb +++ b/app/jobs/socure_docv_results_job.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class SocureDocvResultsJob < ApplicationJob - queue_as :default + queue_as :high_socure_docv attr_reader :document_capture_session_uuid diff --git a/app/models/service_provider.rb b/app/models/service_provider.rb index 34634361288..8146db65b21 100644 --- a/app/models/service_provider.rb +++ b/app/models/service_provider.rb @@ -79,8 +79,7 @@ def ialmax_allowed? end def facial_match_ial_allowed? - IdentityConfig.store.facial_match_general_availability_enabled || - IdentityConfig.store.allowed_biometric_ial_providers.include?(issuer) + IdentityConfig.store.facial_match_general_availability_enabled end private diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index f5ace1ca583..2bc84a7d2cb 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -1255,10 +1255,10 @@ def idv_doc_auth_document_capture_polling_wait_visited( flow_path:, step:, analytics_id:, - redo_document_capture:, - skip_hybrid_handoff:, liveness_checking_required:, selfie_check_required:, + redo_document_capture: nil, + skip_hybrid_handoff: nil, opted_in_to_in_person_proofing: nil, acuant_sdk_upgrade_ab_test_bucket: nil, **extra @@ -1379,10 +1379,35 @@ def idv_doc_auth_exception_visited(step_name:, remaining_submit_attempts:, **ext end # @param [String] side the side of the image submission - def idv_doc_auth_failed_image_resubmitted(side:, **extra) + # @param [Integer] submit_attempts Times that user has tried submitting (previously called + # "attempts") + # @param [Integer] remaining_submit_attempts (previously called "remaining_attempts") + # @param ["hybrid","standard"] flow_path Document capture user flow + # @param [String] liveness_checking_required Whether or not the selfie is required + # @param [String] front_image_fingerprint Fingerprint of front image data + # @param [String] back_image_fingerprint Fingerprint of back image data + # @param [String] selfie_image_fingerprint Fingerprint of selfie image data + def idv_doc_auth_failed_image_resubmitted( + side:, + remaining_submit_attempts:, + flow_path:, + liveness_checking_required:, + submit_attempts:, + front_image_fingerprint:, + back_image_fingerprint:, + selfie_image_fingerprint:, + **extra + ) track_event( 'IdV: failed doc image resubmitted', - side: side, + side:, + remaining_submit_attempts:, + flow_path:, + liveness_checking_required:, + submit_attempts:, + front_image_fingerprint:, + back_image_fingerprint:, + selfie_image_fingerprint:, **extra, ) end @@ -3430,15 +3455,18 @@ def idv_in_person_usps_proofing_results_job_please_call_email_initiated( # GetUspsProofingResultsJob is beginning. Includes some metadata about what the job will do # @param [Integer] enrollments_count number of enrollments eligible for status check # @param [Integer] reprocess_delay_minutes minimum delay since last status check + # @param [String] job_name Name of class which triggered proofing job def idv_in_person_usps_proofing_results_job_started( enrollments_count:, reprocess_delay_minutes:, + job_name:, **extra ) track_event( 'GetUspsProofingResultsJob: Job started', - enrollments_count: enrollments_count, - reprocess_delay_minutes: reprocess_delay_minutes, + enrollments_count:, + reprocess_delay_minutes:, + job_name:, **extra, ) end @@ -4330,12 +4358,15 @@ def idv_request_letter_visited( end # GPO "resend letter" page visited + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # @identity.idp.previous_event_name IdV: request letter visited def idv_resend_letter_visited( + pending_profile_idv_level: nil, **extra ) track_event( :idv_resend_letter_visited, + pending_profile_idv_level:, **extra, ) end @@ -5330,6 +5361,8 @@ def multi_factor_auth_phone_setup( # @param [String, nil] aaguid AAGUID value of WebAuthn device # @param [String[], nil] unknown_transports Array of unrecognized WebAuthn transports, intended to # be used in case of future specification changes. + # @param [:authentication, :account_creation, nil] webauthn_platform_recommended A/B test for + # recommended Face or Touch Unlock setup, if applicable. def multi_factor_auth_setup( success:, multi_factor_auth_method:, @@ -5353,6 +5386,7 @@ def multi_factor_auth_setup( attempts: nil, aaguid: nil, unknown_transports: nil, + webauthn_platform_recommended: nil, **extra ) track_event( @@ -5379,6 +5413,7 @@ def multi_factor_auth_setup( attempts:, aaguid:, unknown_transports:, + webauthn_platform_recommended:, **extra, ) end @@ -5797,7 +5832,6 @@ def password_reset_email( # @param [String] pending_profile_pending_reasons Comma-separated list of the pending states # associated with the associated profile. # @param [Hash] error_details Details for errors that occurred in unsuccessful submission - # @param [Boolean] password_matches_existing Whether the password is same as the user's current # The user changed the password for their account via the password reset flow def password_reset_password( success:, @@ -5805,7 +5839,6 @@ def password_reset_password( profile_deactivated:, pending_profile_invalidated:, pending_profile_pending_reasons:, - password_matches_existing:, error_details: {}, **extra ) @@ -5817,7 +5850,6 @@ def password_reset_password( profile_deactivated:, pending_profile_invalidated:, pending_profile_pending_reasons:, - password_matches_existing:, **extra, ) end @@ -6158,6 +6190,7 @@ def proofing_address_result_missing # @param [String] phone_fingerprint HMAC fingerprint of the phone number formatted as E.164 # @param ["authentication", "reauthentication", "confirmation"] context User session context # @param ["sms", "voice"] otp_delivery_preference Channel used to send the message + # @param [String,nil] step_name Name of step in user flow where rate limit occurred # @identity.idp.previous_event_name Throttler Rate Limit Triggered def rate_limit_reached( limiter_type:, @@ -6165,6 +6198,7 @@ def rate_limit_reached( phone_fingerprint: nil, context: nil, otp_delivery_preference: nil, + step_name: nil, **extra ) track_event( @@ -6174,6 +6208,7 @@ def rate_limit_reached( phone_fingerprint:, context:, otp_delivery_preference:, + step_name:, **extra, ) end @@ -6411,8 +6446,12 @@ def rules_of_use_visit # @param [Boolean] request_signed # @param [String] matching_cert_serial # matches the request certificate in a successful, signed request + # @param [Boolean] certs_different Whether the matching cert changes when SHA256 validations + # are turned on in the saml_idp gem # @param [Hash] cert_error_details Details for errors that occurred because of an invalid # signature + # @param [String] sha256_matching_cert serial of the cert that matches when sha256 validations + # are turned on # @param [String] unknown_authn_contexts space separated list of unknown contexts def saml_auth( success:, @@ -6430,6 +6469,8 @@ def saml_auth( matching_cert_serial:, error_details: nil, cert_error_details: nil, + certs_different: nil, + sha256_matching_cert: nil, unknown_authn_contexts: nil, **extra ) @@ -6450,6 +6491,8 @@ def saml_auth( request_signed:, matching_cert_serial:, cert_error_details:, + certs_different:, + sha256_matching_cert:, unknown_authn_contexts:, **extra, ) @@ -6462,6 +6505,8 @@ def saml_auth( # @param [Boolean] force_authn # @param [Boolean] final_auth_request # @param [String] service_provider + # @param [Boolean] request_signed + # @param [String] matching_cert_serial # @param [String] unknown_authn_contexts space separated list of unknown contexts # @param [Boolean] user_fully_authenticated # An external request for SAML Authentication was received @@ -6473,6 +6518,8 @@ def saml_auth_request( force_authn:, final_auth_request:, service_provider:, + request_signed:, + matching_cert_serial:, unknown_authn_contexts:, user_fully_authenticated:, **extra @@ -6486,6 +6533,8 @@ def saml_auth_request( force_authn:, final_auth_request:, service_provider:, + request_signed:, + matching_cert_serial:, unknown_authn_contexts:, user_fully_authenticated:, **extra, @@ -6863,16 +6912,20 @@ def user_registration_2fa_setup_visit( # reason for the consent screen being shown # @param [Boolean] in_account_creation_flow Whether user is going through account creation # @param [Array] sp_session_requested_attributes Attributes requested by the service provider + # @param [String, nil] in_person_proofing_status In person proofing status + # @param [String, nil] doc_auth_result The doc auth result def user_registration_agency_handoff_page_visit( - ial2:, - service_provider_name:, - page_occurence:, - needs_completion_screen_reason:, - in_account_creation_flow:, - sp_session_requested_attributes:, - ialmax: nil, - **extra - ) + ial2:, + service_provider_name:, + page_occurence:, + needs_completion_screen_reason:, + in_account_creation_flow:, + sp_session_requested_attributes:, + ialmax: nil, + in_person_proofing_status: nil, + doc_auth_result: nil, + **extra + ) track_event( 'User registration: agency handoff visited', ial2:, @@ -6882,6 +6935,8 @@ def user_registration_agency_handoff_page_visit( needs_completion_screen_reason:, in_account_creation_flow:, sp_session_requested_attributes:, + in_person_proofing_status:, + doc_auth_result:, **extra, ) end diff --git a/app/services/asset_preload_linker.rb b/app/services/asset_preload_linker.rb new file mode 100644 index 00000000000..13885272cc7 --- /dev/null +++ b/app/services/asset_preload_linker.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class AssetPreloadLinker + def self.append(headers:, as:, url:, crossorigin: false, integrity: nil) + header = +headers['link'].to_s + header << ',' if header != '' + header << "<#{url}>;rel=preload;as=#{as}" + header << ';crossorigin' if crossorigin + header << ";integrity=#{integrity}" if integrity + headers['link'] = header + end +end diff --git a/app/services/idv/profile_maker.rb b/app/services/idv/profile_maker.rb index 2e9e3a5e2d8..27ef45e8be0 100644 --- a/app/services/idv/profile_maker.rb +++ b/app/services/idv/profile_maker.rb @@ -2,7 +2,7 @@ module Idv class ProfileMaker - attr_reader :pii_attributes + attr_reader :pii_attributes, :proofing_components def initialize( applicant:, @@ -21,13 +21,14 @@ def save_profile( gpo_verification_needed:, in_person_verification_needed:, selfie_check_performed:, + proofing_components:, deactivation_reason: nil ) profile = Profile.new(user: user, active: false, deactivation_reason: deactivation_reason) profile.initiating_service_provider = initiating_service_provider profile.deactivate_for_in_person_verification if in_person_verification_needed profile.encrypt_pii(pii_attributes, user_password) - profile.proofing_components = current_proofing_components + profile.proofing_components = proofing_components profile.fraud_pending_reason = fraud_pending_reason profile.idv_level = set_idv_level( @@ -61,10 +62,6 @@ def set_idv_level(in_person_verification_needed:, selfie_check_performed:) end end - def current_proofing_components - user.proofing_component&.as_json || {} - end - attr_accessor( :user, :user_password, diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb index d31c0d143a5..4f4683997de 100644 --- a/app/services/idv/session.rb +++ b/app/services/idv/session.rb @@ -107,7 +107,9 @@ def respond_to_missing?(method_sym, include_private) VALID_SESSION_ATTRIBUTES.include?(attr_name_sym) || super end - def create_profile_from_applicant_with_password(user_password, is_enhanced_ipp) + def create_profile_from_applicant_with_password( + user_password, is_enhanced_ipp:, proofing_components: + ) if user_has_unscheduled_in_person_enrollment? UspsInPersonProofing::EnrollmentHelper.schedule_in_person_enrollment( user: current_user, @@ -123,6 +125,7 @@ def create_profile_from_applicant_with_password(user_password, is_enhanced_ipp) gpo_verification_needed: !phone_confirmed? || verify_by_mail?, in_person_verification_needed: current_user.has_in_person_enrollment?, selfie_check_performed: session[:selfie_check_performed], + proofing_components:, ) profile.activate unless profile.reason_not_to_activate diff --git a/app/services/proofing/resolution/plugins/aamva_plugin.rb b/app/services/proofing/resolution/plugins/aamva_plugin.rb index 979bd3fd5ef..376e8b7e9a3 100644 --- a/app/services/proofing/resolution/plugins/aamva_plugin.rb +++ b/app/services/proofing/resolution/plugins/aamva_plugin.rb @@ -15,13 +15,13 @@ class AamvaPlugin def call( applicant_pii:, current_sp:, - instant_verify_state_id_address_result:, + state_id_address_resolution_result:, ipp_enrollment_in_progress:, timer: ) - should_proof = should_proof_state_id_with_aamva?( + should_proof = should_proof_state_id?( applicant_pii:, - instant_verify_state_id_address_result:, + state_id_address_resolution_result:, ipp_enrollment_in_progress:, ) @@ -85,37 +85,37 @@ def same_address_as_id?(applicant_pii) applicant_pii[:same_address_as_id].to_s == 'true' end - def should_proof_state_id_with_aamva?( + def should_proof_state_id?( applicant_pii:, - instant_verify_state_id_address_result:, + state_id_address_resolution_result:, ipp_enrollment_in_progress: ) return false unless aamva_supports_state_id_jurisdiction?(applicant_pii) # If the user is in in-person-proofing and they have changed their address then # they are not eligible for get-to-yes if !ipp_enrollment_in_progress || same_address_as_id?(applicant_pii) - user_can_pass_after_state_id_check?(instant_verify_state_id_address_result:) + user_can_pass_after_state_id_check?(state_id_address_resolution_result:) else - instant_verify_state_id_address_result.success? + state_id_address_resolution_result.success? end end def user_can_pass_after_state_id_check?( - instant_verify_state_id_address_result: + state_id_address_resolution_result: ) - return true if instant_verify_state_id_address_result.success? + return true if state_id_address_resolution_result.success? # For failed IV results, this method validates that the user is eligible to pass if the # failed attributes are covered by the same attributes in a successful AAMVA response # aka the Get-to-Yes w/ AAMVA feature. - if !instant_verify_state_id_address_result. + if !state_id_address_resolution_result. failed_result_can_pass_with_additional_verification? return false end attributes_aamva_can_pass = [:address, :dob, :state_id_number] attributes_requiring_additional_verification = - instant_verify_state_id_address_result.attributes_requiring_additional_verification + state_id_address_resolution_result.attributes_requiring_additional_verification results_that_cannot_pass_aamva = attributes_requiring_additional_verification - attributes_aamva_can_pass diff --git a/app/services/proofing/resolution/plugins/instant_verify_residential_address_plugin.rb b/app/services/proofing/resolution/plugins/instant_verify_residential_address_plugin.rb deleted file mode 100644 index 5500b944136..00000000000 --- a/app/services/proofing/resolution/plugins/instant_verify_residential_address_plugin.rb +++ /dev/null @@ -1,81 +0,0 @@ -# frozen_string_literal: true - -module Proofing - module Resolution - module Plugins - class InstantVerifyResidentialAddressPlugin - def call( - applicant_pii:, - current_sp:, - ipp_enrollment_in_progress:, - timer: - ) - return residential_address_unnecessary_result unless ipp_enrollment_in_progress - - timer.time('residential address') do - proofer.proof(applicant_pii) - end.tap do |result| - Db::SpCost::AddSpCost.call( - current_sp, - :lexis_nexis_resolution, - transaction_id: result.transaction_id, - ) - end - end - - def proofer - @proofer ||= begin - # Historically, proofer_mock_fallback has controlled whether we - # use mock implementations of the Resolution and Address proofers - # (true = use mock, false = don't use mock). - # We are transitioning to a place where we will have separate - # configs for both. For the time being, we want to keep support for - # proofer_mock_fallback here. This can be removed after this code - # has been deployed and configs have been updated in all relevant - # environments. - - old_config_says_mock = IdentityConfig.store.proofer_mock_fallback - old_config_says_iv = !old_config_says_mock - new_config_says_mock = - IdentityConfig.store.idv_resolution_default_vendor == :mock - new_config_says_iv = - IdentityConfig.store.idv_resolution_default_vendor == :instant_verify - - proofer_type = - if new_config_says_mock && old_config_says_iv - # This will be the case immediately after deployment, when - # environment configs have not been updated. We need to - # fall back to the old config here. - :instant_verify - elsif new_config_says_iv - :instant_verify - else - :mock - end - - if proofer_type == :mock - Proofing::Mock::ResolutionMockClient.new - else - Proofing::LexisNexis::InstantVerify::Proofer.new( - instant_verify_workflow: IdentityConfig.store.lexisnexis_instant_verify_workflow, - account_id: IdentityConfig.store.lexisnexis_account_id, - base_url: IdentityConfig.store.lexisnexis_base_url, - username: IdentityConfig.store.lexisnexis_username, - password: IdentityConfig.store.lexisnexis_password, - hmac_key_id: IdentityConfig.store.lexisnexis_hmac_key_id, - hmac_secret_key: IdentityConfig.store.lexisnexis_hmac_secret_key, - request_mode: IdentityConfig.store.lexisnexis_request_mode, - ) - end - end - end - - def residential_address_unnecessary_result - Proofing::Resolution::Result.new( - success: true, errors: {}, exception: nil, vendor_name: 'ResidentialAddressNotRequired', - ) - end - end - end - end -end diff --git a/app/services/proofing/resolution/plugins/instant_verify_state_id_address_plugin.rb b/app/services/proofing/resolution/plugins/instant_verify_state_id_address_plugin.rb deleted file mode 100644 index b113f4bc52f..00000000000 --- a/app/services/proofing/resolution/plugins/instant_verify_state_id_address_plugin.rb +++ /dev/null @@ -1,112 +0,0 @@ -# frozen_string_literal: true - -module Proofing - module Resolution - module Plugins - class InstantVerifyStateIdAddressPlugin - SECONDARY_ID_ADDRESS_MAP = { - identity_doc_address1: :address1, - identity_doc_address2: :address2, - identity_doc_city: :city, - identity_doc_address_state: :state, - identity_doc_zipcode: :zipcode, - }.freeze - - def call( - applicant_pii:, - current_sp:, - instant_verify_residential_address_result:, - ipp_enrollment_in_progress:, - timer: - ) - if same_address_as_id?(applicant_pii) && ipp_enrollment_in_progress - return instant_verify_residential_address_result - end - - return resolution_cannot_pass unless instant_verify_residential_address_result.success? - - applicant_pii_with_state_id_address = - if ipp_enrollment_in_progress - with_state_id_address(applicant_pii) - else - applicant_pii - end - - timer.time('resolution') do - proofer.proof(applicant_pii_with_state_id_address) - end.tap do |result| - Db::SpCost::AddSpCost.call( - current_sp, - :lexis_nexis_resolution, - transaction_id: result.transaction_id, - ) - end - end - - def proofer - @proofer ||= begin - # Historically, proofer_mock_fallback has controlled whether we - # use mock implementations of the Resolution and Address proofers - # (true = use mock, false = don't use mock). - # We are transitioning to a place where we will have separate - # configs for both. For the time being, we want to keep support for - # proofer_mock_fallback here. This can be removed after this code - # has been deployed and configs have been updated in all relevant - # environments. - - old_config_says_mock = IdentityConfig.store.proofer_mock_fallback - old_config_says_iv = !old_config_says_mock - new_config_says_mock = - IdentityConfig.store.idv_resolution_default_vendor == :mock - new_config_says_iv = - IdentityConfig.store.idv_resolution_default_vendor == :instant_verify - - proofer_type = - if new_config_says_mock && old_config_says_iv - # This will be the case immediately after deployment, when - # environment configs have not been updated. We need to - # fall back to the old config here. - :instant_verify - elsif new_config_says_iv - :instant_verify - else - :mock - end - - if proofer_type == :mock - Proofing::Mock::ResolutionMockClient.new - else - Proofing::LexisNexis::InstantVerify::Proofer.new( - instant_verify_workflow: IdentityConfig.store.lexisnexis_instant_verify_workflow, - account_id: IdentityConfig.store.lexisnexis_account_id, - base_url: IdentityConfig.store.lexisnexis_base_url, - username: IdentityConfig.store.lexisnexis_username, - password: IdentityConfig.store.lexisnexis_password, - hmac_key_id: IdentityConfig.store.lexisnexis_hmac_key_id, - hmac_secret_key: IdentityConfig.store.lexisnexis_hmac_secret_key, - request_mode: IdentityConfig.store.lexisnexis_request_mode, - ) - end - end - end - - def resolution_cannot_pass - Proofing::Resolution::Result.new( - success: false, errors: {}, exception: nil, vendor_name: 'ResolutionCannotPass', - ) - end - - def same_address_as_id?(applicant_pii) - applicant_pii[:same_address_as_id].to_s == 'true' - end - - # Make a copy of pii with the user's state ID address overwriting the address keys - # Need to first remove the address keys to avoid key/value collision - def with_state_id_address(pii) - pii.except(*SECONDARY_ID_ADDRESS_MAP.values). - transform_keys(SECONDARY_ID_ADDRESS_MAP) - end - end - end - end -end diff --git a/app/services/proofing/resolution/plugins/residential_address_plugin.rb b/app/services/proofing/resolution/plugins/residential_address_plugin.rb new file mode 100644 index 00000000000..4ad356b86ba --- /dev/null +++ b/app/services/proofing/resolution/plugins/residential_address_plugin.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Proofing + module Resolution + module Plugins + class ResidentialAddressPlugin + attr_reader :proofer, :sp_cost_token + + def initialize( + proofer:, + sp_cost_token: + ) + @proofer = proofer + @sp_cost_token = sp_cost_token + end + + def call( + applicant_pii:, + current_sp:, + ipp_enrollment_in_progress:, + timer: + ) + return residential_address_unnecessary_result unless ipp_enrollment_in_progress + + timer.time('residential address') do + proofer.proof(applicant_pii) + end.tap do |result| + Db::SpCost::AddSpCost.call( + current_sp, + :lexis_nexis_resolution, + transaction_id: result.transaction_id, + ) + end + end + + def residential_address_unnecessary_result + Proofing::Resolution::Result.new( + success: true, errors: {}, exception: nil, vendor_name: 'ResidentialAddressNotRequired', + ) + end + end + end + end +end diff --git a/app/services/proofing/resolution/plugins/state_id_address_plugin.rb b/app/services/proofing/resolution/plugins/state_id_address_plugin.rb new file mode 100644 index 00000000000..59768fbf28d --- /dev/null +++ b/app/services/proofing/resolution/plugins/state_id_address_plugin.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module Proofing + module Resolution + module Plugins + class StateIdAddressPlugin + attr_reader :proofer, :sp_cost_token + + SECONDARY_ID_ADDRESS_MAP = { + identity_doc_address1: :address1, + identity_doc_address2: :address2, + identity_doc_city: :city, + identity_doc_address_state: :state, + identity_doc_zipcode: :zipcode, + }.freeze + + def initialize( + proofer:, + sp_cost_token: + ) + @proofer = proofer + @sp_cost_token = sp_cost_token + end + + def call( + applicant_pii:, + current_sp:, + residential_address_resolution_result:, + ipp_enrollment_in_progress:, + timer: + ) + if same_address_as_id?(applicant_pii) && ipp_enrollment_in_progress + return residential_address_resolution_result + end + + return resolution_cannot_pass unless residential_address_resolution_result.success? + + applicant_pii_with_state_id_address = + if ipp_enrollment_in_progress + with_state_id_address(applicant_pii) + else + applicant_pii + end + + timer.time('resolution') do + proofer.proof(applicant_pii_with_state_id_address) + end.tap do |result| + Db::SpCost::AddSpCost.call( + current_sp, + :lexis_nexis_resolution, + transaction_id: result.transaction_id, + ) + end + end + + def resolution_cannot_pass + Proofing::Resolution::Result.new( + success: false, errors: {}, exception: nil, vendor_name: 'ResolutionCannotPass', + ) + end + + def same_address_as_id?(applicant_pii) + applicant_pii[:same_address_as_id].to_s == 'true' + end + + # Make a copy of pii with the user's state ID address overwriting the address keys + # Need to first remove the address keys to avoid key/value collision + def with_state_id_address(pii) + pii.except(*SECONDARY_ID_ADDRESS_MAP.values). + transform_keys(SECONDARY_ID_ADDRESS_MAP) + end + end + end + end +end diff --git a/app/services/proofing/resolution/progressive_proofer.rb b/app/services/proofing/resolution/progressive_proofer.rb index 757a4f244e9..2fbe8c9c3bd 100644 --- a/app/services/proofing/resolution/progressive_proofer.rb +++ b/app/services/proofing/resolution/progressive_proofer.rb @@ -8,17 +8,19 @@ module Resolution # 2. The user has only provided one address for their residential and identity document # address or separate residential and identity document addresses class ProgressiveProofer + class InvalidProofingVendorError; end + attr_reader :aamva_plugin, - :instant_verify_residential_address_plugin, - :instant_verify_state_id_address_plugin, :threatmetrix_plugin + PROOFING_VENDOR_SP_COST_TOKENS = { + mock: :mock_resolution, + instant_verify: :lexis_nexis_resolution, + socure_kyc: :socure_resolution, + }.freeze + def initialize @aamva_plugin = Plugins::AamvaPlugin.new - @instant_verify_residential_address_plugin = - Plugins::InstantVerifyResidentialAddressPlugin.new - @instant_verify_state_id_address_plugin = - Plugins::InstantVerifyStateIdAddressPlugin.new @threatmetrix_plugin = Plugins::ThreatMetrixPlugin.new end @@ -50,17 +52,17 @@ def proof( user_email:, ) - instant_verify_residential_address_result = instant_verify_residential_address_plugin.call( + residential_address_resolution_result = residential_address_plugin.call( applicant_pii:, current_sp:, ipp_enrollment_in_progress:, timer:, ) - instant_verify_state_id_address_result = instant_verify_state_id_address_plugin.call( + state_id_address_resolution_result = state_id_address_plugin.call( applicant_pii:, current_sp:, - instant_verify_residential_address_result:, + residential_address_resolution_result:, ipp_enrollment_in_progress:, timer:, ) @@ -68,7 +70,7 @@ def proof( state_id_result = aamva_plugin.call( applicant_pii:, current_sp:, - instant_verify_state_id_address_result:, + state_id_address_resolution_result:, ipp_enrollment_in_progress:, timer:, ) @@ -76,14 +78,92 @@ def proof( ResultAdjudicator.new( device_profiling_result: device_profiling_result, ipp_enrollment_in_progress: ipp_enrollment_in_progress, - resolution_result: instant_verify_state_id_address_result, + resolution_result: state_id_address_resolution_result, should_proof_state_id: aamva_plugin.aamva_supports_state_id_jurisdiction?(applicant_pii), state_id_result: state_id_result, - residential_resolution_result: instant_verify_residential_address_result, + residential_resolution_result: residential_address_resolution_result, same_address_as_id: applicant_pii[:same_address_as_id], applicant_pii: applicant_pii, ) end + + def proofing_vendor + @proofing_vendor ||= begin + default_vendor = IdentityConfig.store.idv_resolution_default_vendor + alternate_vendor = IdentityConfig.store.idv_resolution_alternate_vendor + alternate_vendor_percent = IdentityConfig.store.idv_resolution_alternate_vendor_percent + + if alternate_vendor == :none + return default_vendor + end + + if (rand * 100) <= alternate_vendor_percent + alternate_vendor + else + default_vendor + end + end + end + + def residential_address_plugin + @residential_address_plugin ||= Plugins::ResidentialAddressPlugin.new( + proofer: create_proofer, + sp_cost_token:, + ) + end + + def state_id_address_plugin + @state_id_address_plugin ||= Plugins::StateIdAddressPlugin.new( + proofer: create_proofer, + sp_cost_token:, + ) + end + + def create_proofer + case proofing_vendor + when :instant_verify then create_instant_verify_proofer + when :mock then create_mock_proofer + when :socure_kyc then create_socure_proofer + else + raise InvalidProofingVendorError, "#{proofing_vendor} is not a valid proofing vendor" + end + end + + def create_instant_verify_proofer + Proofing::LexisNexis::InstantVerify::Proofer.new( + instant_verify_workflow: IdentityConfig.store.lexisnexis_instant_verify_workflow, + account_id: IdentityConfig.store.lexisnexis_account_id, + base_url: IdentityConfig.store.lexisnexis_base_url, + username: IdentityConfig.store.lexisnexis_username, + password: IdentityConfig.store.lexisnexis_password, + hmac_key_id: IdentityConfig.store.lexisnexis_hmac_key_id, + hmac_secret_key: IdentityConfig.store.lexisnexis_hmac_secret_key, + request_mode: IdentityConfig.store.lexisnexis_request_mode, + ) + end + + def create_mock_proofer + Proofing::Mock::ResolutionMockClient.new + end + + def create_socure_proofer + Proofing::Socure::IdPlus::Proofer.new( + Proofing::Socure::IdPlus::Config.new( + api_key: IdentityConfig.store.socure_idplus_api_key, + base_url: IdentityConfig.store.socure_idplus_base_url, + timeout: IdentityConfig.store.socure_idplus_timeout_in_seconds, + ), + ) + end + + def sp_cost_token + PROOFING_VENDOR_SP_COST_TOKENS[proofing_vendor].tap do |token| + if !token.present? + raise InvalidProofingVendorError, + "No cost token present for proofing vendor #{proofing_vendor}" + end + end + end end end end diff --git a/app/services/proofing/socure/id_plus/proofer.rb b/app/services/proofing/socure/id_plus/proofer.rb index 4184fed409d..2bbb2ef7b14 100644 --- a/app/services/proofing/socure/id_plus/proofer.rb +++ b/app/services/proofing/socure/id_plus/proofer.rb @@ -34,14 +34,14 @@ def initialize(config) # @param [Hash] applicant # @return [Proofing::Resolution::Result] def proof(applicant) - input = Input.new(applicant.except(:phone_source)) + input = Input.new(applicant.slice(*Input.members)) request = Request.new(config:, input:) response = request.send_request build_result_from_response(response) - rescue Proofing::TimeoutError, RequestError => err + rescue Proofing::TimeoutError, Request::Error => err build_result_from_error(err) end diff --git a/app/services/proofing/socure/id_plus/request.rb b/app/services/proofing/socure/id_plus/request.rb index 881c495d63d..74b380ffe0b 100644 --- a/app/services/proofing/socure/id_plus/request.rb +++ b/app/services/proofing/socure/id_plus/request.rb @@ -3,42 +3,42 @@ module Proofing module Socure module IdPlus - class RequestError < StandardError - def initialize(wrapped) - @wrapped = wrapped - super(build_message) - end + class Request + class Error < StandardError + def initialize(wrapped) + @wrapped = wrapped + super(build_message) + end - def reference_id - return @reference_id if defined?(@reference_id) - @reference_id = response_body.is_a?(Hash) ? - response_body['referenceId'] : - nil - end + def reference_id + return @reference_id if defined?(@reference_id) + @reference_id = response_body.is_a?(Hash) ? + response_body['referenceId'] : + nil + end - def response_body - return @response_body if defined?(@response_body) - @response_body = wrapped.try(:response_body) - end + def response_body + return @response_body if defined?(@response_body) + @response_body = wrapped.try(:response_body) + end - def response_status - return @response_status if defined?(@response_status) - @response_status = wrapped.try(:response_status) - end + def response_status + return @response_status if defined?(@response_status) + @response_status = wrapped.try(:response_status) + end - private + private - attr_reader :wrapped + attr_reader :wrapped - def build_message - message = response_body.is_a?(Hash) ? response_body['msg'] : nil - message ||= wrapped.message - status = response_status ? " (#{response_status})" : '' - [message, status].join('') + def build_message + message = response_body.is_a?(Hash) ? response_body['msg'] : nil + message ||= wrapped.message + status = response_status ? " (#{response_status})" : '' + [message, status].join('') + end end - end - class Request attr_reader :config, :input SERVICE_NAME = 'socure_id_plus' @@ -78,7 +78,7 @@ def send_request 'Timed out waiting for verification response' end - raise RequestError, e + raise Error, e end def body diff --git a/app/services/reporting/agency_and_sp_report.rb b/app/services/reporting/agency_and_sp_report.rb index e120c81236d..2ab63c8320d 100644 --- a/app/services/reporting/agency_and_sp_report.rb +++ b/app/services/reporting/agency_and_sp_report.rb @@ -10,15 +10,26 @@ def initialize(report_date = Time.zone.today) def agency_and_sp_report idv_sps, auth_sps = service_providers.partition { |sp| sp.ial.present? && sp.ial >= 2 } + idv_agency_ids = idv_sps.map(&:agency_id).uniq idv_agencies, auth_agencies = active_agencies.partition do |agency| idv_agency_ids.include?(agency.id) end + idv_sps_facial_match, idv_sps_legacy = idv_sps.partition do |sp| + facial_match_issuers.include?(sp.issuer) + end + + idv_agency_facial_match_ids = idv_sps_facial_match.map(&:agency_id) + idv_facial_match_agencies, idv_legacy_agencies = idv_agencies.partition do |agency| + idv_agency_facial_match_ids.include?(agency.id) + end + [ ['', 'Number of apps (SPs)', 'Number of agencies and states'], ['Auth', auth_sps.count, auth_agencies.count], - ['IDV', idv_sps.count, idv_agencies.count], + ['IDV (Legacy IDV)', idv_sps_legacy.count, idv_legacy_agencies.count], + ['IDV (Facial matching)', idv_sps_facial_match.count, idv_facial_match_agencies.count], ['Total', auth_sps.count + idv_sps.count, auth_agencies.count + idv_agencies.count], ] rescue ActiveRecord::QueryCanceled => err @@ -54,5 +65,13 @@ def service_providers ServiceProvider.where(issuer: issuers).active.external end end + + def facial_match_issuers + @facial_match_issuers ||= Profile.where(active: true).where( + 'verified_at <= ?', + report_date.end_of_day, + ).where(idv_level: Profile::FACIAL_MATCH_IDV_LEVELS). + pluck(:initiating_service_provider_issuer).uniq + end end end diff --git a/app/services/reporting/total_user_count_report.rb b/app/services/reporting/total_user_count_report.rb index 7d372e282c1..62ac028906d 100644 --- a/app/services/reporting/total_user_count_report.rb +++ b/app/services/reporting/total_user_count_report.rb @@ -10,20 +10,43 @@ def initialize(report_date = Time.zone.today) def total_user_count_report [ - ['Metric', 'All Users', 'Verified users', 'Time Range Start', 'Time Range End'], - ['All-time count', total_user_count, verified_user_count, '-', report_date.to_date], - ['All-time fully registered', total_fully_registered, '-', '-', report_date.to_date], + [ + 'Metric', + 'All Users', + 'Verified users (Legacy IDV)', + 'Verified users (Facial Matching)', + 'Time Range Start', + 'Time Range End', + ], + [ + 'All-time count', + total_user_count, + verified_legacy_idv_user_count, + verified_facial_match_user_count, + '-', + report_date.to_date, + ], + [ + 'All-time fully registered', + total_fully_registered, + '-', + '-', + '-', + report_date.to_date, + ], [ 'New users count', new_user_count, - new_verified_user_count, + new_verified_legacy_idv_user_count, + new_verified_facial_match_user_count, current_month.begin.to_date, current_month.end.to_date, ], [ 'Annual users count', annual_total_user_count, - annual_verified_user_count, + annual_verified_legacy_idv_user_count, + annual_verified_facial_match_user_count, annual_start_date.to_date, annual_end_date.to_date, ], @@ -58,15 +81,35 @@ def total_user_count end end - def verified_user_count + def verified_legacy_idv_user_count Reports::BaseReport.transaction_with_timeout do - Profile.where(active: true).where('verified_at <= ?', end_date).count + Profile.where(active: true).where( + 'verified_at <= ?', + end_date, + ).count - verified_facial_match_user_count end end - def new_verified_user_count + def verified_facial_match_user_count + @verified_facial_match_user_count ||= Reports::BaseReport.transaction_with_timeout do + Profile.where(active: true).where( + 'verified_at <= ?', + end_date, + ).where(idv_level: Profile::FACIAL_MATCH_IDV_LEVELS).count + end + end + + def new_verified_legacy_idv_user_count Reports::BaseReport.transaction_with_timeout do - Profile.where(active: true).where(verified_at: current_month).count + Profile.where(active: true).where(verified_at: current_month).count - + new_verified_facial_match_user_count + end + end + + def new_verified_facial_match_user_count + @new_verified_facial_match_user_count ||= Reports::BaseReport.transaction_with_timeout do + Profile.where(active: true).where(verified_at: current_month). + where(idv_level: Profile::FACIAL_MATCH_IDV_LEVELS).count end end @@ -76,11 +119,17 @@ def annual_total_user_count end end - def annual_verified_user_count + def annual_verified_legacy_idv_user_count Reports::BaseReport.transaction_with_timeout do - Profile.where(active: true). - where(verified_at: annual_start_date..annual_end_date). - count + Profile.where(active: true).where(verified_at: annual_start_date..annual_end_date).count - + annual_verified_facial_match_user_count + end + end + + def annual_verified_facial_match_user_count + @annual_verified_facial_match_user_count ||= Reports::BaseReport.transaction_with_timeout do + Profile.where(active: true).where(verified_at: annual_start_date..annual_end_date). + where(idv_level: Profile::FACIAL_MATCH_IDV_LEVELS).count end end diff --git a/config/application.yml.default b/config/application.yml.default index a69b70d25ea..3e72fd32c6f 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -34,9 +34,7 @@ acuant_sdk_initialization_endpoint: 'https://us.acas.acuant.net' add_email_link_valid_for_hours: 24 address_identity_proofing_supported_country_codes: '["AS", "GU", "MP", "PR", "US", "VI"]' all_redirect_uris_cache_duration_minutes: 2 -allowed_biometric_ial_providers: '[]' allowed_ialmax_providers: '[]' -allowed_valid_authn_contexts_semantic_providers: '[]' allowed_verified_within_providers: '[]' asset_host: '' async_stale_job_timeout_seconds: 300 @@ -59,7 +57,6 @@ backup_code_user_id_per_ip_attempt_window_exponential_factor: 1.1 backup_code_user_id_per_ip_attempt_window_in_minutes: 720 backup_code_user_id_per_ip_attempt_window_max_minutes: 43_200 backup_code_user_id_per_ip_max_attempts: 50 -biometric_ial_enabled: true broken_personal_key_window_finish: '2021-09-22T00:00:00Z' broken_personal_key_window_start: '2021-07-29T00:00:00Z' check_user_password_compromised_enabled: false @@ -133,7 +130,6 @@ facial_match_general_availability_enabled: true feature_idv_force_gpo_verification_enabled: false feature_idv_hybrid_flow_enabled: true feature_select_email_to_share_enabled: true -feature_valid_authn_contexts_semantic_enabled: true geo_data_file_path: 'geo_data/GeoLite2-City.mmdb' get_usps_proofing_results_job_cron: '0/30 * * * *' get_usps_proofing_results_job_reprocess_delay_minutes: 5 @@ -160,6 +156,8 @@ idv_available: true idv_contact_phone_number: (844) 555-5555 idv_max_attempts: 5 idv_min_age_years: 13 +idv_resolution_alternate_vendor: none +idv_resolution_alternate_vendor_percent: 0 idv_resolution_default_vendor: mock idv_send_link_attempt_window_in_minutes: 10 idv_send_link_max_attempts: 5 @@ -232,7 +230,6 @@ lexisnexis_trueid_username: test_username lexisnexis_username: test_username ################################################################### lockout_period_in_minutes: 10 -log_password_reset_matches_existing_ab_test_percent: 0 log_to_stdout: false login_otp_confirmation_max_attempts: 10 logins_per_email_and_ip_bantime: 60 @@ -426,7 +423,6 @@ usps_upload_sftp_password: '' usps_upload_sftp_timeout: 5 usps_upload_sftp_username: '' valid_authn_contexts: '["http://idmanagement.gov/ns/assurance/loa/1", "http://idmanagement.gov/ns/assurance/loa/3", "http://idmanagement.gov/ns/assurance/ial/1", "http://idmanagement.gov/ns/assurance/ial/2", "http://idmanagement.gov/ns/assurance/ial/0", "http://idmanagement.gov/ns/assurance/ial/2?strict=true", "http://idmanagement.gov/ns/assurance/ial/2?bio=preferred", "http://idmanagement.gov/ns/assurance/ial/2?bio=required", "urn:gov:gsa:ac:classes:sp:PasswordProtectedTransport:duo", "http://idmanagement.gov/ns/assurance/aal/2", "http://idmanagement.gov/ns/assurance/aal/3", "http://idmanagement.gov/ns/assurance/aal/3?hspd12=true","http://idmanagement.gov/ns/assurance/aal/2?phishing_resistant=true","http://idmanagement.gov/ns/assurance/aal/2?hspd12=true", "urn:acr.login.gov:auth-only", "urn:acr.login.gov:verified","urn:acr.login.gov:verified-facial-match-preferred","urn:acr.login.gov:verified-facial-match-required"]' -valid_authn_contexts_semantic: '["http://idmanagement.gov/ns/assurance/loa/1", "http://idmanagement.gov/ns/assurance/loa/3", "http://idmanagement.gov/ns/assurance/ial/1", "http://idmanagement.gov/ns/assurance/ial/2", "http://idmanagement.gov/ns/assurance/ial/0", "http://idmanagement.gov/ns/assurance/ial/2?strict=true", "http://idmanagement.gov/ns/assurance/ial/2?bio=preferred", "http://idmanagement.gov/ns/assurance/ial/2?bio=required", "urn:gov:gsa:ac:classes:sp:PasswordProtectedTransport:duo", "http://idmanagement.gov/ns/assurance/aal/2", "http://idmanagement.gov/ns/assurance/aal/3", "http://idmanagement.gov/ns/assurance/aal/3?hspd12=true","http://idmanagement.gov/ns/assurance/aal/2?phishing_resistant=true","http://idmanagement.gov/ns/assurance/aal/2?hspd12=true", "urn:acr.login.gov:auth-only", "urn:acr.login.gov:verified","urn:acr.login.gov:verified-facial-match-preferred","urn:acr.login.gov:verified-facial-match-required"]' vendor_status_idv_scheduled_maintenance_finish: '' vendor_status_idv_scheduled_maintenance_start: '' vendor_status_lexisnexis_instant_verify: 'operational' @@ -507,7 +503,6 @@ production: aamva_send_id_type: false aamva_verification_url: 'https://verificationservices-cert.aamva.org:18449/dldv/2.1/online' available_locales: 'en,es,fr' - biometric_ial_enabled: false disable_email_sending: false disable_logout_get_request: false email_registrations_per_ip_track_only_mode: true @@ -515,7 +510,6 @@ production: enable_usps_verification: false facial_match_general_availability_enabled: false feature_select_email_to_share_enabled: false - feature_valid_authn_contexts_semantic_enabled: false idv_sp_required: true invalid_gpo_confirmation_zipcode: '' lexisnexis_threatmetrix_mock_enabled: false @@ -542,8 +536,6 @@ test: aamva_private_key: 123abc aamva_public_key: 123abc account_reset_fraud_user_wait_period_days: 30 - allowed_biometric_ial_providers: '["urn:gov:gsa:openidconnect:sp:server"]' - allowed_valid_authn_contexts_semantic_providers: '["urn:gov:gsa:openidconnect:sp:server"]' attribute_encryption_key: 2086dfbd15f5b0c584f3664422a1d3409a0d2aa6084f65b6ba57d64d4257431c124158670c7655e45cabe64194f7f7b6c7970153c285bdb8287ec0c4f7553e25 attribute_encryption_key_queue: '[{ "key": "11111111111111111111111111111111" }, { "key": "22222222222222222222222222222222" }]' dashboard_api_token: 123ABC @@ -591,8 +583,10 @@ test: session_encryption_key: 27bad3c25711099429c1afdfd1890910f3b59f5a4faec1c85e945cb8b02b02f261ba501d99cfbb4fab394e0102de6fecf8ffe260f322f610db3e96b2a775c120 short_term_phone_otp_max_attempts: 100 skip_encryption_allowed_list: '[]' + socure_docv_document_request_endpoint: 'https://sandbox.socure.test/documnt-request' socure_docv_webhook_secret_key: 'secret-key' socure_docv_webhook_secret_key_queue: '["old-key-one", "old-key-two"]' + socure_idplus_base_url: 'https://sandbox.socure.test' team_ada_email: 'ada@example.com' team_all_login_emails: '["b@example.com", "c@example.com"]' team_daily_fraud_metrics_emails: '["g@example.com", "h@example.com"]' diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb index 952a7071214..ea7288631d0 100644 --- a/config/initializers/ab_tests.rb +++ b/config/initializers/ab_tests.rb @@ -82,18 +82,12 @@ def self.all end end.freeze - LOG_PASSWORD_RESET_MATCHES_EXISTING = AbTest.new( - experiment_name: 'Log password_matches_existing event property on password reset', - should_log: ['Password Reset: Password Submitted'].to_set, - buckets: { log: IdentityConfig.store.log_password_reset_matches_existing_ab_test_percent }, - ).freeze - RECOMMEND_WEBAUTHN_PLATFORM_FOR_SMS_USER = AbTest.new( experiment_name: 'Recommend Face or Touch Unlock for SMS users', should_log: [ :webauthn_platform_recommended_visited, :webauthn_platform_recommended_submitted, - :webauthn_setup_submitted, + 'Multi-Factor Authentication Setup', ].to_set, buckets: { recommend_for_account_creation: diff --git a/config/routes.rb b/config/routes.rb index 0d832c5e8bf..a01b1257562 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -412,7 +412,7 @@ # Deprecated route - temporary redirect while state id changes are rolled out get '/in_person_proofing/state_id' => redirect('verify/in_person/state_id', status: 307) - put '/in_person_proofing/state_id' => 'in_person/state_id#update' + put '/in_person_proofing/state_id' => redirect('verify/in_person/state_id', status: 307) get '/in_person' => 'in_person#index' get '/in_person/ready_to_verify' => 'in_person/ready_to_verify#show', diff --git a/docs/frontend.md b/docs/frontend.md index 50ea1687641..3b3a48c0d96 100644 --- a/docs/frontend.md +++ b/docs/frontend.md @@ -175,6 +175,14 @@ and independent view components, rendered server-side. For more information, refer to the [components `README.md`](../app/components/README.md). +To preview components and their available options, we use [Lookbook](https://lookbook.build/) to +generate a navigable index of our available components. These previews are available at the [`/components/` route](http://localhost:3000/components/) +in local development, review applications, and in the `dev` environment. When adding a new component +or an option to an existing component, you should also make this component or option available in +Lookbook previews, found under [`spec/components/previews`](https://github.com/18F/identity-idp/tree/main/spec/components/previews). +Refer to [Lookbook's _Previews Overview_ documentation](https://lookbook.build/guide/previews) for +more information on how to author Lookbook previews. + #### React For non-trivial client-side interactivity, we use [React](https://reactjs.org/) to build and combine diff --git a/lib/data_pull.rb b/lib/data_pull.rb index f2c1be32320..35603fe46a1 100644 --- a/lib/data_pull.rb +++ b/lib/data_pull.rb @@ -162,14 +162,22 @@ def run(args:, config:) ActiveRecord::Base.connection.execute('SET statement_timeout = 0') uuids = args + requesting_issuers = + config.requesting_issuers.presence || compute_requesting_issuers(uuids) + users, missing_uuids = uuids.map do |uuid| DataRequests::Deployed::LookupUserByUuid.new(uuid).call || uuid end.partition { |u| u.is_a?(User) } - shared_device_users = DataRequests::Deployed::LookupSharedDeviceUsers.new(users).call + shared_device_users = + if config.depth && config.depth > 0 + DataRequests::Deployed::LookupSharedDeviceUsers.new(users, config.depth).call + else + users + end output = shared_device_users.map do |user| - DataRequests::Deployed::CreateUserReport.new(user, config.requesting_issuers).call + DataRequests::Deployed::CreateUserReport.new(user, requesting_issuers).call end if config.include_missing? @@ -198,6 +206,22 @@ def run(args:, config:) json: output, ) end + + private + + def compute_requesting_issuers(uuids) + service_providers = ServiceProviderIdentity.where(uuid: uuids).pluck(:service_provider) + return nil if service_providers.empty? + service_provider, _count = service_providers.tally.max_by { |_sp, count| count } + + if service_providers.count > 1 + warn "Multiple computed service providers: #{service_providers.join(', ')}" + end + + warn "Computed service provider #{service_provider}" + + Array(service_provider) + end end class ProfileSummary diff --git a/lib/data_requests/deployed/create_user_report.rb b/lib/data_requests/deployed/create_user_report.rb index 062e65ab571..c2980d0b489 100644 --- a/lib/data_requests/deployed/create_user_report.rb +++ b/lib/data_requests/deployed/create_user_report.rb @@ -32,7 +32,6 @@ def mfa_configurations_report end def requesting_issuer_uuid - return user.uuid if requesting_issuers.blank? user.agency_identities.where(agency: requesting_agencies).first&.uuid || "NonSPUser##{user.id}" end diff --git a/lib/identity_config.rb b/lib/identity_config.rb index ea6d23bfc8d..bf04825a906 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -52,9 +52,7 @@ def self.store config.add(:add_email_link_valid_for_hours, type: :integer) config.add(:address_identity_proofing_supported_country_codes, type: :json) config.add(:all_redirect_uris_cache_duration_minutes, type: :integer) - config.add(:allowed_biometric_ial_providers, type: :json) config.add(:allowed_ialmax_providers, type: :json) - config.add(:allowed_valid_authn_contexts_semantic_providers, type: :json) config.add(:allowed_verified_within_providers, type: :json) config.add(:asset_host, type: :string) config.add(:async_stale_job_timeout_seconds, type: :integer) @@ -77,7 +75,6 @@ def self.store config.add(:backup_code_user_id_per_ip_attempt_window_in_minutes, type: :integer) config.add(:backup_code_user_id_per_ip_attempt_window_max_minutes, type: :integer) config.add(:backup_code_user_id_per_ip_max_attempts, type: :integer) - config.add(:biometric_ial_enabled, type: :boolean) config.add(:broken_personal_key_window_finish, type: :timestamp) config.add(:broken_personal_key_window_start, type: :timestamp) config.add(:check_user_password_compromised_enabled, type: :boolean) @@ -151,7 +148,6 @@ def self.store config.add(:feature_idv_force_gpo_verification_enabled, type: :boolean) config.add(:feature_idv_hybrid_flow_enabled, type: :boolean) config.add(:feature_select_email_to_share_enabled, type: :boolean) - config.add(:feature_valid_authn_contexts_semantic_enabled, type: :boolean) config.add(:geo_data_file_path, type: :string) config.add(:get_usps_proofing_results_job_cron, type: :string) config.add(:get_usps_proofing_results_job_reprocess_delay_minutes, type: :integer) @@ -178,10 +174,16 @@ def self.store config.add(:idv_contact_phone_number, type: :string) config.add(:idv_max_attempts, type: :integer) config.add(:idv_min_age_years, type: :integer) + config.add( + :idv_resolution_alternate_vendor, + type: :symbol, + enum: [:instant_verify, :socure_kyc, :mock, :none], + ) + config.add(:idv_resolution_alternate_vendor_percent, type: :integer) config.add( :idv_resolution_default_vendor, type: :symbol, - enum: [:instant_verify, :mock], + enum: [:instant_verify, :socure_kyc, :mock], ) config.add(:idv_send_link_attempt_window_in_minutes, type: :integer) config.add(:idv_send_link_max_attempts, type: :integer) @@ -246,7 +248,6 @@ def self.store config.add(:lexisnexis_trueid_username, type: :string) config.add(:lexisnexis_username, type: :string) config.add(:lockout_period_in_minutes, type: :integer) - config.add(:log_password_reset_matches_existing_ab_test_percent, type: :integer) config.add(:log_to_stdout, type: :boolean) config.add(:login_otp_confirmation_max_attempts, type: :integer) config.add(:logins_per_email_and_ip_bantime, type: :integer) @@ -458,7 +459,6 @@ def self.store config.add(:usps_upload_sftp_timeout, type: :integer) config.add(:usps_upload_sftp_username, type: :string) config.add(:valid_authn_contexts, type: :json) - config.add(:valid_authn_contexts_semantic, type: :json) config.add(:vendor_status_lexisnexis_instant_verify, type: :symbol, enum: VENDOR_STATUS_OPTIONS) config.add(:vendor_status_lexisnexis_phone_finder, type: :symbol, enum: VENDOR_STATUS_OPTIONS) config.add(:vendor_status_lexisnexis_trueid, type: :symbol, enum: VENDOR_STATUS_OPTIONS) diff --git a/lib/saml_idp_constants.rb b/lib/saml_idp_constants.rb index ac117a62739..240e9373253 100644 --- a/lib/saml_idp_constants.rb +++ b/lib/saml_idp_constants.rb @@ -47,8 +47,7 @@ module Constants REQUESTED_ATTRIBUTES_CLASSREF = "#{LEGACY_ACR_NS}/requested_attributes?ReqAttr=".freeze - # TODO: replace valid_authn_contexts_semantic with valid_authn_contexts - VALID_AUTHN_CONTEXTS = IdentityConfig.store.valid_authn_contexts_semantic.freeze + VALID_AUTHN_CONTEXTS = IdentityConfig.store.valid_authn_contexts.freeze FACIAL_MATCH_IAL_CONTEXTS = [ IAL_VERIFIED_FACIAL_MATCH_REQUIRED_ACR, diff --git a/lib/script_base.rb b/lib/script_base.rb index 01352c25dee..c6e0c95f790 100644 --- a/lib/script_base.rb +++ b/lib/script_base.rb @@ -33,6 +33,7 @@ def reason_arg? :show_help, :requesting_issuers, :deflate, + :depth, :reason, keyword_init: true, ) do @@ -111,6 +112,12 @@ def option_parser config.requesting_issuers << issuer end + opts.on('--depth=DEPTH', <<-MSG) do |depth| + depth of connected devices (used for ig-request task) + MSG + config.depth = depth.to_i + end + opts.on('--help') do config.show_help = true end diff --git a/spec/config/initializers/idv_config_spec.rb b/spec/config/initializers/idv_config_spec.rb new file mode 100644 index 00000000000..e69de29bb2d diff --git a/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb b/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb index 60b77e84bdb..ba96dbcfbb4 100644 --- a/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb +++ b/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb @@ -29,6 +29,8 @@ session[:doc_capture_user_id] = user&.id session[:document_capture_session_uuid] = document_capture_session_uuid + + stub_analytics end describe 'before_actions' do @@ -273,6 +275,7 @@ get(:update) expect(response).to redirect_to(idv_hybrid_mobile_capture_complete_url) + expect(@analytics).to have_logged_event('IdV: doc auth document_capture submitted') end context 'when socure is disabled' do @@ -298,6 +301,30 @@ get(:update) expect(response).to redirect_to(idv_hybrid_mobile_socure_document_capture_url) + expect(@analytics).to have_logged_event('IdV: doc auth document_capture submitted') + end + end + + context 'when stored result is nil' do + let(:stored_result) { nil } + + it 'renders the wait view' do + get(:update) + + expect(response).to render_template('idv/socure/document_capture/wait') + expect(@analytics).to have_logged_event(:idv_doc_auth_document_capture_polling_wait_visited) + end + + context 'when the wait times out' do + before do + allow(subject).to receive(:wait_timed_out?).and_return(true) + end + + it 'renders a technical difficulties message' do + get(:update) + expect(response).to have_http_status(:ok) + expect(response.body).to eq('Technical difficulties!!!') + end end end end diff --git a/spec/controllers/idv/personal_key_controller_spec.rb b/spec/controllers/idv/personal_key_controller_spec.rb index dabd49f265e..33946f4bbe1 100644 --- a/spec/controllers/idv/personal_key_controller_spec.rb +++ b/spec/controllers/idv/personal_key_controller_spec.rb @@ -69,7 +69,11 @@ def assert_personal_key_generated_for_profiles(*profile_pii_pairs) idv_session.applicant = applicant if mint_profile_from_idv_session - idv_session.create_profile_from_applicant_with_password(password, is_enhanced_ipp) + idv_session.create_profile_from_applicant_with_password( + password, + is_enhanced_ipp:, + proofing_components: {}, + ) end end diff --git a/spec/controllers/saml_idp_controller_spec.rb b/spec/controllers/saml_idp_controller_spec.rb index 9777cfbc734..c2f6cacea8d 100644 --- a/spec/controllers/saml_idp_controller_spec.rb +++ b/spec/controllers/saml_idp_controller_spec.rb @@ -779,6 +779,8 @@ def name_id_version(format_urn) requested_ial: Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF, service_provider: sp1_issuer, force_authn: false, + request_signed: true, + matching_cert_serial: saml_test_sp_cert_serial, user_fully_authenticated: true, } ) @@ -930,6 +932,8 @@ def name_id_version(format_urn) requested_ial: 'ialmax', service_provider: sp1_issuer, force_authn: false, + request_signed: true, + matching_cert_serial: saml_test_sp_cert_serial, user_fully_authenticated: true, } ) @@ -1221,6 +1225,8 @@ def name_id_version(format_urn) requested_ial: Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, service_provider: 'http://localhost:3000', requested_aal_authn_context: Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + request_signed: true, + matching_cert_serial: saml_test_sp_cert_serial, force_authn: true, user_fully_authenticated: false, } @@ -1525,6 +1531,93 @@ def name_id_version(format_urn) ) end + context 'when request is using SHA1 as the signature method algorithm' do + let(:auth_settings) do + saml_settings( + overrides: { + security: { + authn_requests_signed:, + signature_method: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha1', + }, + }, + ) + end + + context 'when the certificate matches' do + it 'does not note that certs are different in the event' do + user.identities.last.update!(verified_attributes: ['email']) + generate_saml_response(user, auth_settings) + + expect(response.status).to eq(200) + expect(@analytics).to have_logged_event( + 'SAML Auth', hash_not_including( + certs_different: true, + sha256_matching_cert: matching_cert_serial, + ) + ) + end + end + + context 'when the certificate does not match' do + let(:wrong_cert) do + OpenSSL::X509::Certificate.new( + Rails.root.join('certs', 'sp', 'saml_test_sp2.crt').read, + ) + end + + before do + service_provider.update!(certs: [wrong_cert, saml_test_sp_cert]) + end + + it 'notes that certs are different in the event' do + user.identities.last.update!(verified_attributes: ['email']) + generate_saml_response(user, auth_settings) + + expect(response.status).to eq(200) + expect(@analytics).to have_logged_event( + 'SAML Auth', hash_including( + certs_different: true, + sha256_matching_cert: wrong_cert.serial.to_s, + ) + ) + end + end + end + + context 'when request is using SHA1 as the digest method algorithm' do + let(:auth_settings) do + saml_settings( + overrides: { + security: { + authn_requests_signed:, + digest_method: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha1', + }, + }, + ) + end + + it 'notes an error in the event' do + user.identities.last.update!(verified_attributes: ['email']) + generate_saml_response(user, auth_settings) + + expect(response.status).to eq(200) + expect(@analytics).to have_logged_event( + 'SAML Auth', hash_including( + request_signed: authn_requests_signed, + cert_error_details: [ + { + cert: '16692258094164984098', + error_code: :fingerprint_mismatch, + }, + { + cert: '14834808178619537243', error_code: :fingerprint_mismatch + }, + ], + ) + ) + end + end + context 'Certificate sig validation fails because of namespace bug' do let(:request_sp) { double } @@ -1943,6 +2036,8 @@ def name_id_version(format_urn) requested_ial: Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, service_provider: 'http://localhost:3000', requested_aal_authn_context: Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + request_signed: true, + matching_cert_serial: saml_test_sp_cert_serial, force_authn: false, user_fully_authenticated: false, } @@ -2377,6 +2472,7 @@ def name_id_version(format_urn) service_provider: 'http://localhost:3000', requested_aal_authn_context: Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF, force_authn: false, + request_signed: false, user_fully_authenticated: true, } ) @@ -2428,6 +2524,8 @@ def stub_requested_attributes service_provider: 'http://localhost:3000', requested_aal_authn_context: Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, force_authn: false, + request_signed: true, + matching_cert_serial: saml_test_sp_cert_serial, user_fully_authenticated: true, } ) @@ -2478,6 +2576,8 @@ def stub_requested_attributes service_provider: 'http://localhost:3000', requested_aal_authn_context: Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, force_authn: false, + request_signed: true, + matching_cert_serial: saml_test_sp_cert_serial, user_fully_authenticated: true, } ) diff --git a/spec/controllers/users/reset_passwords_controller_spec.rb b/spec/controllers/users/reset_passwords_controller_spec.rb index d3563f9ff27..9f08e5edf0b 100644 --- a/spec/controllers/users/reset_passwords_controller_spec.rb +++ b/spec/controllers/users/reset_passwords_controller_spec.rb @@ -1,8 +1,6 @@ require 'rails_helper' RSpec.describe Users::ResetPasswordsController, devise: true do - include AbTestsHelper - let(:password_error_message) do t('errors.attributes.password.too_short.other', count: Devise.password_length.first) end @@ -404,58 +402,6 @@ expect(user.active_profile.present?).to eq false expect(response).to redirect_to new_user_session_path end - - context 'proofed user submits same password as current' do - let(:user) { create(:user, :proofed) } - let(:password) { user.password } - - it 'logs event indicating profile deactivated while password the same' do - stub_analytics - - reset_password_token = user.set_reset_password_token - - put :update, params: { - reset_password_form: { - password:, - password_confirmation: password, - reset_password_token:, - }, - } - - expect(@analytics).to have_logged_event( - 'Password Reset: Password Submitted', - hash_not_including(password_matches_existing: be_in([true, false])), - ) - end - - context 'when in ab test for logging password matches existing' do - before do - allow(controller).to receive(:ab_test_bucket).with( - :LOG_PASSWORD_RESET_MATCHES_EXISTING, - user:, - ).and_return(:log) - end - - it 'logs event indicating profile deactivated while password the same' do - stub_analytics - - reset_password_token = user.set_reset_password_token - - put :update, params: { - reset_password_form: { - password:, - password_confirmation: password, - reset_password_token:, - }, - } - - expect(@analytics).to have_logged_event( - 'Password Reset: Password Submitted', - hash_including(profile_deactivated: true, password_matches_existing: true), - ) - end - end - end end context 'unconfirmed user submits valid new password' do diff --git a/spec/controllers/users/webauthn_platform_recommended_controller_spec.rb b/spec/controllers/users/webauthn_platform_recommended_controller_spec.rb index 1e9e334500d..853b2b9fe18 100644 --- a/spec/controllers/users/webauthn_platform_recommended_controller_spec.rb +++ b/spec/controllers/users/webauthn_platform_recommended_controller_spec.rb @@ -58,6 +58,11 @@ end end + it 'does not assign recommended session value' do + expect { response }.not_to change { controller.user_session[:webauthn_platform_recommended] }. + from(nil) + end + it 'redirects user to after sign in path' do expect(controller).to receive(:after_sign_in_path_for).with(user).and_return(account_path) @@ -92,6 +97,22 @@ it 'redirects user to set up platform authenticator' do expect(response).to redirect_to(webauthn_setup_path(platform: true)) end + + it 'assigns recommended session value to recommendation flow' do + expect { response }.to change { controller.user_session[:webauthn_platform_recommended] }. + from(nil).to(:authentication) + end + + context 'user is creating account' do + before do + allow(controller).to receive(:in_account_creation_flow?).and_return(true) + end + + it 'assigns recommended session value to recommendation flow' do + expect { response }.to change { controller.user_session[:webauthn_platform_recommended] }. + from(nil).to(:account_creation) + end + end end end end diff --git a/spec/controllers/users/webauthn_setup_controller_spec.rb b/spec/controllers/users/webauthn_setup_controller_spec.rb index d6416d45ae4..8e0e47be037 100644 --- a/spec/controllers/users/webauthn_setup_controller_spec.rb +++ b/spec/controllers/users/webauthn_setup_controller_spec.rb @@ -138,6 +138,21 @@ success: true, ) end + + context 'with setup from sms recommendation' do + before do + controller.user_session[:webauthn_platform_recommended] = :authentication + end + + it 'logs setup event with session value' do + patch :confirm, params: params + + expect(@analytics).to have_logged_event( + 'Multi-Factor Authentication Setup', + hash_including(webauthn_platform_recommended: :authentication), + ) + end + end end end diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb index dd1f0adb23a..3d6c44d8632 100644 --- a/spec/features/idv/doc_auth/document_capture_spec.rb +++ b/spec/features/idv/doc_auth/document_capture_spec.rb @@ -502,11 +502,6 @@ :in_person_proofing_enabled, ).and_return(true) allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(99) - allow(IdentityConfig.store).to receive(:allowed_biometric_ial_providers). - and_return([ipp_service_provider.issuer]) - allow(IdentityConfig.store).to receive( - :allowed_valid_authn_contexts_semantic_providers, - ).and_return([ipp_service_provider.issuer]) perform_in_browser(:mobile) do visit_idp_from_sp_with_ial2( :oidc, @@ -773,11 +768,6 @@ def costing_for(cost_type) allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled).and_return( in_person_proofing_opt_in_enabled, ) - allow(IdentityConfig.store).to receive(:allowed_biometric_ial_providers). - and_return([service_provider.issuer]) - allow(IdentityConfig.store).to receive( - :allowed_valid_authn_contexts_semantic_providers, - ).and_return([service_provider.issuer]) allow_any_instance_of(ServiceProvider).to receive(:in_person_proofing_enabled). and_return(false) visit_idp_from_sp_with_ial2( diff --git a/spec/features/idv/doc_auth/how_to_verify_spec.rb b/spec/features/idv/doc_auth/how_to_verify_spec.rb index 48780d589ab..24a5fb5778a 100644 --- a/spec/features/idv/doc_auth/how_to_verify_spec.rb +++ b/spec/features/idv/doc_auth/how_to_verify_spec.rb @@ -20,11 +20,6 @@ } allow_any_instance_of(ServiceProvider).to receive(:in_person_proofing_enabled). and_return(service_provider_in_person_proofing_enabled) - allow(IdentityConfig.store).to receive(:allowed_biometric_ial_providers). - and_return([ipp_service_provider.issuer]) - allow(IdentityConfig.store).to receive( - :allowed_valid_authn_contexts_semantic_providers, - ).and_return([ipp_service_provider.issuer]) visit_idp_from_sp_with_ial2( :oidc, **{ client_id: ipp_service_provider.issuer, facial_match_required: facial_match_required } diff --git a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb index f6a26076917..c5694c47a20 100644 --- a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb +++ b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb @@ -343,11 +343,6 @@ def verify_no_upload_photos_section_and_link(page) ) allow_any_instance_of(ServiceProvider).to receive(:in_person_proofing_enabled). and_return(sp_ipp_enabled) - allow(IdentityConfig.store).to receive(:allowed_biometric_ial_providers). - and_return([service_provider.issuer]) - allow(IdentityConfig.store).to receive( - :allowed_valid_authn_contexts_semantic_providers, - ).and_return([service_provider.issuer]) visit_idp_from_sp_with_ial2( :oidc, **{ client_id: service_provider.issuer, diff --git a/spec/features/idv/doc_auth/redo_document_capture_spec.rb b/spec/features/idv/doc_auth/redo_document_capture_spec.rb index 1f960747a40..9460ea01896 100644 --- a/spec/features/idv/doc_auth/redo_document_capture_spec.rb +++ b/spec/features/idv/doc_auth/redo_document_capture_spec.rb @@ -500,11 +500,6 @@ :in_person_proofing_enabled, ).and_return(true) allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(99) - allow(IdentityConfig.store).to receive(:allowed_biometric_ial_providers). - and_return([ipp_service_provider.issuer]) - allow(IdentityConfig.store).to receive( - :allowed_valid_authn_contexts_semantic_providers, - ).and_return([ipp_service_provider.issuer]) perform_in_browser(:mobile) do visit_idp_from_sp_with_ial2( :oidc, @@ -771,11 +766,6 @@ def costing_for(cost_type) allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled).and_return( in_person_proofing_opt_in_enabled, ) - allow(IdentityConfig.store).to receive(:allowed_biometric_ial_providers). - and_return([service_provider.issuer]) - allow(IdentityConfig.store).to receive( - :allowed_valid_authn_contexts_semantic_providers, - ).and_return([service_provider.issuer]) allow_any_instance_of(ServiceProvider).to receive(:in_person_proofing_enabled). and_return(false) visit_idp_from_sp_with_ial2( diff --git a/spec/features/idv/doc_auth/socure_document_capture_spec.rb b/spec/features/idv/doc_auth/socure_document_capture_spec.rb new file mode 100644 index 00000000000..ca3e1702bb9 --- /dev/null +++ b/spec/features/idv/doc_auth/socure_document_capture_spec.rb @@ -0,0 +1,209 @@ +require 'rails_helper' + +RSpec.feature 'document capture step', :js do + include IdvStepHelper + include DocAuthHelper + include DocCaptureHelper + include ActionView::Helpers::DateHelper + + let(:max_attempts) { 3 } + let(:fake_analytics) { FakeAnalytics.new } + let(:socure_docv_webhook_secret_key) { 'socure_docv_webhook_secret_key' } + let(:fake_socure_docv_document_request_endpoint) { 'https://fake-socure.test/document-request' } + let(:fake_socure_document_capture_app_url) { 'https://verify.fake-socure.test/something' } + + before(:each) do + allow(IdentityConfig.store).to receive(:socure_docv_enabled).and_return(true) + allow(DocAuthRouter).to receive(:doc_auth_vendor_for_bucket). + and_return(Idp::Constants::Vendors::SOCURE) + allow_any_instance_of(ServiceProviderSession).to receive(:sp_name).and_return('Test SP') + allow(IdentityConfig.store).to receive(:socure_docv_webhook_secret_key). + and_return(socure_docv_webhook_secret_key) + allow(IdentityConfig.store).to receive(:socure_docv_document_request_endpoint). + and_return(fake_socure_docv_document_request_endpoint) + allow(IdentityConfig.store).to receive(:ruby_workers_idv_enabled).and_return(false) + allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics) + @docv_transaction_token = stub_docv_document_request + stub_docv_verification_data_pass + end + + before(:all) do + @user = user_with_2fa + end + + after(:all) { @user.destroy } + + context 'standard desktop flow' do + before do + visit_idp_from_oidc_sp_with_ial2 + sign_in_and_2fa_user(@user) + complete_doc_auth_steps_before_document_capture_step + click_idv_continue + end + + context 'rate limits calls to backend docauth vendor', allow_browser_log: true do + before do + allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(max_attempts) + (max_attempts - 1).times do + socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) + end + end + + it 'redirects to the rate limited error page' do + expect(page).to have_current_path(fake_socure_document_capture_app_url) + visit idv_socure_document_capture_path + expect(page).to have_current_path(idv_socure_document_capture_path) + socure_docv_upload_documents( + docv_transaction_token: @docv_transaction_token, + ) + visit idv_socure_document_capture_path + expect(page).to have_current_path(idv_session_errors_rate_limited_path) + expect(fake_analytics).to have_logged_event( + 'Rate Limit Reached', + limiter_type: :idv_doc_auth, + ) + end + + context 'successfully processes image on last attempt' do + before do + DocAuth::Mock::DocAuthMockClient.reset! + end + + it 'proceeds to the next page with valid info' do + expect(page).to have_current_path(fake_socure_document_capture_app_url) + visit idv_socure_document_capture_path + expect(page).to have_current_path(idv_socure_document_capture_path) + socure_docv_upload_documents( + docv_transaction_token: @docv_transaction_token, + ) + + visit idv_socure_document_capture_update_path + expect(page).to have_current_path(idv_ssn_url) + + visit idv_socure_document_capture_path + + expect(page).to have_current_path(idv_session_errors_rate_limited_path) + end + end + end + + # ToDo post LG-14010 + context 'network connection errors' do + xit 'catches network connection errors on document request', allow_browser_log: true do + # expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error')) + end + + xit 'catches network connection errors on verification data request', + allow_browser_log: true do + # expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error')) + end + end + + it 'does not track state if state tracking is disabled' do + allow(IdentityConfig.store).to receive(:state_tracking_enabled).and_return(false) + socure_docv_upload_documents( + docv_transaction_token: @docv_transaction_token, + ) + + expect(DocAuthLog.find_by(user_id: @user.id).state).to be_nil + end + + xit 'does track state if state tracking is disabled' do + allow(IdentityConfig.store).to receive(:state_tracking_enabled).and_return(true) + socure_docv_upload_documents( + docv_transaction_token: @docv_transaction_token, + ) + + expect(DocAuthLog.find_by(user_id: @user.id).state).not_to be_nil + end + end + + context 'standard mobile flow' do + it 'proceeds to the next page with valid info' do + perform_in_browser(:mobile) do + visit_idp_from_oidc_sp_with_ial2 + sign_in_and_2fa_user(@user) + complete_doc_auth_steps_before_document_capture_step + + expect(page).to have_current_path(idv_socure_document_capture_url) + expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) + click_idv_continue + socure_docv_upload_documents( + docv_transaction_token: @docv_transaction_token, + ) + visit idv_socure_document_capture_update_path + expect(page).to have_current_path(idv_ssn_url) + + expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('NY') + + fill_out_ssn_form_ok + click_idv_continue + complete_verify_step + expect(page).to have_current_path(idv_phone_url) + end + end + end + + def expect_rate_limited_header(expected_to_be_present) + review_issues_h1_heading = strip_tags(t('doc_auth.errors.rate_limited_heading')) + if expected_to_be_present + expect(page).to have_content(review_issues_h1_heading) + else + expect(page).not_to have_content(review_issues_h1_heading) + end + end +end + +RSpec.feature 'direct access to IPP on desktop', :js do + include IdvStepHelper + include DocAuthHelper + + context 'before handoff page' do + let(:sp_ipp_enabled) { true } + let(:in_person_proofing_opt_in_enabled) { true } + let(:facial_match_required) { false } + let(:user) { user_with_2fa } + + before do + service_provider = create(:service_provider, :active, :in_person_proofing_enabled) + allow(IdentityConfig.store).to receive(:doc_auth_selfie_desktop_test_mode).and_return(false) + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) + allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled).and_return( + in_person_proofing_opt_in_enabled, + ) + allow(IdentityConfig.store).to receive(:allowed_biometric_ial_providers). + and_return([service_provider.issuer]) + allow(IdentityConfig.store).to receive( + :allowed_valid_authn_contexts_semantic_providers, + ).and_return([service_provider.issuer]) + allow_any_instance_of(ServiceProvider).to receive(:in_person_proofing_enabled). + and_return(false) + allow(IdentityConfig.store).to receive(:socure_docv_enabled).and_return(true) + allow(DocAuthRouter).to receive(:doc_auth_vendor_for_bucket). + and_return(Idp::Constants::Vendors::SOCURE) + visit_idp_from_sp_with_ial2( + :oidc, + **{ client_id: service_provider.issuer, + facial_match_required: facial_match_required }, + ) + sign_in_via_branded_page(user) + complete_doc_auth_steps_before_agreement_step + + visit idv_socure_document_capture_path(step: 'hybrid_handoff') + end + + context 'when selfie is enabled' do + it 'redirects back to agreement page' do + expect(page).to have_current_path(idv_agreement_path) + end + end + + context 'when selfie is disabled' do + let(:facial_match_required) { false } + + it 'redirects back to agreement page' do + expect(page).to have_current_path(idv_agreement_path) + end + end + end +end diff --git a/spec/features/idv/doc_auth/verify_info_step_spec.rb b/spec/features/idv/doc_auth/verify_info_step_spec.rb index f002b2c5005..661c75dc0f9 100644 --- a/spec/features/idv/doc_auth/verify_info_step_spec.rb +++ b/spec/features/idv/doc_auth/verify_info_step_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'verify_info step and verify_info_concern', :js, allowed_extra_analytics: [:*] do +RSpec.feature 'verify_info step and verify_info_concern', :js do include IdvStepHelper include DocAuthHelper diff --git a/spec/features/idv/end_to_end_idv_spec.rb b/spec/features/idv/end_to_end_idv_spec.rb index 055e011cb7a..dc2c5123915 100644 --- a/spec/features/idv/end_to_end_idv_spec.rb +++ b/spec/features/idv/end_to_end_idv_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe 'Identity verification', :js, allowed_extra_analytics: [:*] do +RSpec.describe 'Identity verification', :js do include IdvStepHelper include InPersonHelper diff --git a/spec/features/idv/hybrid_mobile/entry_spec.rb b/spec/features/idv/hybrid_mobile/entry_spec.rb index 7a979138551..4c43e0b5bfc 100644 --- a/spec/features/idv/hybrid_mobile/entry_spec.rb +++ b/spec/features/idv/hybrid_mobile/entry_spec.rb @@ -40,6 +40,29 @@ expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url) end end + + context 'when socure is the doc auth vendor' do + before do + allow(DocAuthRouter).to receive(:doc_auth_vendor_for_bucket). + and_return(Idp::Constants::Vendors::SOCURE) + stub_docv_document_request + end + + it 'puts the user on the socure document capture page' do + expect(link_to_visit).to be + + Capybara.using_session('mobile') do + visit link_to_visit + # Should have redirected to the actual doc capture url + expect(current_url).to eql(idv_hybrid_mobile_socure_document_capture_url) + + # Confirm that we end up on the LN / Mock page even if we try to + # go to the Socure one. + visit idv_hybrid_mobile_document_capture_url + expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) + end + end + end end context 'old link' do diff --git a/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb b/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb new file mode 100644 index 00000000000..d2da26e722f --- /dev/null +++ b/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb @@ -0,0 +1,255 @@ +require 'rails_helper' + +RSpec.describe 'Hybrid Flow' do + include IdvHelper + include IdvStepHelper + include DocAuthHelper + + let(:phone_number) { '415-555-0199' } + let(:sp) { :oidc } + let(:fake_socure_document_capture_app_url) { 'https://verify.fake-socure.test/something' } + let(:fake_socure_docv_document_request_endpoint) { 'https://fake-socure.test/document-request' } + + before do + allow(FeatureManagement).to receive(:doc_capture_polling_enabled?).and_return(true) + allow(IdentityConfig.store).to receive(:socure_docv_enabled).and_return(true) + allow(DocAuthRouter).to receive(:doc_auth_vendor_for_bucket). + and_return(Idp::Constants::Vendors::SOCURE) + allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true) + allow(IdentityConfig.store).to receive(:ruby_workers_idv_enabled).and_return(false) + allow(IdentityConfig.store).to receive(:socure_docv_document_request_endpoint). + and_return(fake_socure_docv_document_request_endpoint) + allow(Telephony).to receive(:send_doc_auth_link).and_wrap_original do |impl, config| + @sms_link = config[:link] + impl.call(**config) + end.at_least(1).times + @docv_transaction_token = stub_docv_document_request + end + + it 'proofs and hands off to mobile', js: true do + user = nil + + perform_in_browser(:desktop) do + visit_idp_from_sp_with_ial2(sp) + user = sign_up_and_2fa_ial1_user + + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + + expect(page).to have_content(t('doc_auth.headings.text_message')) + expect(page).to have_content(t('doc_auth.info.you_entered')) + expect(page).to have_content('+1 415-555-0199') + + # Confirm that Continue button is not shown when polling is enabled + expect(page).not_to have_content(t('doc_auth.buttons.continue')) + end + + expect(@sms_link).to be_present + + perform_in_browser(:mobile) do + visit @sms_link + + # Confirm that jumping to LinkSent page does not cause errors + visit idv_link_sent_url + expect(page).to have_current_path(root_url) + + # Confirm that we end up on the LN / Mock page even if we try to + # go to the Socure one. + visit idv_hybrid_mobile_socure_document_capture_url + expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) + + # Confirm that clicking cancel and then coming back doesn't cause errors + click_link 'Cancel' + visit idv_hybrid_mobile_socure_document_capture_url + + # Confirm that jumping to Phone page does not cause errors + visit idv_phone_url + expect(page).to have_current_path(root_url) + visit idv_hybrid_mobile_socure_document_capture_url + + # Confirm that jumping to Welcome page does not cause errors + visit idv_welcome_url + expect(page).to have_current_path(root_url) + visit idv_hybrid_mobile_socure_document_capture_url + + expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) + + stub_docv_verification_data_pass + click_idv_continue + expect(page).to have_current_path(fake_socure_document_capture_app_url) + socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) + visit idv_hybrid_mobile_socure_document_capture_update_url + + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + expect(page).to have_content(strip_nbsp(t('doc_auth.headings.capture_complete'))) + expect(page).to have_text(t('doc_auth.instructions.switch_back')) + expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) + + # To be fixed in app: + # Confirm app disallows jumping back to DocumentCapture page + # visit idv_hybrid_mobile_socure_document_capture_url + # expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + end + + perform_in_browser(:desktop) do + expect(page).to_not have_content(t('doc_auth.headings.text_message'), wait: 10) + expect(page).to have_current_path(idv_ssn_path) + + fill_out_ssn_form_ok + click_idv_continue + + expect(page).to have_content(t('headings.verify')) + complete_verify_step + + prefilled_phone = page.find(id: 'idv_phone_form_phone').value + + expect( + PhoneFormatter.format(prefilled_phone), + ).to eq( + PhoneFormatter.format(user.default_phone_configuration.phone), + ) + + fill_out_phone_form_ok + verify_phone_otp + + fill_in t('idv.form.password'), with: Features::SessionHelper::VALID_PASSWORD + click_idv_continue + + acknowledge_and_confirm_personal_key + + validate_idv_completed_page(user) + click_agree_and_continue + + validate_return_to_sp + end + end + + it 'shows the waiting screen correctly after cancelling from mobile and restarting', js: true do + user = nil + + perform_in_browser(:desktop) do + user = sign_in_and_2fa_user + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + + expect(page).to have_content(t('doc_auth.headings.text_message')) + end + + expect(@sms_link).to be_present + + perform_in_browser(:mobile) do + visit @sms_link + expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) + expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie')) + click_on t('links.cancel') + click_on t('forms.buttons.cancel') # Yes, cancel + end + + perform_in_browser(:desktop) do + expect(page).to_not have_content(t('doc_auth.headings.text_message'), wait: 10) + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + + expect(page).to have_content(t('doc_auth.headings.text_message')) + end + end + + context 'user is rate limited on mobile' do + let(:max_attempts) { IdentityConfig.store.doc_auth_max_attempts } + + before do + allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(max_attempts) + DocAuth::Mock::DocAuthMockClient.mock_response!( + method: :post_front_image, + response: DocAuth::Response.new( + success: false, + errors: { network: I18n.t('doc_auth.errors.general.network_error') }, + ), + ) + end + + it 'shows capture complete on mobile and error page on desktop', js: true do + user = nil + + perform_in_browser(:desktop) do + user = sign_in_and_2fa_user + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + + expect(page).to have_content(t('doc_auth.headings.text_message')) + end + + expect(@sms_link).to be_present + + perform_in_browser(:mobile) do + visit @sms_link + + click_idv_continue + expect(page).to have_current_path(fake_socure_document_capture_app_url) + stub_docv_verification_data_pass + max_attempts.times do + socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) + end + + visit idv_hybrid_mobile_socure_document_capture_update_url + + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + expect(page).to have_text(t('doc_auth.instructions.switch_back')) + end + + perform_in_browser(:desktop) do + expect(page).to have_current_path(idv_session_errors_rate_limited_path, wait: 10) + end + end + end + + it 'prefills the phone number used on the phone step if the user has no MFA phone', :js do + user = create(:user, :with_authentication_app) + + perform_in_browser(:desktop) do + start_idv_from_sp(facial_match_required: false) + sign_in_and_2fa_user(user) + + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + end + + expect(@sms_link).to be_present + + perform_in_browser(:mobile) do + visit @sms_link + + expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) + stub_docv_verification_data_pass + click_idv_continue + expect(page).to have_current_path(fake_socure_document_capture_app_url) + socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) + visit idv_hybrid_mobile_socure_document_capture_update_url + + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + expect(page).to have_text(t('doc_auth.instructions.switch_back')) + end + + perform_in_browser(:desktop) do + expect(page).to have_current_path(idv_ssn_path, wait: 10) + + fill_out_ssn_form_ok + click_idv_continue + + expect(page).to have_content(t('headings.verify')) + complete_verify_step + + prefilled_phone = page.find(id: 'idv_phone_form_phone').value + + expect( + PhoneFormatter.format(prefilled_phone), + ).to eq( + PhoneFormatter.format(phone_number), + ) + end + end +end diff --git a/spec/features/idv/in_person_spec.rb b/spec/features/idv/in_person_spec.rb index df4f09ae64a..68bd0646446 100644 --- a/spec/features/idv/in_person_spec.rb +++ b/spec/features/idv/in_person_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' require 'axe-rspec' -RSpec.describe 'In Person Proofing', js: true, allowed_extra_analytics: [:*] do +RSpec.describe 'In Person Proofing', js: true do include IdvStepHelper include SpAuthHelper include InPersonHelper diff --git a/spec/features/idv/steps/enter_code_step_spec.rb b/spec/features/idv/steps/enter_code_step_spec.rb index f12874565c0..a7b0a6a423a 100644 --- a/spec/features/idv/steps/enter_code_step_spec.rb +++ b/spec/features/idv/steps/enter_code_step_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'idv enter letter code step', allowed_extra_analytics: [:*] do +RSpec.feature 'idv enter letter code step' do include IdvStepHelper let(:otp) { 'ABC123' } diff --git a/spec/features/idv/steps/in_person_opt_in_ipp_spec.rb b/spec/features/idv/steps/in_person_opt_in_ipp_spec.rb index 2a66b20974e..db8de8f064d 100644 --- a/spec/features/idv/steps/in_person_opt_in_ipp_spec.rb +++ b/spec/features/idv/steps/in_person_opt_in_ipp_spec.rb @@ -14,9 +14,6 @@ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled).and_return(true) allow(IdentityConfig.store).to receive(:otp_delivery_blocklist_maxretry).and_return(5) - allow(IdentityConfig.store).to receive( - :allowed_valid_authn_contexts_semantic_providers, - ).and_return([ipp_service_provider.issuer, 'urn:gov:gsa:openidconnect:sp:server']) end context 'when ipp_opt_in_enabled and ipp_opt_in_enabled are both enabled' do diff --git a/spec/features/idv/steps/resend_letter_step_spec.rb b/spec/features/idv/steps/resend_letter_step_spec.rb index ab10aae5d6c..1d4a4fe1337 100644 --- a/spec/features/idv/steps/resend_letter_step_spec.rb +++ b/spec/features/idv/steps/resend_letter_step_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'idv resend letter step', allowed_extra_analytics: [:*] do +RSpec.feature 'idv resend letter step' do include IdvStepHelper include OidcAuthHelper diff --git a/spec/features/openid_connect/openid_connect_spec.rb b/spec/features/openid_connect/openid_connect_spec.rb index ede6d47d923..35ee5556c11 100644 --- a/spec/features/openid_connect/openid_connect_spec.rb +++ b/spec/features/openid_connect/openid_connect_spec.rb @@ -601,9 +601,7 @@ to include('Verified within value must be at least 30 days or older') end - it 'sends the user through idv again via verified_within param', - :js, - allowed_extra_analytics: [:*] do + it 'sends the user through idv again via verified_within param', :js do client_id = 'urn:gov:gsa:openidconnect:sp:server' allow(IdentityConfig.store).to receive(:allowed_verified_within_providers). and_return([client_id]) diff --git a/spec/features/reports/authorization_count_spec.rb b/spec/features/reports/authorization_count_spec.rb index 0686dc40f00..a82067f5d68 100644 --- a/spec/features/reports/authorization_count_spec.rb +++ b/spec/features/reports/authorization_count_spec.rb @@ -186,9 +186,6 @@ def visit_idp_from_ial2_saml_sp(issuer:) before do reset_monthly_auth_count_and_login(user) - allow(IdentityConfig.store).to receive( - :allowed_valid_authn_contexts_semantic_providers, - ).and_return([client_id_1, client_id_2]) end context 'using oidc' do diff --git a/spec/features/saml/ial2_sso_spec.rb b/spec/features/saml/ial2_sso_spec.rb index 4d54ee8798a..5c772f42fa7 100644 --- a/spec/features/saml/ial2_sso_spec.rb +++ b/spec/features/saml/ial2_sso_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'IAL2 Single Sign On', allowed_extra_analytics: [:*] do +RSpec.feature 'IAL2 Single Sign On' do include SamlAuthHelper include IdvStepHelper include DocAuthHelper diff --git a/spec/features/saml/saml_spec.rb b/spec/features/saml/saml_spec.rb index 73cb1f469d3..d0bee87d559 100644 --- a/spec/features/saml/saml_spec.rb +++ b/spec/features/saml/saml_spec.rb @@ -508,6 +508,8 @@ service_provider: 'http://localhost:3000', requested_aal_authn_context: Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, force_authn: false, + matching_cert_serial: saml_test_sp_cert_serial, + request_signed: true, user_fully_authenticated: false }], ) expect(fake_analytics.events['SAML Auth'].count).to eq 2 @@ -551,6 +553,8 @@ requested_ial: 'http://idmanagement.gov/ns/assurance/ial/2', service_provider: 'saml_sp_ial2', force_authn: false, + matching_cert_serial: saml_test_sp_cert_serial, + request_signed: true, user_fully_authenticated: false, }, ], @@ -581,6 +585,8 @@ service_provider: 'http://localhost:3000', requested_aal_authn_context: Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, force_authn: false, + matching_cert_serial: saml_test_sp_cert_serial, + request_signed: true, user_fully_authenticated: false }], ) expect(fake_analytics.events['SAML Auth'].count).to eq 2 diff --git a/spec/features/saml/vtr_spec.rb b/spec/features/saml/vtr_spec.rb index 7321a806b07..a22bf2781ac 100644 --- a/spec/features/saml/vtr_spec.rb +++ b/spec/features/saml/vtr_spec.rb @@ -126,9 +126,7 @@ expect_successful_saml_redirect end - scenario 'sign in with VTR request for idv requires idv', - :js, - allowed_extra_analytics: [:*] do + scenario 'sign in with VTR request for idv requires idv', :js do user = create(:user, :fully_registered) visit_saml_authn_request_url( @@ -150,8 +148,7 @@ expect_successful_saml_redirect end - scenario 'sign in with VTR request for idv includes proofed attributes', - allowed_extra_analytics: [:*] do + scenario 'sign in with VTR request for idv includes proofed attributes' do pii = { first_name: 'Jonathan', ssn: '900-66-6666', @@ -190,8 +187,7 @@ end scenario 'sign in with VTR request for idv with facial match requires idv with facial match', - :js, - allowed_extra_analytics: [:*] do + :js do user = create(:user, :proofed) user.active_profile.update!(idv_level: :legacy_unsupervised) diff --git a/spec/features/sign_in/multiple_vot_spec.rb b/spec/features/sign_in/multiple_vot_spec.rb index 8145eee78cb..9210fae3d17 100644 --- a/spec/features/sign_in/multiple_vot_spec.rb +++ b/spec/features/sign_in/multiple_vot_spec.rb @@ -38,9 +38,7 @@ expect(user_info[:vot]).to eq('C1.C2.P1') end - scenario 'identity proofing with facial match is required if user is not proofed', - :js, - allowed_extra_analytics: [:*] do + scenario 'identity proofing with facial match is required if user is not proofed', :js do user = create(:user, :fully_registered) visit_idp_from_oidc_sp_with_vtr(vtr: ['C1.C2.P1.Pb', 'C1.C2.P1']) @@ -176,9 +174,7 @@ expect(first_name).to_not be_blank end - scenario 'identity proofing with facial match is required if user is not proofed', - :js, - allowed_extra_analytics: [:*] do + scenario 'identity proofing with facial match is required if user is not proofed', :js do user = create(:user, :fully_registered) visit_saml_authn_request_url( diff --git a/spec/features/users/user_profile_spec.rb b/spec/features/users/user_profile_spec.rb index 76c594f8e1f..32399dede83 100644 --- a/spec/features/users/user_profile_spec.rb +++ b/spec/features/users/user_profile_spec.rb @@ -158,9 +158,7 @@ expect(current_path).to eq(account_path) end - it 'allows the user reactivate their profile by reverifying', - :js, - allowed_extra_analytics: [:*] do + it 'allows the user reactivate their profile by reverifying', :js do profile = create(:profile, :active, :verified, pii: { ssn: '1234', dob: '1920-01-01' }) user = profile.user diff --git a/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_success.json b/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_success.json index 770b875a008..6b171049751 100644 --- a/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_success.json +++ b/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_success.json @@ -1,474 +1,811 @@ { - "Status": { - "ConversationId": "31000403205968", - "RequestId": "705004858", - "TransactionStatus": "passed", - "Reference": "Reference1", - "ServerInfo": "bctlsidmapp01.risk.regn.net" - }, - "Products": [ { + "Status": { + "ConversationId": "31000403205968", + "RequestId": "705004858", + "TransactionStatus": "passed", + "Reference": "Reference1", + "ServerInfo": "bctlsidmapp01.risk.regn.net" + }, + "Products": [ + { "ProductType": "TrueID", "ExecutedStepName": "True_ID_Step", "ProductConfigurationName": "AndreV3_TrueID_Flow", "ProductStatus": "pass", - "ParameterDetails": [ - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DocumentName", - "Values": [{"Value": "New York (NY) Learner Permit"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DocAuthResult", - "Values": [{"Value": "Passed"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DocIssuerCode", - "Values": [{"Value": "NY"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DocIssuerName", - "Values": [{"Value": "New York"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DocClassCode", - "Values": [{"Value": "DriversLicense"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DocClass", - "Values": [{"Value": "DriversLicense"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DocClassName", - "Values": [{"Value": "Drivers License"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DocIsGeneric", - "Values": [{"Value": "false"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DocIssue", - "Values": [{"Value": "1997"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DocIssueType", - "Values": [{"Value": "Learner Permit"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DocSize", - "Values": [{"Value": "ID1"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "ClassificationMode", - "Values": [{"Value": "Automatic"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "OrientationChanged", - "Values": [{"Value": "false"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "PresentationChanged", - "Values": [{"Value": "false"}] - }, - { - "Group": "IMAGE_METRICS_RESULT", - "Name": "Side", - "Values": [{"Value": "Front"}] - }, - { - "Group": "IMAGE_METRICS_RESULT", - "Name": "GlareMetric", - "Values": [{"Value": "95"}] - }, - { - "Group": "IMAGE_METRICS_RESULT", - "Name": "SharpnessMetric", - "Values": [{"Value": "64"}] - }, - { - "Group": "IMAGE_METRICS_RESULT", - "Name": "IsTampered", - "Values": [{"Value": "0"}] - }, - { - "Group": "IMAGE_METRICS_RESULT", - "Name": "IsCropped", - "Values": [{"Value": "0"}] - }, - { - "Group": "IMAGE_METRICS_RESULT", - "Name": "HorizontalResolution", - "Values": [{"Value": "353"}] - }, - { - "Group": "IMAGE_METRICS_RESULT", - "Name": "VerticalResolution", - "Values": [{"Value": "353"}] - }, - { - "Group": "IMAGE_METRICS_RESULT", - "Name": "Light", - "Values": [{"Value": "White"}] - }, - { - "Group": "IMAGE_METRICS_RESULT", - "Name": "MimeType", - "Values": [{"Value": "image/jpeg"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "FullName", - "Values": [{"Value": "LICENSE SAMPLE"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Sex", - "Values": [{"Value": "Male"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Age", - "Values": [{"Value": "54"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DOB_Year", - "Values": [{"Value": "1966"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DOB_Month", - "Values": [{"Value": "5"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DOB_Day", - "Values": [{"Value": "5"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "ExpirationDate_Year", - "Values": [{"Value": "2099"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "ExpirationDate_Month", - "Values": [{"Value": "5"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "ExpirationDate_Day", - "Values": [{"Value": "5"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Portrait", - "Values": [{"Value": "/9j/4AAQSkZJRgABAQEBYgFiAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9\nPDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhC\nY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAAR\nCAGzAVwDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAA\nAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkK\nFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWG\nh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl\n5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREA\nAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYk\nNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOE\nhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk\n5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDSkZiHQszc5AJqATOSshYgjg805xmNHP8A\nD1qEEBzxkNSNCRZNpPzHD56GlDMVwzHI9TUOxijKv3kPHNPZhgPjrwaYD/OUDeGbnjigONrR\nEkhvunNMUYZlJ425FNBzEMclDyfWkA8yOY8ZO9MZwaeZWVwckq/HFR7trA9QwximAHa8TcHO\nQaAJA+dyMSWTkc07ziqpJkkdDioiS3ly9ME7qRh8zKDwwz7UwJAcKU3/AOsHHtSecSFY53J7\n1HnCMO68ilLZPUBWHNAEjS993DDoTTd+QYyenI5poQBNpJODkGkMgPzDqODQBKZG2I+9iBwR\nmoiyqxBPyyDP0pxJjO3++MjPas66ncBk3jco5oE2WmuVEBffyjcfSo2v8ndENwdeTmsSa4aR\n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\n/Hv/AMCNSZJmTPpRRTAbH92WpIf9Sv0oooQM/9k="}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_1_AlertName", - "Values": [{"Value": "Birth Date Crosscheck"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_2_AlertName", - "Values": [{"Value": "Visible Pattern"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_3_AlertName", - "Values": [{"Value": "Birth Date Valid"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_4_AlertName", - "Values": [{"Value": "Document Classification"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_5_AlertName", - "Values": [{"Value": "Document Expired"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_6_AlertName", - "Values": [{"Value": "Expiration Date Valid"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_7_AlertName", - "Values": [{"Value": "Issue Date Valid"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_8_AlertName", - "Values": [{"Value": "Visible Pattern"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_9_AlertName", - "Values": [{"Value": "Visible Pattern"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_1_AuthenticationResult", - "Values": [ { - "Value": "Failed", - "Detail": "Compare the machine-readable birth date field to the human-readable birth date field." - }] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_2_AuthenticationResult", - "Values": [ { - "Value": "Failed", - "Detail": "Verified the presence of a pattern on the visible image." - }] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_3_AuthenticationResult", - "Values": [ { - "Value": "Passed", - "Detail": "Verified that the birth date is valid." - }] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_4_AuthenticationResult", - "Values": [ { - "Value": "Passed", - "Detail": "Verified that the type of document is supported and is able to be fully authenticated." - }] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_5_AuthenticationResult", - "Values": [ { - "Value": "Passed", - "Detail": "Checked if the document is expired." - }] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_6_AuthenticationResult", - "Values": [ { - "Value": "Passed", - "Detail": "Verified that the expiration date is valid." - }] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_7_AuthenticationResult", - "Values": [ { - "Value": "Passed", - "Detail": "Verified that the issue date is valid." - }] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_8_AuthenticationResult", - "Values": [ { - "Value": "Passed", - "Detail": "Verified the presence of a pattern on the visible image." - }] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_9_AuthenticationResult", - "Values": [ { - "Value": "Passed", - "Detail": "Verified the presence of a pattern on the visible image." - }] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_2_Regions", - "Values": [{"Value": "Verify Layout 1"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_8_Regions", - "Values": [{"Value": "Visible Pattern"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_9_Regions", - "Values": [{"Value": "Name Registration Verify"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_FullName", - "Values": [{"Value": "LICENSE SAMPLE"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_Surname", - "Values": [{"Value": "SAMPLE"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_GivenName", - "Values": [{"Value": "LICENSE"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_FirstName", - "Values": [{"Value": "LICENSE"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_DOB_Year", - "Values": [{"Value": "1966"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_DOB_Month", - "Values": [{"Value": "5"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_DOB_Day", - "Values": [{"Value": "5"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_DocumentClassName", - "Values": [{"Value": "Drivers License"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_DocumentNumber", - "Values": [{"Value": "020000060"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_ExpirationDate_Year", - "Values": [{"Value": "2099"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_ExpirationDate_Month", - "Values": [{"Value": "5"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_xpirationDate_Day", - "Values": [{"Value": "5"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_IssuingStateCode", - "Values": [{"Value": "NY"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_IssuingStateName", - "Values": [{"Value": "New York"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_Address", - "Values": [{"Value": "123 ABC AVE
ANYTOWN NY
12345"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_AddressLine1", - "Values": [{"Value": "123 ABC AVE"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_AddressLine2", - "Values": [{"Value": "APT 3E"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_City", - "Values": [{"Value": "ANYTOWN"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_State", - "Values": [{"Value": "NY"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_PostalCode", - "Values": [{"Value": "12345"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_Sex", - "Values": [{"Value": "M"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_ControlNumber", - "Values": [{"Value": "6820051160"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_Height", - "Values": [{"Value": "5'08\""}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_IssueDate_Year", - "Values": [{"Value": "1997"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_IssueDate_Month", - "Values": [{"Value": "7"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_IssueDate_Day", - "Values": [{"Value": "15"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_LicenseClass", - "Values": [{"Value": "D"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_LicenseRestrictions", - "Values": [{"Value": "B"}] - }, - { - "Group": "PORTRAIT_MATCH_RESULT", - "Name": "FaceMatchResult", - "Values": [{"Value": "Fail"}] - }, - { - "Group": "PORTRAIT_MATCH_RESULT", - "Name": "FaceMatchScore", - "Values": [{"Value": "0"}] - }, - { - "Group": "PORTRAIT_MATCH_RESULT", - "Name": "FaceStatusCode", - "Values": [{"Value": "0"}] - }, - { - "Group": "PORTRAIT_MATCH_RESULT", - "Name": "FaceErrorMessage", - "Values": [{"Value": "Liveness: PoorQuality"}] - } + "ParameterDetails": [ + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocumentName", + "Values": [ + { + "Value": "New York (NY) Learner Permit" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocAuthResult", + "Values": [ + { + "Value": "Passed" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIssuerCode", + "Values": [ + { + "Value": "NY" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIssuerName", + "Values": [ + { + "Value": "New York" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocClassCode", + "Values": [ + { + "Value": "DriversLicense" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocClass", + "Values": [ + { + "Value": "DriversLicense" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocClassName", + "Values": [ + { + "Value": "Drivers License" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIsGeneric", + "Values": [ + { + "Value": "false" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIssue", + "Values": [ + { + "Value": "1997" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIssueType", + "Values": [ + { + "Value": "Learner Permit" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocSize", + "Values": [ + { + "Value": "ID1" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "ClassificationMode", + "Values": [ + { + "Value": "Automatic" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "OrientationChanged", + "Values": [ + { + "Value": "false" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "PresentationChanged", + "Values": [ + { + "Value": "false" + } + ] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "Side", + "Values": [ + { + "Value": "Front" + } + ] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "GlareMetric", + "Values": [ + { + "Value": "95" + } + ] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "SharpnessMetric", + "Values": [ + { + "Value": "64" + } + ] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "IsTampered", + "Values": [ + { + "Value": "0" + } + ] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "IsCropped", + "Values": [ + { + "Value": "0" + } + ] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "HorizontalResolution", + "Values": [ + { + "Value": "353" + } + ] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "VerticalResolution", + "Values": [ + { + "Value": "353" + } + ] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "Light", + "Values": [ + { + "Value": "White" + } + ] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "MimeType", + "Values": [ + { + "Value": "image/jpeg" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "FullName", + "Values": [ + { + "Value": "LICENSE SAMPLE" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Sex", + "Values": [ + { + "Value": "Male" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Age", + "Values": [ + { + "Value": "54" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DOB_Year", + "Values": [ + { + "Value": "1966" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DOB_Month", + "Values": [ + { + "Value": "5" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DOB_Day", + "Values": [ + { + "Value": "5" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "ExpirationDate_Year", + "Values": [ + { + "Value": "2099" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "ExpirationDate_Month", + "Values": [ + { + "Value": "5" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "ExpirationDate_Day", + "Values": [ + { + "Value": "5" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Portrait", + "Values": [ + { + "Value": "/9j/4AAQSkZJRgABAQEBYgFiAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9\nPDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhC\nY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAAR\nCAGzAVwDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAA\nAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkK\nFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWG\nh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl\n5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREA\nAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYk\nNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOE\nhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk\n5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDSkZiHQszc5AJqATOSshYgjg805xmNHP8A\nD1qEEBzxkNSNCRZNpPzHD56GlDMVwzHI9TUOxijKv3kPHNPZhgPjrwaYD/OUDeGbnjigONrR\nEkhvunNMUYZlJ425FNBzEMclDyfWkA8yOY8ZO9MZwaeZWVwckq/HFR7trA9QwximAHa8TcHO\nQaAJA+dyMSWTkc07ziqpJkkdDioiS3ly9ME7qRh8zKDwwz7UwJAcKU3/AOsHHtSecSFY53J7\n1HnCMO68ilLZPUBWHNAEjS993DDoTTd+QYyenI5poQBNpJODkGkMgPzDqODQBKZG2I+9iBwR\nmoiyqxBPyyDP0pxJjO3++MjPas66ncBk3jco5oE2WmuVEBffyjcfSo2v8ndENwdeTmsSa4aR\n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\n/Hv/AMCNSZJmTPpRRTAbH92WpIf9Sv0oooQM/9k=" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_1_AlertName", + "Values": [ + { + "Value": "Birth Date Crosscheck" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_2_AlertName", + "Values": [ + { + "Value": "Visible Pattern" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_3_AlertName", + "Values": [ + { + "Value": "Birth Date Valid" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_4_AlertName", + "Values": [ + { + "Value": "Document Classification" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_5_AlertName", + "Values": [ + { + "Value": "Document Expired" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_6_AlertName", + "Values": [ + { + "Value": "Expiration Date Valid" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_7_AlertName", + "Values": [ + { + "Value": "Issue Date Valid" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_8_AlertName", + "Values": [ + { + "Value": "Visible Pattern" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_9_AlertName", + "Values": [ + { + "Value": "Visible Pattern" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_1_AuthenticationResult", + "Values": [ + { + "Value": "Failed", + "Detail": "Compare the machine-readable birth date field to the human-readable birth date field." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_2_AuthenticationResult", + "Values": [ + { + "Value": "Failed", + "Detail": "Verified the presence of a pattern on the visible image." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_3_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified that the birth date is valid." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_4_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified that the type of document is supported and is able to be fully authenticated." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_5_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Checked if the document is expired." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_6_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified that the expiration date is valid." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_7_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified that the issue date is valid." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_8_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified the presence of a pattern on the visible image." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_9_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified the presence of a pattern on the visible image." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_2_Regions", + "Values": [ + { + "Value": "Verify Layout 1" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_8_Regions", + "Values": [ + { + "Value": "Visible Pattern" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_9_Regions", + "Values": [ + { + "Value": "Name Registration Verify" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_FullName", + "Values": [ + { + "Value": "LICENSE SAMPLE" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_Surname", + "Values": [ + { + "Value": "SAMPLE" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_GivenName", + "Values": [ + { + "Value": "LICENSE" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_FirstName", + "Values": [ + { + "Value": "LICENSE" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_NameSuffix", + "Values": [ + { + "Value": "JR" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_DOB_Year", + "Values": [ + { + "Value": "1966" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_DOB_Month", + "Values": [ + { + "Value": "5" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_DOB_Day", + "Values": [ + { + "Value": "5" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_DocumentClassName", + "Values": [ + { + "Value": "Drivers License" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_DocumentNumber", + "Values": [ + { + "Value": "020000060" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_ExpirationDate_Year", + "Values": [ + { + "Value": "2099" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_ExpirationDate_Month", + "Values": [ + { + "Value": "5" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_xpirationDate_Day", + "Values": [ + { + "Value": "5" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_IssuingStateCode", + "Values": [ + { + "Value": "NY" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_IssuingStateName", + "Values": [ + { + "Value": "New York" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_Address", + "Values": [ + { + "Value": "123 ABC AVE
ANYTOWN NY
12345" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_AddressLine1", + "Values": [ + { + "Value": "123 ABC AVE" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_AddressLine2", + "Values": [ + { + "Value": "APT 3E" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_City", + "Values": [ + { + "Value": "ANYTOWN" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_State", + "Values": [ + { + "Value": "NY" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_PostalCode", + "Values": [ + { + "Value": "12345" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_Sex", + "Values": [ + { + "Value": "M" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_ControlNumber", + "Values": [ + { + "Value": "6820051160" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_Height", + "Values": [ + { + "Value": "5'08\"" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_IssueDate_Year", + "Values": [ + { + "Value": "1997" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_IssueDate_Month", + "Values": [ + { + "Value": "7" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_IssueDate_Day", + "Values": [ + { + "Value": "15" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_LicenseClass", + "Values": [ + { + "Value": "D" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_LicenseRestrictions", + "Values": [ + { + "Value": "B" + } + ] + }, + { + "Group": "PORTRAIT_MATCH_RESULT", + "Name": "FaceMatchResult", + "Values": [ + { + "Value": "Fail" + } + ] + }, + { + "Group": "PORTRAIT_MATCH_RESULT", + "Name": "FaceMatchScore", + "Values": [ + { + "Value": "0" + } + ] + }, + { + "Group": "PORTRAIT_MATCH_RESULT", + "Name": "FaceStatusCode", + "Values": [ + { + "Value": "0" + } + ] + }, + { + "Group": "PORTRAIT_MATCH_RESULT", + "Name": "FaceErrorMessage", + "Values": [ + { + "Value": "Liveness: PoorQuality" + } + ] + } ] - }] + } + ] } diff --git a/spec/fixtures/socure_docv/pass.json b/spec/fixtures/socure_docv/pass.json new file mode 100644 index 00000000000..d5ca2efa7f1 --- /dev/null +++ b/spec/fixtures/socure_docv/pass.json @@ -0,0 +1,41 @@ +{ + "referenceId": "a1234b56-e789-0123-4fga-56b7c890d123", + "previousReferenceId": "e9c170f2-b3e4-423b-a373-5d6e1e9b23f8", + "documentVerification": { + "reasonCodes": [ + "I831", + "R810" + ], + "documentType": { + "type": "Drivers License", + "country": "USA", + "state": "NY" + }, + "decision": { + "name": "lenient", + "value": "accept" + }, + "documentData": { + "firstName": "Dwayne", + "surName": "Denver", + "fullName": "Dwayne Denver", + "address": "123 Example Street, New York City, NY 10001", + "parsedAddress": { + "physicalAddress": "123 Example Street", + "physicalAddress2": "New York City NY 10001", + "city": "New York City", + "state": "NY", + "country": "US", + "zip": "10001" + }, + "documentNumber": "000000000", + "dob": "2002-01-01", + "issueDate": "2024-01-01", + "expirationDate": "2070-01-01" + } + }, + "customerProfile": { + "customerUserId": "129", + "userId": "u8JpWn4QsF3R7tA2" + } +} diff --git a/spec/forms/idv/api_image_upload_form_spec.rb b/spec/forms/idv/api_image_upload_form_spec.rb index 03ac3d58c2d..422527d7b89 100644 --- a/spec/forms/idv/api_image_upload_form_spec.rb +++ b/spec/forms/idv/api_image_upload_form_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe Idv::ApiImageUploadForm, allowed_extra_analytics: [:*] do +RSpec.describe Idv::ApiImageUploadForm do include DocPiiHelper subject(:form) do diff --git a/spec/forms/openid_connect_authorize_form_spec.rb b/spec/forms/openid_connect_authorize_form_spec.rb index 8f3caffcdd0..fe1598234dc 100644 --- a/spec/forms/openid_connect_authorize_form_spec.rb +++ b/spec/forms/openid_connect_authorize_form_spec.rb @@ -198,11 +198,6 @@ end context 'with a known IAL value' do - before do - allow(IdentityConfig.store).to receive( - :allowed_valid_authn_contexts_semantic_providers, - ).and_return(client_id) - end let(:acr_values) do [ 'unknown-value', @@ -252,32 +247,14 @@ ).and_return(false) end - context 'when the service provider is allowed to use facial match ials' do - before do - allow(IdentityConfig.store).to receive( - :allowed_biometric_ial_providers, - ).and_return([client_id]) - end - - it 'succeeds validation' do - expect(form).to be_valid - end - end - - context 'when the service provider is not allowed to use facial match ials' do - it 'fails with a not authorized error' do - expect(form).not_to be_valid - expect(form.errors[:acr_values]). - to include(t('openid_connect.authorization.errors.no_auth')) - end + it 'fails with a not authorized error' do + expect(form).not_to be_valid + expect(form.errors[:acr_values]). + to include(t('openid_connect.authorization.errors.no_auth')) end end context 'when facial match general availability is turned on' do - before do - expect(IdentityConfig.store).not_to receive(:allowed_biometric_ial_providers) - end - it 'succeeds validation' do expect(form).to be_valid end diff --git a/spec/forms/reset_password_form_spec.rb b/spec/forms/reset_password_form_spec.rb index df25444e89f..a9c588cd2e6 100644 --- a/spec/forms/reset_password_form_spec.rb +++ b/spec/forms/reset_password_form_spec.rb @@ -2,8 +2,7 @@ RSpec.describe ResetPasswordForm, type: :model do let(:user) { create(:user, uuid: '123') } - let(:log_password_matches_existing) { false } - subject(:form) { ResetPasswordForm.new(user:, log_password_matches_existing:) } + subject(:form) { ResetPasswordForm.new(user:) } let(:password) { 'a good and powerful password' } let(:password_confirmation) { password } @@ -33,7 +32,6 @@ profile_deactivated: false, pending_profile_invalidated: false, pending_profile_pending_reasons: '', - password_matches_existing: nil, ) end end @@ -64,7 +62,6 @@ profile_deactivated: false, pending_profile_invalidated: false, pending_profile_pending_reasons: '', - password_matches_existing: nil, ) end end @@ -84,7 +81,6 @@ profile_deactivated: false, pending_profile_invalidated: false, pending_profile_pending_reasons: '', - password_matches_existing: nil, ) end end @@ -117,7 +113,6 @@ profile_deactivated: false, pending_profile_invalidated: false, pending_profile_pending_reasons: '', - password_matches_existing: nil, ) end end @@ -134,7 +129,6 @@ profile_deactivated: false, pending_profile_invalidated: false, pending_profile_pending_reasons: '', - password_matches_existing: nil, ) end end @@ -147,22 +141,6 @@ expect(result.extra[:profile_deactivated]).to eq(true) expect(user.profiles.any?(&:active?)).to eq(false) end - - context 'when the password is same as current' do - let(:password) { user.password } - - it 'does not include extra detail for password matching existing' do - expect(result.extra[:password_matches_existing]).to eq(nil) - end - - context 'when initialized to log password_matches_existing' do - let(:log_password_matches_existing) { true } - - it 'includes extra detail for password matching existing' do - expect(result.extra[:password_matches_existing]).to eq(true) - end - end - end end context 'when the user does not have an active profile' do diff --git a/spec/helpers/script_helper_spec.rb b/spec/helpers/script_helper_spec.rb index a7b256ba506..a13e12aa403 100644 --- a/spec/helpers/script_helper_spec.rb +++ b/spec/helpers/script_helper_spec.rb @@ -82,8 +82,8 @@ render_javascript_pack_once_tags expect(response.headers['link']).to eq( - '; rel=preload; as=script,' \ - '; rel=preload; as=script', + ';rel=preload;as=script,' \ + ';rel=preload;as=script', ) expect(response.headers['link']).to_not include('nopush') end @@ -107,6 +107,18 @@ end end + context 'with preload links header disabled' do + before do + javascript_packs_tag_once('application', preload_links_header: false) + end + + it 'does not append preload header' do + render_javascript_pack_once_tags + + expect(response.headers['link']).to eq(';rel=preload;as=script') + end + end + context 'with attributes' do before do javascript_packs_tag_once('track-errors', defer: true) diff --git a/spec/helpers/stylesheet_helper_spec.rb b/spec/helpers/stylesheet_helper_spec.rb index 9c4a9835bae..8237c2690ea 100644 --- a/spec/helpers/stylesheet_helper_spec.rb +++ b/spec/helpers/stylesheet_helper_spec.rb @@ -34,7 +34,7 @@ it 'adds preload header without nopush attribute' do render_stylesheet_once_tags - expect(response.headers['link']).to eq('; rel=preload; as=style') + expect(response.headers['link']).to eq(';rel=preload;as=style') expect(response.headers['link']).to_not include('nopush') end end diff --git a/spec/jobs/resolution_proofing_job_spec.rb b/spec/jobs/resolution_proofing_job_spec.rb index f00dafabb6e..0ce568616d6 100644 --- a/spec/jobs/resolution_proofing_job_spec.rb +++ b/spec/jobs/resolution_proofing_job_spec.rb @@ -26,6 +26,8 @@ and_return(lexisnexis_threatmetrix_mock_enabled) allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_base_url). and_return('https://www.example.com') + allow(IdentityConfig.store).to receive(:idv_resolution_default_vendor). + and_return(:instant_verify) end describe '#perform' do diff --git a/spec/lib/data_pull_spec.rb b/spec/lib/data_pull_spec.rb index 640f0741544..d772f9abfaf 100644 --- a/spec/lib/data_pull_spec.rb +++ b/spec/lib/data_pull_spec.rb @@ -319,6 +319,26 @@ expect(result.subtask).to eq('ig-request') expect(result.uuids).to eq([user.uuid]) end + + context 'with SP UUID argument and no requesting issuer' do + let(:args) { [identity.uuid] } + let(:config) { ScriptBase::Config.new } + + it 'runs the report with computed requesting issuer', aggregate_failures: true do + expect(result.table).to be_nil + expect(result.json.first.keys).to contain_exactly( + :user_id, + :login_uuid, + :requesting_issuer_uuid, + :email_addresses, + :mfa_configurations, + :user_events, + ) + + expect(result.subtask).to eq('ig-request') + expect(result.uuids).to eq([user.uuid]) + end + end end end diff --git a/spec/lib/data_requests/deployed/create_user_report_spec.rb b/spec/lib/data_requests/deployed/create_user_report_spec.rb index 8b66c950bea..5658d2459b6 100644 --- a/spec/lib/data_requests/deployed/create_user_report_spec.rb +++ b/spec/lib/data_requests/deployed/create_user_report_spec.rb @@ -9,7 +9,7 @@ expect(result[:user_id]).to eq(user.id) expect(result[:login_uuid]).to eq(user.uuid) - expect(result[:requesting_issuer_uuid]).to eq(user.uuid) + expect(result[:requesting_issuer_uuid]).to eq("NonSPUser##{user.id}") expect(result[:email_addresses]).to be_a(Array) expect(result[:mfa_configurations]).to be_a(Hash) expect(result[:user_events]).to be_a(Array) diff --git a/spec/models/service_provider_spec.rb b/spec/models/service_provider_spec.rb index e952fb106c2..387a17e1dbb 100644 --- a/spec/models/service_provider_spec.rb +++ b/spec/models/service_provider_spec.rb @@ -88,24 +88,8 @@ and_return(true) end - context 'when the service provider is in the allowed list' do - before do - expect(IdentityConfig.store).not_to receive(:allowed_biometric_ial_providers) - end - - it 'allows the service provider to use facial match IALs' do - expect(service_provider.facial_match_ial_allowed?).to be(true) - end - end - - context 'when the service provider is not in the allowed list' do - before do - expect(IdentityConfig.store).not_to receive(:allowed_biometric_ial_providers) - end - - it 'allows the service provider to use facial match IALs' do - expect(service_provider.facial_match_ial_allowed?).to be(true) - end + it 'allows the service provider to use facial match IALs' do + expect(service_provider.facial_match_ial_allowed?).to be(true) end end @@ -115,26 +99,8 @@ and_return(false) end - context 'when the service provider is in the allowed list' do - before do - allow(IdentityConfig.store).to receive(:allowed_biometric_ial_providers). - and_return([service_provider.issuer]) - end - - it 'allows the service provider to use facial match IALs' do - expect(service_provider.facial_match_ial_allowed?).to be(true) - end - end - - context 'when the service provider is not in the allowed list' do - before do - allow(IdentityConfig.store).to receive(:allowed_biometric_ial_providers). - and_return([]) - end - - it 'does not allow the service provider to use facial match IALs' do - expect(service_provider.facial_match_ial_allowed?).to be(false) - end + it 'does not allow the service provider to use facial match IALs' do + expect(service_provider.facial_match_ial_allowed?).to be(false) end end end diff --git a/spec/policies/idv/flow_policy_spec.rb b/spec/policies/idv/flow_policy_spec.rb index d96c9006355..8561f382d84 100644 --- a/spec/policies/idv/flow_policy_spec.rb +++ b/spec/policies/idv/flow_policy_spec.rb @@ -315,7 +315,9 @@ it 'returns personal_key' do stub_up_to(:request_letter, idv_session: idv_session) idv_session.gpo_code_verified = true - idv_session.create_profile_from_applicant_with_password('password', is_enhanced_ipp) + idv_session.create_profile_from_applicant_with_password( + 'password', is_enhanced_ipp:, proofing_components: {} + ) expect(subject.info_for_latest_step.key).to eq(:personal_key) expect(subject.controller_allowed?(controller: Idv::PersonalKeyController)).to be @@ -326,7 +328,9 @@ let(:is_enhanced_ipp) { false } it 'returns personal_key' do stub_up_to(:otp_verification, idv_session: idv_session) - idv_session.create_profile_from_applicant_with_password('password', is_enhanced_ipp) + idv_session.create_profile_from_applicant_with_password( + 'password', is_enhanced_ipp:, proofing_components: {} + ) expect(subject.info_for_latest_step.key).to eq(:personal_key) expect(subject.controller_allowed?(controller: Idv::PersonalKeyController)).to be diff --git a/spec/services/asset_preload_linker_spec.rb b/spec/services/asset_preload_linker_spec.rb new file mode 100644 index 00000000000..be302316c2c --- /dev/null +++ b/spec/services/asset_preload_linker_spec.rb @@ -0,0 +1,63 @@ +require 'rails_helper' + +RSpec.describe AssetPreloadLinker do + describe '.append' do + let(:link) { nil } + let(:as) { 'script' } + let(:url) { '/script.js' } + let(:crossorigin) { nil } + let(:integrity) { nil } + let(:headers) { { 'link' => link } } + subject(:result) do + AssetPreloadLinker.append(**{ headers:, as:, url:, crossorigin:, integrity: }.compact) + end + + context 'with absent link value' do + let(:link) { nil } + + it 'returns a string with only the appended link' do + expect(result).to eq(';rel=preload;as=script') + end + end + + context 'with empty link value' do + let(:link) { '' } + + it 'returns a string with only the appended link' do + expect(result).to eq(';rel=preload;as=script') + end + end + + context 'with non-empty link value' do + let(:link) { ';rel=preload;as=script' } + + it 'returns a comma-separated link value of the new and existing link' do + expect(result).to eq(';rel=preload;as=script,;rel=preload;as=script') + end + + context 'with existing link value as frozen string' do + let(:link) { ';rel=preload;as=script'.freeze } + + it 'returns a comma-separated link value of the new and existing link' do + expect(result).to eq(';rel=preload;as=script,;rel=preload;as=script') + end + end + end + + context 'with crossorigin option' do + let(:crossorigin) { true } + + it 'includes crossorigin link param' do + expect(result).to eq(';rel=preload;as=script;crossorigin') + end + end + + context 'with integrity option' do + let(:integrity) { 'abc123' } + + it 'includes integrity link param' do + expect(result).to eq(';rel=preload;as=script;integrity=abc123') + end + end + end +end diff --git a/spec/services/idv/phone_step_spec.rb b/spec/services/idv/phone_step_spec.rb index a5213b09c6f..a98978b034c 100644 --- a/spec/services/idv/phone_step_spec.rb +++ b/spec/services/idv/phone_step_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe Idv::PhoneStep, allowed_extra_analytics: [:*] do +RSpec.describe Idv::PhoneStep do let(:user) { create(:user) } let(:service_provider) do create( diff --git a/spec/services/idv/profile_maker_spec.rb b/spec/services/idv/profile_maker_spec.rb index 6736bcba24e..987438eff83 100644 --- a/spec/services/idv/profile_maker_spec.rb +++ b/spec/services/idv/profile_maker_spec.rb @@ -7,6 +7,7 @@ let(:user_password) { user.password } let(:initiating_service_provider) { nil } let(:in_person_proofing_enforce_tmx_mock) { false } + let(:proofing_components) { { document_check: :mock } } subject do described_class.new( @@ -18,12 +19,12 @@ end it 'creates an inactive Profile with encrypted PII' do - proofing_component = ProofingComponent.create(user_id: user.id, document_check: 'mock') profile = subject.save_profile( fraud_pending_reason: nil, gpo_verification_needed: false, in_person_verification_needed: false, selfie_check_performed: false, + proofing_components:, ) pii = subject.pii_attributes @@ -32,7 +33,7 @@ expect(profile.encrypted_pii).to_not be_nil expect(profile.encrypted_pii).to_not match('Some') expect(profile.fraud_pending_reason).to be_nil - expect(profile.proofing_components).to match(proofing_component.as_json) + expect(profile.proofing_components).to match(proofing_components.as_json) expect(profile.active).to eq(false) expect(profile.deactivation_reason).to be_nil @@ -48,6 +49,7 @@ deactivation_reason: :encryption_error, in_person_verification_needed: false, selfie_check_performed: false, + proofing_components:, ) end it 'creates an inactive profile with deactivation reason' do @@ -75,6 +77,7 @@ deactivation_reason: nil, in_person_verification_needed: in_person_verification_needed, selfie_check_performed: false, + proofing_components:, ) end @@ -118,6 +121,7 @@ deactivation_reason: nil, in_person_verification_needed: false, selfie_check_performed: false, + proofing_components:, ) end it 'creates a pending profile for gpo verification' do @@ -151,6 +155,7 @@ deactivation_reason: nil, in_person_verification_needed: true, selfie_check_performed: false, + proofing_components:, ) end @@ -190,6 +195,7 @@ deactivation_reason: nil, in_person_verification_needed: true, selfie_check_performed: false, + proofing_components:, ) end @@ -220,6 +226,7 @@ deactivation_reason: nil, in_person_verification_needed: false, selfie_check_performed: selfie_check_performed, + proofing_components:, ) end @@ -267,6 +274,7 @@ deactivation_reason: nil, in_person_verification_needed: false, selfie_check_performed: false, + proofing_components:, ) end it 'creates a profile with the initiating sp recorded' do diff --git a/spec/services/idv/session_spec.rb b/spec/services/idv/session_spec.rb index e7009ef5352..df023971d8d 100644 --- a/spec/services/idv/session_spec.rb +++ b/spec/services/idv/session_spec.rb @@ -3,7 +3,6 @@ RSpec.describe Idv::Session do let(:user) { create(:user) } let(:user_session) { {} } - let(:is_enhanced_ipp) { false } subject do Idv::Session.new(user_session: user_session, current_user: user, service_provider: nil) @@ -128,6 +127,8 @@ end describe '#create_profile_from_applicant_with_password' do + let(:is_enhanced_ipp) { false } + let(:proofing_components) { { document_check: 'mock' } } let(:opt_in_param) { nil } before do @@ -146,7 +147,9 @@ now = Time.zone.now subject.user_phone_confirmation = true - subject.create_profile_from_applicant_with_password(user.password, is_enhanced_ipp) + subject.create_profile_from_applicant_with_password( + user.password, is_enhanced_ipp:, proofing_components: + ) profile = subject.profile expect(profile.activated_at).to eq now @@ -165,7 +168,9 @@ it 'does not complete the profile if the user has not completed OTP phone confirmation' do subject.user_phone_confirmation = nil - subject.create_profile_from_applicant_with_password(user.password, is_enhanced_ipp) + subject.create_profile_from_applicant_with_password( + user.password, is_enhanced_ipp:, proofing_components: + ) profile = subject.profile expect(profile.activated_at).to eq nil @@ -200,7 +205,9 @@ end it 'creates an USPS enrollment' do - subject.create_profile_from_applicant_with_password(user.password, is_enhanced_ipp) + subject.create_profile_from_applicant_with_password( + user.password, is_enhanced_ipp:, proofing_components: + ) expect(UspsInPersonProofing::EnrollmentHelper).to have_received( :schedule_in_person_enrollment, ).with( @@ -212,7 +219,9 @@ end it 'creates a profile with in person verification pending' do - subject.create_profile_from_applicant_with_password(user.password, is_enhanced_ipp) + subject.create_profile_from_applicant_with_password( + user.password, is_enhanced_ipp:, proofing_components: + ) expect(profile).to have_attributes( { activated_at: nil, @@ -227,12 +236,16 @@ end it 'saves the pii to the session' do - subject.create_profile_from_applicant_with_password(user.password, is_enhanced_ipp) + subject.create_profile_from_applicant_with_password( + user.password, is_enhanced_ipp:, proofing_components: + ) expect(Pii::Cacher.new(user, user_session).fetch(profile.id)).to_not be_nil end it 'associates the in person enrollment with the created profile' do - subject.create_profile_from_applicant_with_password(user.password, is_enhanced_ipp) + subject.create_profile_from_applicant_with_password( + user.password, is_enhanced_ipp:, proofing_components: + ) expect(enrollment.reload.profile_id).to eq(profile.id) end end @@ -245,7 +258,9 @@ end it 'does not create a profile' do - subject.create_profile_from_applicant_with_password(user.password, is_enhanced_ipp) + subject.create_profile_from_applicant_with_password( + user.password, is_enhanced_ipp:, proofing_components: + ) rescue expect(profile).to be_nil end @@ -264,14 +279,18 @@ end it 'does not create an USPS enrollment' do - subject.create_profile_from_applicant_with_password(user.password, is_enhanced_ipp) + subject.create_profile_from_applicant_with_password( + user.password, is_enhanced_ipp:, proofing_components: + ) expect(UspsInPersonProofing::EnrollmentHelper).to_not have_received( :schedule_in_person_enrollment, ) end it 'creates a profile' do - subject.create_profile_from_applicant_with_password(user.password, is_enhanced_ipp) + subject.create_profile_from_applicant_with_password( + user.password, is_enhanced_ipp:, proofing_components: + ) expect(profile).to have_attributes( { active: true, @@ -284,7 +303,9 @@ end it 'saves the pii to the session' do - subject.create_profile_from_applicant_with_password(user.password, is_enhanced_ipp) + subject.create_profile_from_applicant_with_password( + user.password, is_enhanced_ipp:, proofing_components: + ) expect(Pii::Cacher.new(user, user_session).fetch(profile.id)).to_not be_nil end end @@ -297,7 +318,9 @@ end it 'sets profile to pending gpo verification' do - subject.create_profile_from_applicant_with_password(user.password, is_enhanced_ipp) + subject.create_profile_from_applicant_with_password( + user.password, is_enhanced_ipp:, proofing_components: + ) profile = subject.profile expect(profile.activated_at).to eq nil @@ -321,7 +344,9 @@ end it 'does not complete the user profile' do - subject.create_profile_from_applicant_with_password(user.password, is_enhanced_ipp) + subject.create_profile_from_applicant_with_password( + user.password, is_enhanced_ipp:, proofing_components: + ) profile = subject.profile expect(profile.activated_at).to eq nil diff --git a/spec/services/proofing/resolution/plugins/aamva_plugin_spec.rb b/spec/services/proofing/resolution/plugins/aamva_plugin_spec.rb index cfd24e0c754..f23c51056c4 100644 --- a/spec/services/proofing/resolution/plugins/aamva_plugin_spec.rb +++ b/spec/services/proofing/resolution/plugins/aamva_plugin_spec.rb @@ -3,7 +3,7 @@ RSpec.describe Proofing::Resolution::Plugins::AamvaPlugin do let(:applicant_pii) { Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN } let(:current_sp) { build(:service_provider) } - let(:instant_verify_state_id_address_result) { nil } + let(:state_id_address_resolution_result) { nil } let(:ipp_enrollment_in_progress) { false } let(:proofer) { instance_double(Proofing::Aamva::Proofer, proof: proofer_result) } let(:proofer_result) do @@ -40,7 +40,7 @@ def sp_cost_count_with_transaction_id plugin.call( applicant_pii:, current_sp:, - instant_verify_state_id_address_result:, + state_id_address_resolution_result:, ipp_enrollment_in_progress:, timer: JobHelpers::Timer.new, ) @@ -60,7 +60,7 @@ def sp_cost_count_with_transaction_id end context 'InstantVerify succeeded' do - let(:instant_verify_state_id_address_result) do + let(:state_id_address_resolution_result) do Proofing::Resolution::Result.new( success: true, vendor_name: 'lexisnexis:instant_verify', @@ -96,7 +96,7 @@ def sp_cost_count_with_transaction_id context 'InstantVerify failed' do context 'and the failure can possibly be covered by AAMVA' do - let(:instant_verify_state_id_address_result) do + let(:state_id_address_resolution_result) do Proofing::Resolution::Result.new( success: false, vendor_name: 'lexisnexis:instant_verify', @@ -120,7 +120,7 @@ def sp_cost_count_with_transaction_id end context 'but the failure cannot be covered by AAMVA' do - let(:instant_verify_state_id_address_result) do + let(:state_id_address_resolution_result) do Proofing::Resolution::Result.new( success: false, vendor_name: 'lexisnexis:instant_verify', @@ -164,7 +164,7 @@ def sp_cost_count_with_transaction_id context 'residential address same as id address' do let(:applicant_pii) { Idp::Constants::MOCK_IDV_APPLICANT_SAME_ADDRESS_AS_ID } - let(:instant_verify_state_id_address_result) do + let(:state_id_address_resolution_result) do Proofing::Resolution::Result.new( success: true, vendor_name: 'lexisnexis:instant_verify', @@ -184,7 +184,7 @@ def sp_cost_count_with_transaction_id context 'InstantVerify failed' do context 'and the failure can possibly be covered by AAMVA' do - let(:instant_verify_state_id_address_result) do + let(:state_id_address_resolution_result) do Proofing::Resolution::Result.new( success: false, vendor_name: 'lexisnexis:instant_verify', @@ -204,7 +204,7 @@ def sp_cost_count_with_transaction_id end context 'but the failure cannot be covered by AAMVA' do - let(:instant_verify_state_id_address_result) do + let(:state_id_address_resolution_result) do Proofing::Resolution::Result.new( success: false, vendor_name: 'lexisnexis:instant_verify', @@ -236,7 +236,7 @@ def sp_cost_count_with_transaction_id context 'InstantVerify succeeded for residential address' do context 'and InstantVerify passed for id address' do - let(:instant_verify_state_id_address_result) do + let(:state_id_address_resolution_result) do Proofing::Resolution::Result.new( success: true, vendor_name: 'lexisnexis:instant_verify', @@ -255,7 +255,7 @@ def sp_cost_count_with_transaction_id context 'and InstantVerify failed for state id address' do context 'but the failure can possibly be covered by AAMVA' do - let(:instant_verify_state_id_address_result) do + let(:state_id_address_resolution_result) do Proofing::Resolution::Result.new( success: false, vendor_name: 'lexisnexis:instant_verify', @@ -275,7 +275,7 @@ def sp_cost_count_with_transaction_id end context 'and the failure cannot be covered by AAMVA' do - let(:instant_verify_state_id_address_result) do + let(:state_id_address_resolution_result) do Proofing::Resolution::Result.new( success: false, vendor_name: 'lexisnexis:instant_verify', diff --git a/spec/services/proofing/resolution/plugins/instant_verify_residential_address_plugin_spec.rb b/spec/services/proofing/resolution/plugins/residential_address_plugin_spec.rb similarity index 61% rename from spec/services/proofing/resolution/plugins/instant_verify_residential_address_plugin_spec.rb rename to spec/services/proofing/resolution/plugins/residential_address_plugin_spec.rb index ed9cab23033..e8af3647ee9 100644 --- a/spec/services/proofing/resolution/plugins/instant_verify_residential_address_plugin_spec.rb +++ b/spec/services/proofing/resolution/plugins/residential_address_plugin_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe Proofing::Resolution::Plugins::InstantVerifyResidentialAddressPlugin do +RSpec.describe Proofing::Resolution::Plugins::ResidentialAddressPlugin do let(:current_sp) { build(:service_provider) } let(:ipp_enrollment_in_progress) { false } @@ -11,12 +11,21 @@ Proofing::Resolution::Result.new( success: true, transaction_id: proofer_transaction_id, - vendor_name: 'lexisnexis:instant_verify', + vendor_name: 'test_resolution_vendor', ) end + let(:proofer) do + instance_double(Proofing::LexisNexis::InstantVerify::Proofer, proof: proofer_result) + end + + let(:sp_cost_token) { :test_cost_token } + subject(:plugin) do - described_class.new + described_class.new( + proofer:, + sp_cost_token:, + ) end describe '#call' do @@ -128,59 +137,4 @@ def sp_cost_count_with_transaction_id end end end - - describe '#proofer' do - subject(:proofer) { plugin.proofer } - - before do - allow(IdentityConfig.store).to receive(:proofer_mock_fallback). - and_return(proofer_mock_fallback) - allow(IdentityConfig.store).to receive(:idv_resolution_default_vendor). - and_return(idv_resolution_default_vendor) - end - - context 'when proofer_mock_fallback is set to true' do - let(:proofer_mock_fallback) { true } - - context 'and idv_resolution_default_vendor is set to :instant_verify' do - let(:idv_resolution_default_vendor) do - :instant_verify - end - - # rubocop:disable Layout/LineLength - it 'creates an Instant Verify proofer because the new setting takes precedence over the old one when the old one is set to its default value' do - expect(proofer).to be_an_instance_of(Proofing::LexisNexis::InstantVerify::Proofer) - end - # rubocop:enable Layout/LineLength - end - - context 'and idv_resolution_default_vendor is set to :mock' do - let(:idv_resolution_default_vendor) { :mock } - - it 'creates a mock proofer because the two settings agree' do - expect(proofer).to be_an_instance_of(Proofing::Mock::ResolutionMockClient) - end - end - end - - context 'when proofer_mock_fallback is set to false' do - let(:proofer_mock_fallback) { false } - - context 'and idv_resolution_default_vendor is set to :instant_verify' do - let(:idv_resolution_default_vendor) { :instant_verify } - - it 'creates an Instant Verify proofer because the two settings agree' do - expect(proofer).to be_an_instance_of(Proofing::LexisNexis::InstantVerify::Proofer) - end - end - - context 'and idv_resolution_default_vendor is set to :mock' do - let(:idv_resolution_default_vendor) { :mock } - - it 'creates an Instant Verify proofer to support transition between configs' do - expect(proofer).to be_an_instance_of(Proofing::LexisNexis::InstantVerify::Proofer) - end - end - end - end end diff --git a/spec/services/proofing/resolution/plugins/instant_verify_state_id_address_plugin_spec.rb b/spec/services/proofing/resolution/plugins/state_id_address_plugin_spec.rb similarity index 68% rename from spec/services/proofing/resolution/plugins/instant_verify_state_id_address_plugin_spec.rb rename to spec/services/proofing/resolution/plugins/state_id_address_plugin_spec.rb index e395a45d64f..de3ed2161bc 100644 --- a/spec/services/proofing/resolution/plugins/instant_verify_state_id_address_plugin_spec.rb +++ b/spec/services/proofing/resolution/plugins/state_id_address_plugin_spec.rb @@ -1,16 +1,16 @@ require 'rails_helper' -RSpec.describe Proofing::Resolution::Plugins::InstantVerifyStateIdAddressPlugin do +RSpec.describe Proofing::Resolution::Plugins::StateIdAddressPlugin do let(:applicant_pii) { Idp::Constants::MOCK_IDV_APPLICANT_SAME_ADDRESS_AS_ID } let(:current_sp) { build(:service_provider) } - let(:instant_verify_residential_address_result) do + let(:residential_address_resolution_result) do Proofing::Resolution::Result.new( success: true, errors: {}, exception: nil, - vendor_name: 'lexisnexis:instant_verify', + vendor_name: 'test_resolution_vendor', ) end @@ -21,12 +21,21 @@ success: true, errors: {}, exception: nil, - vendor_name: 'lexisnexis:instant_verify', + vendor_name: 'test_resolution_vendor', ) end + let(:proofer) do + instance_double(Proofing::LexisNexis::InstantVerify::Proofer, proof: proofer_result) + end + + let(:sp_cost_token) { :test_cost_token } + subject(:plugin) do - described_class.new + described_class.new( + proofer:, + sp_cost_token:, + ) end describe '#call' do @@ -35,15 +44,11 @@ applicant_pii:, current_sp:, ipp_enrollment_in_progress:, - instant_verify_residential_address_result:, + residential_address_resolution_result:, timer: JobHelpers::Timer.new, ) end - before do - allow(plugin.proofer).to receive(:proof).and_return(proofer_result) - end - context 'remote unsupervised proofing' do let(:ipp_enrollment_in_progress) { false } @@ -61,18 +66,17 @@ it 'passes state id address to proofer' do expect(plugin.proofer). to receive(:proof). - with(hash_including(state_id_address)). - and_call_original + with(hash_including(state_id_address)) call end - context 'when InstantVerify call succeeds' do + context 'when vendor call succeeds' do it 'returns the proofer result' do expect(call).to eql(proofer_result) end - it 'records a LexisNexis SP cost' do + it 'records correct SP cost' do expect { call }. to change { SpCost.where( @@ -83,13 +87,13 @@ end end - context 'when InstantVerify call fails' do + context 'when vendor call fails' do let(:proofer_result) do Proofing::Resolution::Result.new( success: false, errors: {}, exception: nil, - vendor_name: 'lexisnexis:instant_verify', + vendor_name: 'test_resolution_vendor', ) end @@ -108,7 +112,7 @@ end end - context 'when InstantVerify call results in exception' do + context 'when vendor call results in exception' do let(:proofer_result) do Proofing::Resolution::Result.new( success: false, @@ -139,7 +143,7 @@ it 'reuses residential address result' do result = call expect(plugin.proofer).not_to have_received(:proof) - expect(result).to eql(instant_verify_residential_address_result) + expect(result).to eql(residential_address_resolution_result) end it 'does not add a new LexisNexis SP cost (since residential address result was reused)' do @@ -176,15 +180,14 @@ } end - context 'LexisNexis InstantVerify passes for residential address' do - it 'calls the InstantVerify Proofer with state id address' do - expect(plugin.proofer).to receive(:proof).with(hash_including(state_id_address)). - and_call_original + context 'LexisNexis vendor passes for residential address' do + it 'calls the vendor Proofer with state id address' do + expect(plugin.proofer).to receive(:proof).with(hash_including(state_id_address)) call end - context 'when InstantVerify call succeeds' do + context 'when vendor call succeeds' do it 'returns the proofer result' do expect(call).to eql(proofer_result) end @@ -200,7 +203,7 @@ end end - context 'when InstantVerify call fails' do + context 'when vendor call fails' do let(:proofer_result) do Proofing::Resolution::Result.new( success: false, @@ -225,7 +228,7 @@ end end - context 'when InstantVerify call results in exception' do + context 'when vendor call results in exception' do let(:proofer_result) do Proofing::Resolution::Result.new( success: false, @@ -250,8 +253,8 @@ end end - context 'LexisNexis InstantVerify failed for residential address' do - let(:instant_verify_residential_address_result) do + context 'LexisNexis vendor failed for residential address' do + let(:residential_address_resolution_result) do Proofing::Resolution::Result.new( success: false, errors: {}, @@ -286,59 +289,4 @@ end end end - - describe '#proofer' do - subject(:proofer) { plugin.proofer } - - before do - allow(IdentityConfig.store).to receive(:proofer_mock_fallback). - and_return(proofer_mock_fallback) - allow(IdentityConfig.store).to receive(:idv_resolution_default_vendor). - and_return(idv_resolution_default_vendor) - end - - context 'when proofer_mock_fallback is set to true' do - let(:proofer_mock_fallback) { true } - - context 'and idv_resolution_default_vendor is set to :instant_verify' do - let(:idv_resolution_default_vendor) do - :instant_verify - end - - # rubocop:disable Layout/LineLength - it 'creates an Instant Verify proofer because the new setting takes precedence over the old one when the old one is set to its default value' do - expect(proofer).to be_an_instance_of(Proofing::LexisNexis::InstantVerify::Proofer) - end - # rubocop:enable Layout/LineLength - end - - context 'and idv_resolution_default_vendor is set to :mock' do - let(:idv_resolution_default_vendor) { :mock } - - it 'creates a mock proofer because the two settings agree' do - expect(proofer).to be_an_instance_of(Proofing::Mock::ResolutionMockClient) - end - end - end - - context 'when proofer_mock_fallback is set to false' do - let(:proofer_mock_fallback) { false } - - context 'and idv_resolution_default_vendor is set to :instant_verify' do - let(:idv_resolution_default_vendor) { :instant_verify } - - it 'creates an Instant Verify proofer because the two settings agree' do - expect(proofer).to be_an_instance_of(Proofing::LexisNexis::InstantVerify::Proofer) - end - end - - context 'and idv_resolution_default_vendor is set to :mock' do - let(:idv_resolution_default_vendor) { :mock } - - it 'creates an Instant Verify proofer to support transition between configs' do - expect(proofer).to be_an_instance_of(Proofing::LexisNexis::InstantVerify::Proofer) - end - end - end - end end diff --git a/spec/services/proofing/resolution/progressive_proofer_spec.rb b/spec/services/proofing/resolution/progressive_proofer_spec.rb index 10723e687da..5b7eab4a20a 100644 --- a/spec/services/proofing/resolution/progressive_proofer_spec.rb +++ b/spec/services/proofing/resolution/progressive_proofer_spec.rb @@ -1,116 +1,14 @@ require 'rails_helper' RSpec.describe Proofing::Resolution::ProgressiveProofer do - let(:applicant_pii) { Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN } - let(:ipp_enrollment_in_progress) { false } - let(:request_ip) { Faker::Internet.ip_v4_address } - let(:threatmetrix_session_id) { SecureRandom.uuid } - let(:user_email) { Faker::Internet.email } - let(:current_sp) { build(:service_provider) } - - let(:instant_verify_residential_address_plugin) do - Proofing::Resolution::Plugins::InstantVerifyResidentialAddressPlugin.new - end - - let(:instant_verify_residential_address_result) do - Proofing::Resolution::Result.new( - success: true, - transaction_id: 'iv-residential', - ) - end - - let(:instant_verify_residential_address_proofer) do - instance_double( - Proofing::LexisNexis::InstantVerify::Proofer, - proof: instant_verify_residential_address_result, - ) - end - - let(:instant_verify_state_id_address_plugin) do - Proofing::Resolution::Plugins::InstantVerifyStateIdAddressPlugin.new - end - - let(:instant_verify_state_id_address_result) do - Proofing::Resolution::Result.new( - success: true, - transaction_id: 'iv-state-id', - ) - end - - let(:instant_verify_state_id_address_proofer) do - instance_double( - Proofing::LexisNexis::InstantVerify::Proofer, - proof: instant_verify_state_id_address_result, - ) - end - - let(:aamva_plugin) { Proofing::Resolution::Plugins::AamvaPlugin.new } - - let(:aamva_result) do - Proofing::StateIdResult.new( - success: false, - transaction_id: 'aamva-123', - ) - end - - let(:aamva_proofer) { instance_double(Proofing::Aamva::Proofer, proof: aamva_result) } - - let(:threatmetrix_plugin) do - Proofing::Resolution::Plugins::ThreatMetrixPlugin.new - end - - let(:threatmetrix_result) do - Proofing::DdpResult.new( - success: true, - transaction_id: 'ddp-123', - ) - end - - let(:threatmetrix_proofer) do - instance_double( - Proofing::LexisNexis::Ddp::Proofer, - proof: threatmetrix_result, - ) - end - subject(:progressive_proofer) { described_class.new } - before do - allow(progressive_proofer).to receive(:threatmetrix_plugin).and_return(threatmetrix_plugin) - allow(threatmetrix_plugin).to receive(:proofer).and_return(threatmetrix_proofer) - - allow(progressive_proofer).to receive(:aamva_plugin).and_return(aamva_plugin) - allow(aamva_plugin).to receive(:proofer).and_return(aamva_proofer) - - allow(progressive_proofer).to receive(:instant_verify_residential_address_plugin). - and_return(instant_verify_residential_address_plugin) - allow(instant_verify_residential_address_plugin).to receive(:proofer). - and_return(instant_verify_residential_address_proofer) - - allow(progressive_proofer).to receive(:instant_verify_state_id_address_plugin). - and_return(instant_verify_state_id_address_plugin) - allow(instant_verify_state_id_address_plugin).to receive(:proofer). - and_return(instant_verify_state_id_address_proofer) - end - it 'assigns aamva_plugin' do expect(described_class.new.aamva_plugin).to be_a( Proofing::Resolution::Plugins::AamvaPlugin, ) end - it 'assigns instant_verify_residential_address_plugin' do - expect(described_class.new.instant_verify_residential_address_plugin).to be_a( - Proofing::Resolution::Plugins::InstantVerifyResidentialAddressPlugin, - ) - end - - it 'assigns instant_verify_state_id_address_plugin' do - expect(described_class.new.instant_verify_state_id_address_plugin).to be_a( - Proofing::Resolution::Plugins::InstantVerifyStateIdAddressPlugin, - ) - end - it 'assigns threatmetrix_plugin' do expect(described_class.new.threatmetrix_plugin).to be_a( Proofing::Resolution::Plugins::ThreatMetrixPlugin, @@ -118,6 +16,62 @@ end describe '#proof' do + let(:applicant_pii) { Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN } + let(:ipp_enrollment_in_progress) { false } + let(:request_ip) { Faker::Internet.ip_v4_address } + let(:threatmetrix_session_id) { SecureRandom.uuid } + let(:user_email) { Faker::Internet.email } + let(:current_sp) { build(:service_provider) } + + let(:residential_address_resolution_result) do + Proofing::Resolution::Result.new( + success: true, + transaction_id: 'residential-resolution-tx', + ) + end + + let(:state_id_address_resolution_result) do + Proofing::Resolution::Result.new( + success: true, + transaction_id: 'state-id-resolution-tx', + ) + end + + let(:resolution_proofing_results) do + # In cases where both calls are made, the residential call is made + # before the state id address call + [residential_address_resolution_result, state_id_address_resolution_result] + end + + let(:resolution_proofer) do + instance_double( + Proofing::LexisNexis::InstantVerify::Proofer, + ) + end + + let(:aamva_result) do + Proofing::StateIdResult.new( + success: false, + transaction_id: 'aamva-123', + ) + end + + let(:aamva_proofer) { instance_double(Proofing::Aamva::Proofer, proof: aamva_result) } + + let(:threatmetrix_result) do + Proofing::DdpResult.new( + success: true, + transaction_id: 'ddp-123', + ) + end + + let(:threatmetrix_proofer) do + instance_double( + Proofing::LexisNexis::Ddp::Proofer, + proof: threatmetrix_result, + ) + end + subject(:proof) do progressive_proofer.proof( applicant_pii:, @@ -130,20 +84,37 @@ ) end + before do + allow(resolution_proofer).to receive(:proof).and_return(*resolution_proofing_results) + allow(progressive_proofer).to receive(:create_proofer). + and_return(resolution_proofer) + + allow(progressive_proofer.threatmetrix_plugin).to receive(:proofer). + and_return(threatmetrix_proofer) + + allow(progressive_proofer.aamva_plugin).to receive(:proofer). + and_return(aamva_proofer) + end + context 'remote unsupervised proofing' do + let(:resolution_proofing_results) do + # No call is made for residential address on remote unsupervised path + [state_id_address_resolution_result] + end + it 'calls AamvaPlugin' do - expect(aamva_plugin).to receive(:call).with( + expect(progressive_proofer.aamva_plugin).to receive(:call).with( applicant_pii:, current_sp:, - instant_verify_state_id_address_result:, + state_id_address_resolution_result:, ipp_enrollment_in_progress: false, timer: an_instance_of(JobHelpers::Timer), ) proof end - it 'calls InstantVerifyResidentialAddressPlugin' do - expect(instant_verify_residential_address_plugin).to receive(:call).with( + it 'calls ResidentialAddressPlugin' do + expect(progressive_proofer.residential_address_plugin).to receive(:call).with( applicant_pii:, current_sp:, ipp_enrollment_in_progress: false, @@ -152,11 +123,11 @@ proof end - it 'calls InstantVerifyStateIdAddressPlugin' do - expect(instant_verify_state_id_address_plugin).to receive(:call).with( + it 'calls StateIdAddressPlugin' do + expect(progressive_proofer.state_id_address_plugin).to receive(:call).with( applicant_pii:, current_sp:, - instant_verify_residential_address_result: satisfy do |result| + residential_address_resolution_result: satisfy do |result| expect(result.success?).to eql(true) expect(result.vendor_name).to eql('ResidentialAddressNotRequired') end, @@ -167,7 +138,7 @@ end it 'calls ThreatMetrixPlugin' do - expect(threatmetrix_plugin).to receive(:call).with( + expect(progressive_proofer.threatmetrix_plugin).to receive(:call).with( applicant_pii:, current_sp:, request_ip:, @@ -182,7 +153,7 @@ proof.tap do |result| expect(result).to be_an_instance_of(Proofing::Resolution::ResultAdjudicator) - expect(result.resolution_result).to eql(instant_verify_state_id_address_result) + expect(result.resolution_result).to eql(state_id_address_resolution_result) expect(result.state_id_result).to eql(aamva_result) expect(result.device_profiling_result).to eql(threatmetrix_result) expect(result.residential_resolution_result).to satisfy do |result| @@ -201,15 +172,15 @@ context 'residential address is same as id' do let(:applicant_pii) { Idp::Constants::MOCK_IDV_APPLICANT_SAME_ADDRESS_AS_ID } - let(:instant_verify_state_id_address_result) do - instant_verify_residential_address_result + let(:state_id_address_resolution_result) do + residential_address_resolution_result end it 'calls AamvaPlugin' do - expect(aamva_plugin).to receive(:call).with( + expect(progressive_proofer.aamva_plugin).to receive(:call).with( applicant_pii:, current_sp:, - instant_verify_state_id_address_result:, + state_id_address_resolution_result:, ipp_enrollment_in_progress: true, timer: an_instance_of(JobHelpers::Timer), ) @@ -217,8 +188,8 @@ proof end - it 'calls InstantVerifyResidentialAddressPlugin' do - expect(instant_verify_residential_address_plugin).to receive(:call).with( + it 'calls ResidentialAddressPlugin' do + expect(progressive_proofer.residential_address_plugin).to receive(:call).with( applicant_pii:, current_sp:, ipp_enrollment_in_progress: true, @@ -227,11 +198,11 @@ proof end - it 'calls InstantVerifyStateIdAddressPlugin' do - expect(instant_verify_state_id_address_plugin).to receive(:call).with( + it 'calls StateIdAddressPlugin' do + expect(progressive_proofer.state_id_address_plugin).to receive(:call).with( applicant_pii:, current_sp:, - instant_verify_residential_address_result:, + residential_address_resolution_result:, ipp_enrollment_in_progress: true, timer: an_instance_of(JobHelpers::Timer), ).and_call_original @@ -239,7 +210,7 @@ end it 'calls ThreatMetrixPlugin' do - expect(threatmetrix_plugin).to receive(:call).with( + expect(progressive_proofer.threatmetrix_plugin).to receive(:call).with( applicant_pii:, current_sp:, request_ip:, @@ -254,11 +225,11 @@ proof.tap do |result| expect(result).to be_an_instance_of(Proofing::Resolution::ResultAdjudicator) - expect(result.resolution_result).to eql(instant_verify_state_id_address_result) + expect(result.resolution_result).to eql(state_id_address_resolution_result) expect(result.state_id_result).to eql(aamva_result) expect(result.device_profiling_result).to eql(threatmetrix_result) expect(result.residential_resolution_result).to( - eql(instant_verify_state_id_address_result), + eql(state_id_address_resolution_result), ) expect(result.ipp_enrollment_in_progress).to eql(true) expect(proof.same_address_as_id).to eq(applicant_pii[:same_address_as_id]) @@ -270,7 +241,7 @@ let(:applicant_pii) { Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_ADDRESS } it 'calls ThreatMetrixPlugin' do - expect(threatmetrix_plugin).to receive(:call).with( + expect(progressive_proofer.threatmetrix_plugin).to receive(:call).with( applicant_pii:, current_sp:, request_ip:, @@ -281,8 +252,8 @@ proof end - it 'calls InstantVerifyResidentialAddressPlugin' do - expect(instant_verify_residential_address_plugin).to receive(:call).with( + it 'calls ResidentialAddressPlugin' do + expect(progressive_proofer.residential_address_plugin).to receive(:call).with( applicant_pii:, current_sp:, ipp_enrollment_in_progress: true, @@ -291,11 +262,11 @@ proof end - it 'calls InstantVerifyStateIdAddressPlugin' do - expect(instant_verify_state_id_address_plugin).to receive(:call).with( + it 'calls StateIdAddressPlugin' do + expect(progressive_proofer.state_id_address_plugin).to receive(:call).with( applicant_pii:, current_sp:, - instant_verify_residential_address_result:, + residential_address_resolution_result:, ipp_enrollment_in_progress: true, timer: an_instance_of(JobHelpers::Timer), ).and_call_original @@ -303,10 +274,10 @@ end it 'calls AamvaPlugin' do - expect(aamva_plugin).to receive(:call).with( + expect(progressive_proofer.aamva_plugin).to receive(:call).with( applicant_pii:, current_sp:, - instant_verify_state_id_address_result:, + state_id_address_resolution_result:, ipp_enrollment_in_progress: true, timer: an_instance_of(JobHelpers::Timer), ).and_call_original @@ -316,11 +287,11 @@ it 'returns a ResultAdjudicator' do proof.tap do |result| expect(result).to be_an_instance_of(Proofing::Resolution::ResultAdjudicator) - expect(result.resolution_result).to eql(instant_verify_state_id_address_result) + expect(result.resolution_result).to eql(state_id_address_resolution_result) expect(result.state_id_result).to eql(aamva_result) expect(result.device_profiling_result).to eql(threatmetrix_result) expect(result.residential_resolution_result).to( - eql(instant_verify_residential_address_result), + eql(residential_address_resolution_result), ) expect(result.ipp_enrollment_in_progress).to eql(true) expect(result.same_address_as_id).to eql('false') @@ -339,12 +310,15 @@ it 'does not pass the phone number to plugins' do expected_applicant_pii = applicant_pii.except(:best_effort_phone_number_for_socure) - [ - aamva_plugin, - instant_verify_residential_address_plugin, - instant_verify_state_id_address_plugin, - threatmetrix_plugin, - ].each do |plugin| + plugin_methods = %i[ + aamva_plugin + residential_address_plugin + state_id_address_plugin + threatmetrix_plugin + ] + + plugin_methods.each do |plugin_method_name| + plugin = progressive_proofer.send(plugin_method_name) expect(plugin).to receive(:call).with( hash_including( applicant_pii: expected_applicant_pii, @@ -356,4 +330,166 @@ end end end + + describe '#proofing_vendor' do + let(:idv_resolution_default_vendor) { :default_vendor } + let(:idv_resolution_alternate_vendor) { :alternate_vendor } + let(:idv_resolution_alternate_vendor_percent) { 0 } + + subject(:proofing_vendor) { progressive_proofer.proofing_vendor } + + before do + allow(IdentityConfig.store).to receive(:idv_resolution_default_vendor). + and_return(idv_resolution_default_vendor) + allow(IdentityConfig.store).to receive(:idv_resolution_alternate_vendor). + and_return(idv_resolution_alternate_vendor) + allow(IdentityConfig.store).to receive(:idv_resolution_alternate_vendor_percent). + and_return(idv_resolution_alternate_vendor_percent) + end + + context 'when default is set to 100%' do + it 'uses the default' do + expect(proofing_vendor).to eql(:default_vendor) + end + end + + context 'when alternate is set to 100%' do + let(:idv_resolution_alternate_vendor_percent) { 100 } + + it 'uses the alternate' do + expect(proofing_vendor).to eql(:alternate_vendor) + end + end + + context 'when no alternate is set' do + let(:idv_resolution_alternate_vendor) { :none } + + it 'uses default' do + expect(proofing_vendor).to eql(:default_vendor) + end + + context 'and alternate is set to > 0' do + let(:idv_resolution_alternate_vendor_percent) { 100 } + it 'uses default' do + expect(proofing_vendor).to eql(:default_vendor) + end + end + end + end + + describe '#residential_address_plugin' do + let(:proofing_vendor) { nil } + + before do + allow(progressive_proofer).to receive(:proofing_vendor).and_return(proofing_vendor) + end + + context 'when proofing_vendor is :instant_verify' do + let(:proofing_vendor) { :instant_verify } + + it 'returns ResidentialAddressPlugin with an InstantVerify proofer' do + expect(progressive_proofer.residential_address_plugin).to be_an_instance_of( + Proofing::Resolution::Plugins::ResidentialAddressPlugin, + ) + + expect(progressive_proofer.residential_address_plugin.proofer).to be_an_instance_of( + Proofing::LexisNexis::InstantVerify::Proofer, + ) + end + end + + context 'when proofing_vendor is :mock' do + let(:proofing_vendor) { :mock } + + it 'returns ResidentialAddressPlugin with a mock proofer' do + expect(progressive_proofer.residential_address_plugin).to be_an_instance_of( + Proofing::Resolution::Plugins::ResidentialAddressPlugin, + ) + + expect(progressive_proofer.residential_address_plugin.proofer).to be_an_instance_of( + Proofing::Mock::ResolutionMockClient, + ) + end + end + + context 'when proofing_vendor is :socure_kyc' do + let(:proofing_vendor) { :socure_kyc } + + it 'returns ResidentialAddressPlugin with a Socure proofer' do + expect(progressive_proofer.residential_address_plugin).to be_an_instance_of( + Proofing::Resolution::Plugins::ResidentialAddressPlugin, + ) + + expect(progressive_proofer.residential_address_plugin.proofer).to be_an_instance_of( + Proofing::Socure::IdPlus::Proofer, + ) + end + end + + context 'when proofing_vendor is another value' do + let(:proofing_vendor) { :a_dog } + + it 'raises an error' do + expect { progressive_proofer.residential_address_plugin }.to raise_error + end + end + end + + describe '#state_id_address_plugin' do + let(:proofing_vendor) { nil } + + before do + allow(progressive_proofer).to receive(:proofing_vendor).and_return(proofing_vendor) + end + + context 'when proofing_vendor is :instant_verify' do + let(:proofing_vendor) { :instant_verify } + + it 'returns StateIdAddressPlugin with an InstantVerify proofer' do + expect(progressive_proofer.state_id_address_plugin).to be_an_instance_of( + Proofing::Resolution::Plugins::StateIdAddressPlugin, + ) + + expect(progressive_proofer.state_id_address_plugin.proofer).to be_an_instance_of( + Proofing::LexisNexis::InstantVerify::Proofer, + ) + end + end + + context 'when proofing_vendor is :socure_kyc' do + let(:proofing_vendor) { :socure_kyc } + + it 'returns StateIdAddressPlugin with a Socure proofer' do + expect(progressive_proofer.state_id_address_plugin).to be_an_instance_of( + Proofing::Resolution::Plugins::StateIdAddressPlugin, + ) + + expect(progressive_proofer.state_id_address_plugin.proofer).to be_an_instance_of( + Proofing::Socure::IdPlus::Proofer, + ) + end + end + + context 'when proofing_vendor is :mock' do + let(:proofing_vendor) { :mock } + + it 'returns StateIdAddressPlugin with a mock proofer' do + expect(progressive_proofer.state_id_address_plugin).to be_an_instance_of( + Proofing::Resolution::Plugins::StateIdAddressPlugin, + ) + + expect(progressive_proofer.state_id_address_plugin.proofer).to be_an_instance_of( + Proofing::Mock::ResolutionMockClient, + ) + end + end + + context 'when proofing_vendor is another value' do + let(:proofing_vendor) { :🦨 } + + it 'raises an error' do + expect { progressive_proofer.state_id_address_plugin }.to raise_error + end + end + end end diff --git a/spec/services/proofing/socure/id_plus/proofer_spec.rb b/spec/services/proofing/socure/id_plus/proofer_spec.rb index 9393dd588c2..a95c06893e9 100644 --- a/spec/services/proofing/socure/id_plus/proofer_spec.rb +++ b/spec/services/proofing/socure/id_plus/proofer_spec.rb @@ -172,6 +172,17 @@ end end + context 'when applicant includes extra fields' do + let(:applicant) do + { + some_weird_field_the_proofer_is_not_expecting: ':ohno:', + } + end + it 'does not raise an error' do + expect { result }.not_to raise_error + end + end + context 'when request times out' do before do stub_request(:post, URI.join(base_url, '/api/3.0/EmailAuthScore').to_s). @@ -224,7 +235,7 @@ end it 'includes exception details' do - expect(result.exception).to be_an_instance_of(Proofing::Socure::IdPlus::RequestError) + expect(result.exception).to be_an_instance_of(Proofing::Socure::IdPlus::Request::Error) end end end @@ -253,7 +264,7 @@ end it 'includes exception details' do - expect(result.exception).to be_an_instance_of(Proofing::Socure::IdPlus::RequestError) + expect(result.exception).to be_an_instance_of(Proofing::Socure::IdPlus::Request::Error) end end end @@ -278,7 +289,7 @@ end it 'includes exception details' do - expect(result.exception).to be_an_instance_of(Proofing::Socure::IdPlus::RequestError) + expect(result.exception).to be_an_instance_of(Proofing::Socure::IdPlus::Request::Error) end end end diff --git a/spec/services/proofing/socure/id_plus/request_spec.rb b/spec/services/proofing/socure/id_plus/request_spec.rb index 7ff73f5d5e8..333ee26285d 100644 --- a/spec/services/proofing/socure/id_plus/request_spec.rb +++ b/spec/services/proofing/socure/id_plus/request_spec.rb @@ -148,20 +148,20 @@ ) end - it 'raises RequestError' do + it 'raises Request::Error' do expect do request.send_request end.to raise_error( - Proofing::Socure::IdPlus::RequestError, + Proofing::Socure::IdPlus::Request::Error, 'Request-specific error message goes here (400)', ) end - it 'includes reference_id on RequestError' do + it 'includes reference_id on Request::Error' do expect do request.send_request end.to raise_error( - Proofing::Socure::IdPlus::RequestError, + Proofing::Socure::IdPlus::Request::Error, ) do |err| expect(err.reference_id).to eql('a-big-unique-reference-id') end @@ -186,11 +186,11 @@ ) end - it 'raises RequestError' do + it 'raises Request::Error' do expect do request.send_request end.to raise_error( - Proofing::Socure::IdPlus::RequestError, + Proofing::Socure::IdPlus::Request::Error, 'Request-specific error message goes here (401)', ) end @@ -205,10 +205,10 @@ ) end - it 'raises RequestError' do + it 'raises Request::Error' do expect do request.send_request - end.to raise_error(Proofing::Socure::IdPlus::RequestError) + end.to raise_error(Proofing::Socure::IdPlus::Request::Error) end end @@ -229,8 +229,8 @@ to_raise(Errno::ECONNRESET) end - it 'raises a RequestError' do - expect { request.send_request }.to raise_error Proofing::Socure::IdPlus::RequestError + it 'raises a Request::Error' do + expect { request.send_request }.to raise_error Proofing::Socure::IdPlus::Request::Error end end end diff --git a/spec/services/reporting/agency_and_sp_report_spec.rb b/spec/services/reporting/agency_and_sp_report_spec.rb index 9711359f667..7ed81700074 100644 --- a/spec/services/reporting/agency_and_sp_report_spec.rb +++ b/spec/services/reporting/agency_and_sp_report_spec.rb @@ -53,7 +53,8 @@ [ header_row, ['Auth', 1, 1], - ['IDV', 0, 0], + ['IDV (Facial matching)', 0, 0], + ['IDV (Legacy IDV)', 0, 0], ['Total', 1, 1], ] end @@ -69,7 +70,8 @@ [ header_row, ['Auth', 0, 1], - ['IDV', 0, 0], + ['IDV (Facial matching)', 0, 0], + ['IDV (Legacy IDV)', 0, 0], ['Total', 0, 1], ] end @@ -94,7 +96,8 @@ [ header_row, ['Auth', 1, 1], - ['IDV', 0, 0], + ['IDV (Facial matching)', 0, 0], + ['IDV (Legacy IDV)', 0, 0], ['Total', 1, 1], ] end @@ -103,7 +106,8 @@ [ header_row, ['Auth', 1, 0], - ['IDV', 1, 1], + ['IDV (Facial matching)', 0, 0], + ['IDV (Legacy IDV)', 1, 1], ['Total', 2, 1], ] end @@ -127,7 +131,7 @@ end context 'when adding an IDV SP' do - let!(:idv_sp) do + let!(:idv_legacy_sp) do create( :service_provider, :external, @@ -138,15 +142,32 @@ ) end + let!(:idv_facial_match_sp) do + create( + :service_provider, + :external, + :idv, + :active, + agency:, + identities: [build(:service_provider_identity, service_provider: 'https://facialmatch.com')], + ) + end + let(:expected_report) do [ header_row, ['Auth', 0, 0], - ['IDV', 1, 1], - ['Total', 1, 1], + ['IDV (Facial matching)', 1, 1], + ['IDV (Legacy IDV)', 1, 0], + ['Total', 2, 1], ] end + before do + allow_any_instance_of(Reporting::AgencyAndSpReport).to receive(:facial_match_issuers). + and_return([idv_facial_match_sp.issuer]) + end + it 'counts the SP and its Agency as IDV' do expect(subject).to match_array(expected_report) end diff --git a/spec/services/reporting/total_user_count_report_spec.rb b/spec/services/reporting/total_user_count_report_spec.rb index 9135cd25e67..1f4ba98ef25 100644 --- a/spec/services/reporting/total_user_count_report_spec.rb +++ b/spec/services/reporting/total_user_count_report_spec.rb @@ -8,26 +8,43 @@ let(:expected_report) do [ - ['Metric', 'All Users', 'Verified users', 'Time Range Start', 'Time Range End'], - ['All-time count', expected_total_count, expected_verified_count, '-', Date.new(2021, 3, 1)], + [ + 'Metric', + 'All Users', + 'Verified users (Legacy IDV)', + 'Verified users (Facial Matching)', + 'Time Range Start', + 'Time Range End', + ], + [ + 'All-time count', + expected_total_count, + expected_verified_legacy_idv_count, + expected_verified_facial_match_count, + '-', + Date.new(2021, 3, 1), + ], [ 'All-time fully registered', expected_total_fully_registered, '-', '-', + '-', Date.new(2021, 3, 1), ], [ 'New users count', expected_new_count, - expected_new_verified_count, + expected_new_verified_legacy_idv_count, + expected_new_verified_facial_match_count, Date.new(2021, 3, 1), Date.new(2021, 3, 31), ], [ 'Annual users count', expected_annual_count, - expected_annual_verified_count, + expected_annual_verified_legacy_idv_count, + expected_annual_verified_facial_match_count, Date.new(2020, 10, 1), Date.new(2021, 9, 30), ], @@ -50,12 +67,15 @@ context 'with only a non-verified user' do before { create(:user) } let(:expected_total_count) { 1 } - let(:expected_verified_count) { 0 } + let(:expected_verified_legacy_idv_count) { 0 } + let(:expected_verified_facial_match_count) { 0 } let(:expected_total_fully_registered) { 0 } let(:expected_new_count) { 1 } - let(:expected_new_verified_count) { 0 } + let(:expected_new_verified_legacy_idv_count) { 0 } + let(:expected_new_verified_facial_match_count) { 0 } let(:expected_annual_count) { expected_total_count } - let(:expected_annual_verified_count) { 0 } + let(:expected_annual_verified_legacy_idv_count) { 0 } + let(:expected_annual_verified_facial_match_count) { 0 } it_behaves_like 'a report with the specified counts' end @@ -65,17 +85,20 @@ let!(:old_user) { create(:user, created_at: report_date - 13.months) } let(:expected_total_count) { 2 } - let(:expected_verified_count) { 0 } + let(:expected_verified_legacy_idv_count) { 0 } + let(:expected_verified_facial_match_count) { 0 } let(:expected_total_fully_registered) { 0 } let(:expected_new_count) { 1 } - let(:expected_new_verified_count) { 0 } + let(:expected_new_verified_legacy_idv_count) { 0 } + let(:expected_new_verified_facial_match_count) { 0 } let(:expected_annual_count) { 1 } - let(:expected_annual_verified_count) { 0 } + let(:expected_annual_verified_legacy_idv_count) { 0 } + let(:expected_annual_verified_facial_match_count) { 0 } it_behaves_like 'a report with the specified counts' end - context 'with one verified and one non-verified user' do + context 'with one legacy verified and one non-verified user' do before do user1 = create(:user) user2 = create(:user) @@ -86,12 +109,37 @@ user2.profiles.first.deactivate(:password_reset) end let(:expected_total_count) { 2 } - let(:expected_verified_count) { 1 } + let(:expected_verified_legacy_idv_count) { 1 } + let(:expected_verified_facial_match_count) { 0 } + let(:expected_total_fully_registered) { 0 } + let(:expected_new_count) { 2 } + let(:expected_new_verified_legacy_idv_count) { 1 } + let(:expected_new_verified_facial_match_count) { 0 } + let(:expected_annual_count) { expected_total_count } + let(:expected_annual_verified_legacy_idv_count) { 1 } + let(:expected_annual_verified_facial_match_count) { 0 } + + it_behaves_like 'a report with the specified counts' + end + + context 'with one facial match verified and one non-verified user' do + before do + user1 = create(:user) + user2 = create(:user) + create(:profile, :active, :facial_match_proof, user: user1) + create(:profile, :active, :verified, user: user2) + user2.profiles.first.deactivate(:password_reset) + end + let(:expected_total_count) { 2 } + let(:expected_verified_legacy_idv_count) { 0 } + let(:expected_verified_facial_match_count) { 1 } let(:expected_total_fully_registered) { 0 } let(:expected_new_count) { 2 } - let(:expected_new_verified_count) { 1 } + let(:expected_new_verified_legacy_idv_count) { 0 } + let(:expected_new_verified_facial_match_count) { 1 } let(:expected_annual_count) { expected_total_count } - let(:expected_annual_verified_count) { 1 } + let(:expected_annual_verified_legacy_idv_count) { 0 } + let(:expected_annual_verified_facial_match_count) { 1 } it_behaves_like 'a report with the specified counts' end @@ -104,12 +152,15 @@ # A suspended user is still a total user: let(:expected_total_count) { 1 } - let(:expected_verified_count) { 0 } + let(:expected_verified_legacy_idv_count) { 0 } + let(:expected_verified_facial_match_count) { 0 } let(:expected_total_fully_registered) { 0 } let(:expected_new_count) { 1 } - let(:expected_new_verified_count) { 0 } + let(:expected_new_verified_legacy_idv_count) { 0 } + let(:expected_new_verified_facial_match_count) { 0 } let(:expected_annual_count) { 1 } - let(:expected_annual_verified_count) { 0 } + let(:expected_annual_verified_legacy_idv_count) { 0 } + let(:expected_annual_verified_facial_match_count) { 0 } it_behaves_like 'a report with the specified counts' end @@ -119,12 +170,15 @@ # A user with a fraud rejection is still a total user let(:expected_total_count) { 1 } - let(:expected_verified_count) { 0 } + let(:expected_verified_legacy_idv_count) { 0 } + let(:expected_verified_facial_match_count) { 0 } let(:expected_total_fully_registered) { 1 } let(:expected_new_count) { 1 } - let(:expected_new_verified_count) { 0 } + let(:expected_new_verified_legacy_idv_count) { 0 } + let(:expected_new_verified_facial_match_count) { 0 } let(:expected_annual_count) { 1 } - let(:expected_annual_verified_count) { 0 } + let(:expected_annual_verified_legacy_idv_count) { 0 } + let(:expected_annual_verified_facial_match_count) { 0 } it_behaves_like 'a report with the specified counts' end @@ -137,12 +191,15 @@ end end let(:expected_total_count) { 3 } - let(:expected_verified_count) { 0 } + let(:expected_verified_legacy_idv_count) { 0 } + let(:expected_verified_facial_match_count) { 0 } let(:expected_total_fully_registered) { 2 } let(:expected_new_count) { 3 } - let(:expected_new_verified_count) { 0 } + let(:expected_new_verified_legacy_idv_count) { 0 } + let(:expected_new_verified_facial_match_count) { 0 } let(:expected_annual_count) { 3 } - let(:expected_annual_verified_count) { 0 } + let(:expected_annual_verified_legacy_idv_count) { 0 } + let(:expected_annual_verified_facial_match_count) { 0 } it_behaves_like 'a report with the specified counts' end diff --git a/spec/services/saml_request_validator_spec.rb b/spec/services/saml_request_validator_spec.rb index 3a6fc370fea..bf530ff771d 100644 --- a/spec/services/saml_request_validator_spec.rb +++ b/spec/services/saml_request_validator_spec.rb @@ -32,9 +32,6 @@ ).and_return( use_vot_in_sp_requests, ) - allow(IdentityConfig.store).to receive( - :allowed_biometric_ial_providers, - ).and_return([issuer]) end context 'valid authn context and sp and authorized nameID format' do diff --git a/spec/support/features/document_capture_step_helper.rb b/spec/support/features/document_capture_step_helper.rb index 179b333acfb..f5b82332515 100644 --- a/spec/support/features/document_capture_step_helper.rb +++ b/spec/support/features/document_capture_step_helper.rb @@ -71,4 +71,73 @@ def api_image_submission_test_credential_part def click_try_again click_spinner_button_and_wait t('idv.failure.button.warning') end + + def socure_docv_upload_documents(docv_transaction_token:) + [ + 'WAITING_FOR_USER_TO_REDIRECT', + 'APP_OPENED', + 'DOCUMENT_FRONT_UPLOADED', + 'DOCUMENT_BACK_UPLOADED', + 'DOCUMENTS_UPLOADED', + 'SESSION_COMPLETE', + ].each { |event_type| socure_docv_send_webhook(docv_transaction_token:, event_type:) } + end + + def socure_docv_send_webhook( + docv_transaction_token:, + event_type: 'DOCUMENTS_UPLOADED' + ) + Faraday.post "http://#{[page.server.host, + page.server.port].join(':')}/api/webhooks/socure/event" do |req| + req.body = { + event: { + eventType: event_type, + docvTransactionToken: docv_transaction_token, + }, + }.to_json + req.headers = { + 'Content-Type': 'application/json', + Authorization: "secret #{IdentityConfig.store.socure_docv_webhook_secret_key}", + } + req.options.context = { service_name: 'socure-docv-webhook' } + end + end + + def stub_docv_verification_data_pass + stub_docv_verification_data(body: SocureDocvFixtures.pass_json) + end + + def stub_docv_verification_data(body:) + stub_request(:post, "#{IdentityConfig.store.socure_idplus_base_url}/api/3.0/EmailAuthScore"). + to_return( + headers: { + 'Content-Type' => 'application/json', + }, + body:, + ) + end + + def stub_docv_document_request( + url: 'https://verify.fake-socure.test/something', + status: 200, + token: SecureRandom.hex, + body: nil + ) + body ||= { + referenceId: 'socure-reference-id', + data: { + eventId: 'socure-event-id', + docvTransactionToken: token, + qrCode: 'qr-code', + url:, + }, + } + + stub_request(:post, IdentityConfig.store.socure_docv_document_request_endpoint). + to_return( + status:, + body: body.to_json, + ) + token + end end diff --git a/spec/support/socure_docv_fixtures.rb b/spec/support/socure_docv_fixtures.rb new file mode 100644 index 00000000000..a0e6ab756c4 --- /dev/null +++ b/spec/support/socure_docv_fixtures.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +module SocureDocvFixtures + class << self + def pass_json + raw = read_fixture_file_at_path('pass.json') + JSON.parse(raw).to_json + end + + private + + def read_fixture_file_at_path(filepath) + expanded_path = Rails.root.join( + 'spec', + 'fixtures', + 'socure_docv', + filepath, + ) + File.read(expanded_path) + end + end +end