diff --git a/.stylelintrc.json b/.stylelintrc.json index e216e395d45..d975da860ba 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -1,5 +1,4 @@ { "extends": "@18f/identity-stylelint-config", - "ignoreFiles": "**/fixtures/**/*", - "reportNeedlessDisables": true + "ignoreFiles": "**/fixtures/**/*" } diff --git a/app/assets/stylesheets/components/_alert-icon.scss b/app/assets/stylesheets/components/_alert-icon.scss index cf540a4686c..7ba7706c059 100644 --- a/app/assets/stylesheets/components/_alert-icon.scss +++ b/app/assets/stylesheets/components/_alert-icon.scss @@ -7,6 +7,6 @@ .alert-icon--centered-top { position: absolute; left: 50%; - top: 0px; + top: 0; transform: translate(-50%, -50%); } diff --git a/app/assets/stylesheets/components/_block-link.scss b/app/assets/stylesheets/components/_block-link.scss index 52fd59731a9..8cdffde641e 100644 --- a/app/assets/stylesheets/components/_block-link.scss +++ b/app/assets/stylesheets/components/_block-link.scss @@ -24,13 +24,10 @@ &::before { @include u-border('1px', 'primary'); border-radius: 6px; - bottom: 0; + inset: 0 units(-1) 0 units(-1); content: ''; - left: units(-1); pointer-events: none; position: absolute; - right: units(-1); - top: 0; } } } diff --git a/app/assets/stylesheets/components/_full-screen.scss b/app/assets/stylesheets/components/_full-screen.scss index 17f229fcc5b..637444a8110 100644 --- a/app/assets/stylesheets/components/_full-screen.scss +++ b/app/assets/stylesheets/components/_full-screen.scss @@ -5,11 +5,8 @@ } .full-screen { - bottom: 0; - left: 0; + inset: 0; position: fixed; - right: 0; - top: 0; z-index: 1000; } diff --git a/app/assets/stylesheets/components/_spinner-dots.scss b/app/assets/stylesheets/components/_spinner-dots.scss index 9978f3f7d9d..bfe47990fef 100644 --- a/app/assets/stylesheets/components/_spinner-dots.scss +++ b/app/assets/stylesheets/components/_spinner-dots.scss @@ -65,7 +65,7 @@ animation-iteration-count: infinite; animation-timing-function: linear; animation-delay: 0.01s; // See: https://stackoverflow.com/a/40028240 - background-color: currentColor; + background-color: currentcolor; border-radius: 50%; content: ''; display: block; diff --git a/app/assets/stylesheets/components/_step-indicator.scss b/app/assets/stylesheets/components/_step-indicator.scss index 3dbb4bac676..c9a4384a917 100644 --- a/app/assets/stylesheets/components/_step-indicator.scss +++ b/app/assets/stylesheets/components/_step-indicator.scss @@ -6,7 +6,7 @@ $step-indicator-line-height: 4px; lg-step-indicator { display: block; border-bottom: 1px solid color('primary-light'); - box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1); + box-shadow: 0 2px 2px rgb(0 0 0 / 10%); margin-bottom: units(4); position: relative; @@ -29,9 +29,9 @@ lg-step-indicator { &::before { background: linear-gradient( to right, - rgba(255, 255, 255, 1), - rgba(255, 255, 255, 1) 17%, - rgba(255, 255, 255, 0) + rgb(255 255 255 / 100%), + rgb(255 255 255 / 100%) 17%, + rgb(255 255 255 / 0%) ); left: 0; z-index: 1; @@ -40,9 +40,9 @@ lg-step-indicator { &::after { background: linear-gradient( to left, - rgba(255, 255, 255, 1), - rgba(255, 255, 255, 1) 17%, - rgba(255, 255, 255, 0) + rgb(255 255 255 / 100%), + rgb(255 255 255 / 100%) 17%, + rgb(255 255 255 / 0%) ); right: 0; } diff --git a/app/assets/stylesheets/design-system-waiting-room.scss b/app/assets/stylesheets/design-system-waiting-room.scss index 2c2ebb18ae7..69baa9cde87 100644 --- a/app/assets/stylesheets/design-system-waiting-room.scss +++ b/app/assets/stylesheets/design-system-waiting-room.scss @@ -1,7 +1,7 @@ // To be removed once design system incorporates styles included below. -//basscss-base-typography -//------------------------------------------------ +// basscss-base-typography +// ------------------------------------------------ h1, h2, h3, @@ -18,7 +18,7 @@ ul { } // basscss-utility-typography -//------------------------------------------------ +// ------------------------------------------------ .break-word { word-wrap: break-word; } diff --git a/app/assets/stylesheets/tables-report.css.scss b/app/assets/stylesheets/tables-report.css.scss index 75147b324d8..baf6228af1f 100644 --- a/app/assets/stylesheets/tables-report.css.scss +++ b/app/assets/stylesheets/tables-report.css.scss @@ -10,7 +10,7 @@ .usa-alert.usa-alert--info.usa-alert--email { .usa-alert__body { - &:before { + &::before { background-image: url('email/info.png'); } } diff --git a/app/assets/stylesheets/variables/_email.scss b/app/assets/stylesheets/variables/_email.scss index aca2ac2c8f6..3ceaa4bde7f 100644 --- a/app/assets/stylesheets/variables/_email.scss +++ b/app/assets/stylesheets/variables/_email.scss @@ -21,8 +21,8 @@ $success-color: #3adb76; $warning-color: #ffae00; $alert-color: #ec5840; $light-gray: #f3f3f3; -$black: #111111; -$white: #ffffff; +$black: #111; +$white: #fff; $gray: #5b616a; $medium-gray: #cacaca; $dark-gray: #212121; diff --git a/app/components/badge_component.html.erb b/app/components/badge_component.html.erb index 4514b4de4d3..e0bbd3bc4a5 100644 --- a/app/components/badge_component.html.erb +++ b/app/components/badge_component.html.erb @@ -1,4 +1,4 @@ <%= content_tag('div', **tag_options, class: ['lg-verification-badge', *tag_options[:class]]) do %> - <%= image_tag(asset_path("alerts/#{icon}.svg"), size: 16, alt: '', role: 'img') %> + <%= render IconComponent.new(icon:, class: 'text-success') %> <%= content %> <% end %> diff --git a/app/components/badge_component.rb b/app/components/badge_component.rb index a076d174cef..6f5f8a75c2c 100644 --- a/app/components/badge_component.rb +++ b/app/components/badge_component.rb @@ -2,8 +2,8 @@ class BadgeComponent < BaseComponent ICONS = %i[ - unphishable - success + lock + check_circle ].to_set.freeze attr_reader :icon, :tag_options diff --git a/app/components/badge_component.scss b/app/components/badge_component.scss index d761a6ebf05..2dbbf933fc0 100644 --- a/app/components/badge_component.scss +++ b/app/components/badge_component.scss @@ -1 +1,7 @@ +@use 'uswds-core' as *; @forward 'usa-verification-badge'; + +// Upstream: https://github.com/18F/identity-design-system/pull/445 +.lg-verification-badge .usa-icon { + margin-right: units(1); +} diff --git a/app/components/icon_component.scss b/app/components/icon_component.scss index 49b41ae68f8..3184eec06eb 100644 --- a/app/components/icon_component.scss +++ b/app/components/icon_component.scss @@ -5,5 +5,5 @@ .icon { mask-size: 100%; - background-color: currentColor; + background-color: currentcolor; } diff --git a/app/components/phone_input_component.scss b/app/components/phone_input_component.scss index b01bf036565..330bf4378da 100644 --- a/app/components/phone_input_component.scss +++ b/app/components/phone_input_component.scss @@ -10,7 +10,7 @@ lg-phone-input { .iti__flag { background-image: url('/flags.png'); - @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { + @media (resolution >= 2x) { background-image: url('/flags@2x.png'); } } diff --git a/app/components/security_key_image_component.scss b/app/components/security_key_image_component.scss index 654f2a41d8a..8046a088261 100644 --- a/app/components/security_key_image_component.scss +++ b/app/components/security_key_image_component.scss @@ -1,5 +1,5 @@ .security-key-image__key { - animation: security-key-image__key__move 4s ease-in-out infinite; + animation: security-key-image-key-move 4s ease-in-out infinite; @media (prefers-reduced-motion) { animation: none; @@ -7,7 +7,7 @@ } .security-key-image__arrow { - animation: security-key-image__arrow__move 4s ease-in-out infinite; + animation: security-key-image-arrow-move 4s ease-in-out infinite; @media (prefers-reduced-motion) { animation: none; @@ -16,15 +16,15 @@ .security-key--mobile { .security-key-image__key { - animation-name: security-key-image__key__move__mobile; + animation-name: security-key-image-key-move-mobile; } .security-key-image__arrow { - animation-name: security-key-image__arrow__move__mobile; + animation-name: security-key-image-arrow-move-mobile; } } -@keyframes security-key-image__key__move { +@keyframes security-key-image-key-move { 25% { transform: translate(0, 0); } @@ -39,7 +39,7 @@ } } -@keyframes security-key-image__key__move__mobile { +@keyframes security-key-image-key-move-mobile { 25% { transform: translate(0, 0); } @@ -54,7 +54,7 @@ } } -@keyframes security-key-image__arrow__move { +@keyframes security-key-image-arrow-move { 7.5% { transform: translate(0, 0); } @@ -75,7 +75,7 @@ } } -@keyframes security-key-image__arrow__move__mobile { +@keyframes security-key-image-arrow-move-mobile { 7.5% { transform: translate(0, 0); } diff --git a/app/components/webauthn_input_component.scss b/app/components/webauthn_input_component.scss index 46ab1ce833d..b6392df9874 100644 --- a/app/components/webauthn_input_component.scss +++ b/app/components/webauthn_input_component.scss @@ -1,3 +1,3 @@ .webauthn-input--unsupported-passkey .usa-checkbox__label { - background: rgba(255, 0, 0, 0.1); + background: rgb(255 0 0 / 10%); } diff --git a/app/controllers/completions_cancellation_controller.rb b/app/controllers/completions_cancellation_controller.rb new file mode 100644 index 00000000000..29c1e670c0a --- /dev/null +++ b/app/controllers/completions_cancellation_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class CompletionsCancellationController < ApplicationController + before_action :confirm_two_factor_authenticated + + def show + analytics.completions_cancellation_visited + end +end diff --git a/app/controllers/concerns/idv/ab_test_analytics_concern.rb b/app/controllers/concerns/idv/ab_test_analytics_concern.rb index 580cd6a0785..714eeb895da 100644 --- a/app/controllers/concerns/idv/ab_test_analytics_concern.rb +++ b/app/controllers/concerns/idv/ab_test_analytics_concern.rb @@ -6,7 +6,8 @@ module AbTestAnalyticsConcern include OptInHelper def ab_test_analytics_buckets - buckets = {} + buckets = { ab_tests: {} } + if defined?(idv_session) buckets[:skip_hybrid_handoff] = idv_session&.skip_hybrid_handoff buckets = buckets.merge(opt_in_analytics_properties) @@ -18,7 +19,23 @@ def ab_test_analytics_buckets buckets = buckets.merge(lniv_args) end - buckets.merge(acuant_sdk_ab_test_analytics_args) + if defined?(idv_session) + phone_confirmation_session = idv_session.user_phone_confirmation_session || + PhoneConfirmationSession.new( + code: nil, + phone: nil, + sent_at: nil, + delivery_method: :sms, + user: current_user, + ) + buckets[:ab_tests].merge!( + phone_confirmation_session.ab_test_analytics_args, + ) + end + + buckets.merge!(acuant_sdk_ab_test_analytics_args) + buckets.delete(:ab_tests) if buckets[:ab_tests].blank? + buckets end end end diff --git a/app/controllers/concerns/idv/document_capture_concern.rb b/app/controllers/concerns/idv/document_capture_concern.rb index 4054e20a230..bda39da195a 100644 --- a/app/controllers/concerns/idv/document_capture_concern.rb +++ b/app/controllers/concerns/idv/document_capture_concern.rb @@ -49,7 +49,9 @@ def stored_result end def selfie_requirement_met? - !decorated_sp_session.biometric_comparison_required? || stored_result.selfie_check_performed? + !FeatureManagement.idv_allow_selfie_check? || + !resolved_authn_context_result.biometric_comparison? || + stored_result.selfie_check_performed? end private diff --git a/app/controllers/concerns/idv/step_indicator_concern.rb b/app/controllers/concerns/idv/step_indicator_concern.rb index 117591e7c65..223eb1783ed 100644 --- a/app/controllers/concerns/idv/step_indicator_concern.rb +++ b/app/controllers/concerns/idv/step_indicator_concern.rb @@ -8,15 +8,15 @@ module StepIndicatorConcern { name: :getting_started }, { name: :verify_id }, { name: :verify_info }, - { name: :verify_phone_or_address }, - { name: :secure_account }, + { name: :verify_phone }, + { name: :re_enter_password }, ].freeze STEP_INDICATOR_STEPS_GPO = [ { name: :getting_started }, { name: :verify_id }, { name: :verify_info }, - { name: :get_a_letter }, + { name: :verify_address }, { name: :secure_account }, ].freeze diff --git a/app/controllers/concerns/idv_session_concern.rb b/app/controllers/concerns/idv_session_concern.rb index d1ceafc3858..87a715c14d3 100644 --- a/app/controllers/concerns/idv_session_concern.rb +++ b/app/controllers/concerns/idv_session_concern.rb @@ -67,7 +67,8 @@ def idv_session_user end def user_needs_biometric_comparison? - decorated_sp_session.biometric_comparison_required? && + FeatureManagement.idv_allow_selfie_check? && + resolved_authn_context_result.biometric_comparison? && !current_user.identity_verified_with_biometric_comparison? end end diff --git a/app/controllers/concerns/idv_step_concern.rb b/app/controllers/concerns/idv_step_concern.rb index 396dc73fa25..c2c6c166295 100644 --- a/app/controllers/concerns/idv_step_concern.rb +++ b/app/controllers/concerns/idv_step_concern.rb @@ -111,7 +111,9 @@ def flow_policy def confirm_step_allowed # set it everytime, since user may switch SP - idv_session.selfie_check_required = decorated_sp_session.biometric_comparison_required? + idv_session.selfie_check_required = + FeatureManagement.idv_allow_selfie_check? && + resolved_authn_context_result.biometric_comparison? return if flow_policy.controller_allowed?(controller: self.class) redirect_to url_for_latest_step diff --git a/app/controllers/idv/by_mail/request_letter_controller.rb b/app/controllers/idv/by_mail/request_letter_controller.rb index 673a0222bd3..2b6e0b53afb 100644 --- a/app/controllers/idv/by_mail/request_letter_controller.rb +++ b/app/controllers/idv/by_mail/request_letter_controller.rb @@ -15,7 +15,6 @@ class RequestLetterController < ApplicationController def index @applicant = idv_session.applicant @presenter = RequestLetterPresenter.new(current_user, url_options) - @step_indicator_current_step = step_indicator_current_step Funnel::DocAuth::RegisterStep.new(current_user.id, current_sp&.issuer). call(:usps_address, :view, true) @@ -63,14 +62,6 @@ def confirm_profile_not_too_old redirect_to idv_path if gpo_mail_service.profile_too_old? end - def step_indicator_current_step - if resend_requested? - :get_a_letter - else - :verify_phone_or_address - end - end - def update_tracking Funnel::DocAuth::RegisterStep.new(current_user.id, current_sp&.issuer). call(:usps_letter_sent, :update, true) @@ -139,6 +130,14 @@ def send_reminder def pii_locked? !Pii::Cacher.new(current_user, user_session).exists_in_session? end + + def step_indicator_steps + if in_person_proofing? + Idv::Flows::InPersonFlow::STEP_INDICATOR_STEPS_GPO + else + StepIndicatorConcern::STEP_INDICATOR_STEPS_GPO + end + end end end end diff --git a/app/controllers/idv/confirm_start_over_controller.rb b/app/controllers/idv/confirm_start_over_controller.rb index 7067730dfd4..61080628ee9 100644 --- a/app/controllers/idv/confirm_start_over_controller.rb +++ b/app/controllers/idv/confirm_start_over_controller.rb @@ -11,13 +11,13 @@ class ConfirmStartOverController < ApplicationController before_action :confirm_idv_needed def index - @step_indicator_step = requested_letter_before? ? :get_a_letter : :verify_phone_or_address + @step_indicator_step = requested_letter_before? ? :verify_address : :verify_phone analytics.idv_gpo_confirm_start_over_visited end def before_letter - @step_indicator_step = requested_letter_before? ? :get_a_letter : :verify_phone_or_address + @step_indicator_step = requested_letter_before? ? :verify_address : :verify_phone analytics.idv_gpo_confirm_start_over_before_letter_visited end diff --git a/app/controllers/idv/document_capture_controller.rb b/app/controllers/idv/document_capture_controller.rb index 1801378902e..e2330286120 100644 --- a/app/controllers/idv/document_capture_controller.rb +++ b/app/controllers/idv/document_capture_controller.rb @@ -43,6 +43,10 @@ def update end def extra_view_variables + doc_auth_selfie_capture = + FeatureManagement.idv_allow_selfie_check? && + resolved_authn_context_result.biometric_comparison? + { document_capture_session_uuid: document_capture_session_uuid, flow_path: 'standard', @@ -51,7 +55,7 @@ def extra_view_variables skip_doc_auth: idv_session.skip_doc_auth, skip_doc_auth_from_handoff: idv_session.skip_doc_auth_from_handoff, opted_in_to_in_person_proofing: idv_session.opted_in_to_in_person_proofing, - doc_auth_selfie_capture: decorated_sp_session.biometric_comparison_required?, + doc_auth_selfie_capture:, }.merge( acuant_sdk_upgrade_a_b_testing_variables, ) @@ -90,6 +94,10 @@ def cancel_establishing_in_person_enrollments end def analytics_arguments + liveness_checking_required = + FeatureManagement.idv_allow_selfie_check? && + resolved_authn_context_result.biometric_comparison? + { flow_path: flow_path, step: 'document_capture', @@ -97,8 +105,8 @@ def analytics_arguments irs_reproofing: irs_reproofing?, redo_document_capture: idv_session.redo_document_capture, skip_hybrid_handoff: idv_session.skip_hybrid_handoff, - liveness_checking_required: decorated_sp_session.biometric_comparison_required?, - selfie_check_required: idv_session.selfie_check_required, + liveness_checking_required:, + selfie_check_required: liveness_checking_required, }.merge(ab_test_analytics_buckets) end diff --git a/app/controllers/idv/enter_password_controller.rb b/app/controllers/idv/enter_password_controller.rb index 08c35cc2074..6508f4a7cc2 100644 --- a/app/controllers/idv/enter_password_controller.rb +++ b/app/controllers/idv/enter_password_controller.rb @@ -70,8 +70,8 @@ def create end def step_indicator_step - return :secure_account unless idv_session.verify_by_mail? - :get_a_letter + return :re_enter_password unless idv_session.verify_by_mail? + :verify_address end def self.step_info diff --git a/app/controllers/idv/hybrid_mobile/capture_complete_controller.rb b/app/controllers/idv/hybrid_mobile/capture_complete_controller.rb index d40671dddb6..4ed8ece9efd 100644 --- a/app/controllers/idv/hybrid_mobile/capture_complete_controller.rb +++ b/app/controllers/idv/hybrid_mobile/capture_complete_controller.rb @@ -20,12 +20,16 @@ def show private def analytics_arguments + liveness_checking_required = + FeatureManagement.idv_allow_selfie_check? && + resolved_authn_context_result.biometric_comparison? + { flow_path: 'hybrid', step: 'capture_complete', analytics_id: 'Doc Auth', irs_reproofing: irs_reproofing?, - liveness_checking_required: decorated_sp_session.biometric_comparison_required?, + liveness_checking_required:, }.merge(ab_test_analytics_buckets) end end diff --git a/app/controllers/idv/hybrid_mobile/document_capture_controller.rb b/app/controllers/idv/hybrid_mobile/document_capture_controller.rb index ae138d92096..0efbef1595d 100644 --- a/app/controllers/idv/hybrid_mobile/document_capture_controller.rb +++ b/app/controllers/idv/hybrid_mobile/document_capture_controller.rb @@ -39,11 +39,14 @@ def update end def extra_view_variables + doc_auth_selfie_capture = FeatureManagement.idv_allow_selfie_check? && + resolved_authn_context_result.biometric_comparison? + { flow_path: 'hybrid', document_capture_session_uuid: document_capture_session_uuid, failure_to_proof_url: return_to_sp_failure_to_proof_url(step: 'document_capture'), - doc_auth_selfie_capture: decorated_sp_session.biometric_comparison_required?, + doc_auth_selfie_capture:, }.merge( acuant_sdk_upgrade_a_b_testing_variables, ) @@ -52,13 +55,17 @@ def extra_view_variables private def analytics_arguments + biometric_comparison_required = + FeatureManagement.idv_allow_selfie_check? && + resolved_authn_context_result.biometric_comparison? + { flow_path: 'hybrid', step: 'document_capture', analytics_id: 'Doc Auth', irs_reproofing: irs_reproofing?, - liveness_checking_required: decorated_sp_session.biometric_comparison_required?, - selfie_check_required: decorated_sp_session.biometric_comparison_required?, + liveness_checking_required: biometric_comparison_required, + selfie_check_required: biometric_comparison_required, }.merge( ab_test_analytics_buckets, ) diff --git a/app/controllers/idv/image_uploads_controller.rb b/app/controllers/idv/image_uploads_controller.rb index e9a7105e6ad..76bcf175e29 100644 --- a/app/controllers/idv/image_uploads_controller.rb +++ b/app/controllers/idv/image_uploads_controller.rb @@ -24,13 +24,8 @@ def image_upload_form analytics: analytics, uuid_prefix: current_sp&.app_id, irs_attempts_api_tracker: irs_attempts_api_tracker, - store_encrypted_images: store_encrypted_images?, - liveness_checking_required: decorated_sp_session.biometric_comparison_required?, + liveness_checking_required: resolved_authn_context_result.biometric_comparison?, ) end - - def store_encrypted_images? - IdentityConfig.store.encrypted_document_storage_enabled - end end end diff --git a/app/controllers/idv/in_person/state_id_controller.rb b/app/controllers/idv/in_person/state_id_controller.rb index e4fc9e1fa1f..abcd09cf430 100644 --- a/app/controllers/idv/in_person/state_id_controller.rb +++ b/app/controllers/idv/in_person/state_id_controller.rb @@ -16,6 +16,51 @@ def show render :show, locals: extra_view_variables end + def update + # don't clear the ssn when updating address, clear after SsnController + clear_future_steps_from!(controller: Idv::InPerson::SsnController) + + pii_from_user = flow_session[:pii_from_user] + initial_state_of_same_address_as_id = pii_from_user[:same_address_as_id] + Idv::StateIdForm::ATTRIBUTES.each do |attr| + pii_from_user[attr] = flow_params[attr] + end + form_result = form.submit(flow_params) + + analytics.idv_in_person_proofing_state_id_submitted( + **analytics_arguments.merge(**form_result.to_h), + ) + + if form_result.success? + # Accept Date of Birth from both memorable date and input date components + formatted_dob = MemorableDateComponent.extract_date_param flow_params&.[](:dob) + pii_from_user[:dob] = formatted_dob if formatted_dob + + if pii_from_user[:same_address_as_id] == 'true' + copy_state_id_address_to_residential_address(pii_from_user) + redirect_url = idv_in_person_ssn_url + end + + if initial_state_of_same_address_as_id == 'true' && + pii_from_user[:same_address_as_id] == 'false' + clear_residential_address(pii_from_user) + end + + if (idv_session.ssn && pii_from_user[:same_address_as_id] == 'true') || + initial_state_of_same_address_as_id == 'false' + redirect_url = idv_in_person_verify_info_url + elsif pii_from_user[:same_address_as_id] == 'false' + redirect_url = idv_in_person_address_url + else + redirect_url = idv_in_person_ssn_url + end + + redirect_to redirect_url + else + render :show, locals: extra_view_variables + end + end + def extra_view_variables { form:, @@ -25,6 +70,24 @@ def extra_view_variables } end + # update Idv::DocumentCaptureController.step_info.next_steps to include + # :ipp_state_id instead of :ipp_ssn (or :ipp_address) in delete PR + def self.step_info + Idv::StepInfo.new( + key: :ipp_state_id, + controller: self, + next_steps: [:ipp_address, :ipp_ssn], + preconditions: ->(idv_session:, user:) { user.establishing_in_person_enrollment }, + undo_step: ->(idv_session:, user:) do + pii_from_user[:identity_doc_address1] = nil + pii_from_user[:identity_doc_address2] = nil + pii_from_user[:identity_doc_city] = nil + pii_from_user[:identity_doc_zipcode] = nil + pii_from_user[:identity_doc_state] = nil + end, + ) + end + private def render_404_if_controller_not_enabled @@ -50,8 +113,24 @@ def analytics_arguments merge(extra_analytics_properties) end + def clear_residential_address(pii_from_user) + pii_from_user.delete(:address1) + pii_from_user.delete(:address2) + pii_from_user.delete(:city) + pii_from_user.delete(:state) + pii_from_user.delete(:zipcode) + end + + def copy_state_id_address_to_residential_address(pii_from_user) + pii_from_user[:address1] = flow_params[:identity_doc_address1] + pii_from_user[:address2] = flow_params[:identity_doc_address2] + pii_from_user[:city] = flow_params[:identity_doc_city] + pii_from_user[:state] = flow_params[:identity_doc_address_state] + pii_from_user[:zipcode] = flow_params[:identity_doc_zipcode] + end + def updating_state_id? - flow_session[:pii_from_user].has_key?(:first_name) + pii_from_user.has_key?(:first_name) end def parsed_dob @@ -67,7 +146,7 @@ def parsed_dob end def pii - data = flow_session[:pii_from_user] + data = pii_from_user data = data.merge(flow_params) if params.has_key?(:state_id) data.deep_symbolize_keys end diff --git a/app/controllers/idv/otp_verification_controller.rb b/app/controllers/idv/otp_verification_controller.rb index 483dbf7e0a9..4253e861938 100644 --- a/app/controllers/idv/otp_verification_controller.rb +++ b/app/controllers/idv/otp_verification_controller.rb @@ -16,6 +16,7 @@ def show # memoize the form so the ivar is available to the view phone_confirmation_otp_verification_form analytics.idv_phone_confirmation_otp_visit + @otp_code_length = code_length end def update @@ -34,6 +35,7 @@ def update flash[:success] = t('idv.messages.enter_password.phone_verified') redirect_to idv_enter_password_url else + @otp_code_length = code_length handle_otp_confirmation_failure end end @@ -96,5 +98,18 @@ def phone_confirmation_otp_verification_form irs_attempts_api_tracker: irs_attempts_api_tracker, ) end + + def code_length + if ten_digit_otp? + 10 + else + TwoFactorAuthenticatable::PROOFING_DIRECT_OTP_LENGTH + end + end + + def ten_digit_otp? + AbTests::IDV_TEN_DIGIT_OTP.bucket(current_user.uuid) == :ten_digit_otp && + idv_session.user_phone_confirmation_session.delivery_method == :voice + end end end diff --git a/app/controllers/idv/personal_key_controller.rb b/app/controllers/idv/personal_key_controller.rb index 22bf5eaa61a..310d335f15f 100644 --- a/app/controllers/idv/personal_key_controller.rb +++ b/app/controllers/idv/personal_key_controller.rb @@ -133,5 +133,13 @@ def redirect_to_retrieve_pii user_session[:stored_location] = request.original_fullpath redirect_to fix_broken_personal_key_url end + + def step_indicator_step + return :secure_account if idv_session.verify_by_mail? + return :go_to_the_post_office if in_person_proofing? + + StepIndicatorComponent::ALL_STEPS_COMPLETE + end + helper_method :step_indicator_step end end diff --git a/app/controllers/idv/phone_controller.rb b/app/controllers/idv/phone_controller.rb index 3a15f231c48..e8972f9197c 100644 --- a/app/controllers/idv/phone_controller.rb +++ b/app/controllers/idv/phone_controller.rb @@ -257,6 +257,7 @@ def save_delivery_preference phone: original_session.phone, sent_at: original_session.sent_at, delivery_method: original_session.delivery_method, + user: current_user, ) end end diff --git a/app/controllers/idv_controller.rb b/app/controllers/idv_controller.rb index 1f0c366e581..272fe1d2123 100644 --- a/app/controllers/idv_controller.rb +++ b/app/controllers/idv_controller.rb @@ -32,11 +32,12 @@ def activated private def already_verified? - if decorated_sp_session.biometric_comparison_required? - return current_user.identity_verified_with_biometric_comparison? + if FeatureManagement.idv_allow_selfie_check? && + resolved_authn_context_result.biometric_comparison? + current_user.identity_verified_with_biometric_comparison? + else + current_user.active_profile.present? end - - return current_user.active_profile.present? end def verify_identity diff --git a/app/controllers/openid_connect/authorization_controller.rb b/app/controllers/openid_connect/authorization_controller.rb index 43d60764941..2e46ad869e9 100644 --- a/app/controllers/openid_connect/authorization_controller.rb +++ b/app/controllers/openid_connect/authorization_controller.rb @@ -127,7 +127,8 @@ def identity_needs_verification? end def biometric_comparison_needed? - decorated_sp_session.biometric_comparison_required? && + FeatureManagement.idv_allow_selfie_check? && + resolved_authn_context_result.biometric_comparison? && !current_user.identity_verified_with_biometric_comparison? end diff --git a/app/controllers/saml_idp_controller.rb b/app/controllers/saml_idp_controller.rb index d7f37c7c2fa..b4dd3343e81 100644 --- a/app/controllers/saml_idp_controller.rb +++ b/app/controllers/saml_idp_controller.rb @@ -113,7 +113,8 @@ def prompt_for_password_if_ial2_request_and_pii_locked end def biometric_comparison_needed? - decorated_sp_session.biometric_comparison_required? && + FeatureManagement.idv_allow_selfie_check? && + resolved_authn_context_result.biometric_comparison? && !current_user.identity_verified_with_biometric_comparison? end diff --git a/app/controllers/two_factor_authentication/otp_verification_controller.rb b/app/controllers/two_factor_authentication/otp_verification_controller.rb index c2b6cc26348..31d836e8c88 100644 --- a/app/controllers/two_factor_authentication/otp_verification_controller.rb +++ b/app/controllers/two_factor_authentication/otp_verification_controller.rb @@ -67,6 +67,10 @@ def track_mfa_added analytics.multi_factor_auth_added_phone( enabled_mfa_methods_count: MfaContext.new(current_user).enabled_mfa_methods_count, in_account_creation_flow: user_session[:in_account_creation_flow] || false, + recaptcha_annotation: RecaptchaAnnotator.annotate( + assessment_id: user_session.delete(:phone_recaptcha_assessment_id), + reason: RecaptchaAnnotator::AnnotationReasons::PASSED_TWO_FACTOR, + ), ) Funnel::Registration::AddMfa.call(current_user.id, 'phone', analytics) end diff --git a/app/controllers/users/phone_setup_controller.rb b/app/controllers/users/phone_setup_controller.rb index 41f9ff5508f..e10cf92ac4f 100644 --- a/app/controllers/users/phone_setup_controller.rb +++ b/app/controllers/users/phone_setup_controller.rb @@ -73,30 +73,27 @@ def setup_voice_preference? def handle_create_success(phone) if MfaContext.new(current_user).phone_configurations.map(&:phone).index(phone).nil? - prompt_to_confirm_phone( - id: nil, - phone: @new_phone_form.phone, - selected_delivery_method: @new_phone_form.otp_delivery_preference, - phone_type: @new_phone_form.phone_info&.type, - selected_default_number: @new_phone_form.otp_make_default_number, - ) + prompt_to_confirm_phone else flash[:error] = t('errors.messages.phone_duplicate') redirect_to phone_setup_url end end - def prompt_to_confirm_phone(id:, phone:, selected_delivery_method: nil, - selected_default_number: nil, phone_type: nil) - - user_session[:unconfirmed_phone] = phone + def prompt_to_confirm_phone + user_session[:unconfirmed_phone] = @new_phone_form.phone user_session[:context] = 'confirmation' - user_session[:phone_type] = phone_type.to_s + user_session[:phone_type] = @new_phone_form.phone_info&.type.to_s + user_session[:phone_recaptcha_assessment_id] = @new_phone_form.recaptcha_assessment_id redirect_to otp_send_url( otp_delivery_selection_form: { - otp_delivery_preference: otp_delivery_method(id, phone, selected_delivery_method), - otp_make_default_number: selected_default_number, + otp_delivery_preference: otp_delivery_method( + nil, + @new_phone_form.phone, + @new_phone_form.otp_delivery_preference, + ), + otp_make_default_number: @new_phone_form.otp_make_default_number, }, ) end diff --git a/app/controllers/users/two_factor_authentication_controller.rb b/app/controllers/users/two_factor_authentication_controller.rb index 640c62f071b..09d9c825126 100644 --- a/app/controllers/users/two_factor_authentication_controller.rb +++ b/app/controllers/users/two_factor_authentication_controller.rb @@ -231,6 +231,10 @@ def track_events(otp_delivery_preference:, otp_delivery_selection_result:) adapter: Telephony.config.adapter, telephony_response: @telephony_result.to_h, success: @telephony_result.success?, + recaptcha_annotation: RecaptchaAnnotator.annotate( + assessment_id: user_session[:phone_recaptcha_assessment_id], + reason: RecaptchaAnnotator::AnnotationReasons::INITIATED_TWO_FACTOR, + ), ) if UserSessionContext.reauthentication_context?(context) @@ -312,10 +316,16 @@ def send_user_otp(method) if UserSessionContext.authentication_or_reauthentication_context?(context) Telephony.send_authentication_otp(**otp_params) else - Telephony.send_confirmation_otp(**otp_params) + Telephony.send_confirmation_otp(**otp_params, otp_length: otp_length) end end + def otp_length + bucket = AbTests::IDV_TEN_DIGIT_OTP.bucket(current_user.uuid) + length = bucket == :ten_digit_otp ? 'ten' : 'six' + I18n.t("telephony.format_length.#{length}") + end + def user_selected_default_number delivery_params[:otp_make_default_number] end diff --git a/app/decorators/null_service_provider_session.rb b/app/decorators/null_service_provider_session.rb index 74cae9b0b4a..d9037fd6c82 100644 --- a/app/decorators/null_service_provider_session.rb +++ b/app/decorators/null_service_provider_session.rb @@ -39,10 +39,6 @@ def request_url_params {} end - def biometric_comparison_required? - false - end - def current_user view_context&.current_user end diff --git a/app/decorators/service_provider_session.rb b/app/decorators/service_provider_session.rb index b1b70196278..9ce2f15a38f 100644 --- a/app/decorators/service_provider_session.rb +++ b/app/decorators/service_provider_session.rb @@ -68,11 +68,6 @@ def sp_issuer sp.issuer end - def biometric_comparison_required? - !!(FeatureManagement.idv_allow_selfie_check? && - sp_session[:biometric_comparison_required]) - end - def cancel_link_url view_context.new_user_session_url(request_id: sp_session[:request_id]) end diff --git a/app/forms/idv/api_image_upload_form.rb b/app/forms/idv/api_image_upload_form.rb index 5445711e84b..9dbf7778a09 100644 --- a/app/forms/idv/api_image_upload_form.rb +++ b/app/forms/idv/api_image_upload_form.rb @@ -16,14 +16,13 @@ class ApiImageUploadForm def initialize(params, service_provider:, analytics: nil, uuid_prefix: nil, irs_attempts_api_tracker: nil, - store_encrypted_images: false, liveness_checking_required: false) + liveness_checking_required: false) @params = params @service_provider = service_provider @analytics = analytics @readable = {} @uuid_prefix = uuid_prefix @irs_attempts_api_tracker = irs_attempts_api_tracker - @store_encrypted_images = store_encrypted_images @liveness_checking_required = liveness_checking_required end @@ -355,25 +354,6 @@ def update_analytics(client_response:, vendor_request_time_in_ms:) ) end - def store_encrypted_images_if_required - return unless store_encrypted_images? - - encrypted_document_storage_writer.encrypt_and_write_document( - front_image: front_image_bytes, - front_image_content_type: front.content_type, - back_image: back_image_bytes, - back_image_content_type: back.content_type, - ) - end - - def store_encrypted_images? - @store_encrypted_images - end - - def encrypted_document_storage_writer - @encrypted_document_storage_writer ||= EncryptedDocumentStorage::DocumentWriter.new - end - def acuant_sdk_upgrade_ab_test_data return {} unless IdentityConfig.store.idv_acuant_sdk_upgrade_a_b_testing_enabled { @@ -455,7 +435,6 @@ def rate_limited? def track_event(response) pii_from_doc = response.pii_from_doc.to_h || {} - stored_image_result = store_encrypted_images_if_required irs_attempts_api_tracker.idv_document_upload_submitted( success: response.success?, @@ -463,9 +442,9 @@ def track_event(response) document_number: pii_from_doc[:state_id_number], document_issued: pii_from_doc[:state_id_issued], document_expiration: pii_from_doc[:state_id_expiration], - document_front_image_filename: stored_image_result&.front_filename, - document_back_image_filename: stored_image_result&.back_filename, - document_image_encryption_key: stored_image_result&.encryption_key, + document_front_image_filename: nil, + document_back_image_filename: nil, + document_image_encryption_key: nil, first_name: pii_from_doc[:first_name], last_name: pii_from_doc[:last_name], date_of_birth: pii_from_doc[:dob], diff --git a/app/forms/new_phone_form.rb b/app/forms/new_phone_form.rb index 01944cadcfc..4a525354fcf 100644 --- a/app/forms/new_phone_form.rb +++ b/app/forms/new_phone_form.rb @@ -24,7 +24,8 @@ class NewPhoneForm :otp_make_default_number, :setup_voice_preference, :recaptcha_token, - :recaptcha_mock_score + :recaptcha_mock_score, + :recaptcha_assessment_id alias_method :setup_voice_preference?, :setup_voice_preference @@ -131,7 +132,8 @@ def validate_not_premium_rate def validate_recaptcha_token return if !validate_recaptcha_token? - recaptcha_form.submit(recaptcha_token) + _response, assessment_id = recaptcha_form.submit(recaptcha_token) + @recaptcha_assessment_id = assessment_id errors.merge!(recaptcha_form) end diff --git a/app/forms/recaptcha_enterprise_form.rb b/app/forms/recaptcha_enterprise_form.rb index b3439341c67..6191a338282 100644 --- a/app/forms/recaptcha_enterprise_form.rb +++ b/app/forms/recaptcha_enterprise_form.rb @@ -36,6 +36,7 @@ def recaptcha_result RecaptchaResult.new( success: response.body.dig('tokenProperties', 'valid') == true && response.body.dig('tokenProperties', 'action') == recaptcha_action, + assessment_id: response.body.dig('name'), score: response.body.dig('riskAnalysis', 'score'), reasons: [ *response.body.dig('riskAnalysis', 'reasons').to_a, diff --git a/app/forms/recaptcha_form.rb b/app/forms/recaptcha_form.rb index 7254961c663..4d49437d65b 100644 --- a/app/forms/recaptcha_form.rb +++ b/app/forms/recaptcha_form.rb @@ -16,10 +16,17 @@ class RecaptchaForm validate :validate_token_exists validate :validate_recaptcha_result - RecaptchaResult = Struct.new(:success, :score, :errors, :reasons, keyword_init: true) do + RecaptchaResult = Struct.new( + :success, + :assessment_id, + :score, + :errors, + :reasons, + keyword_init: true, + ) do alias_method :success?, :success - def initialize(success:, score: nil, errors: [], reasons: []) + def initialize(success:, assessment_id: nil, score: nil, errors: [], reasons: []) super end end @@ -40,15 +47,18 @@ def exempt? !score_threshold.positive? end + # @return [Array(Boolean, String), Array(Boolean, nil)] def submit(recaptcha_token) @recaptcha_token = recaptcha_token @recaptcha_result = recaptcha_result if !exempt? && recaptcha_token.present? log_analytics(result: @recaptcha_result) if @recaptcha_result - FormResponse.new(success: valid?, errors:, serialize_error_details_only: true) + response = FormResponse.new(success: valid?, errors:, serialize_error_details_only: true) + [response, @recaptcha_result&.assessment_id] rescue Faraday::Error => error log_analytics(error:) - FormResponse.new(success: true, serialize_error_details_only: true) + response = FormResponse.new(success: true, serialize_error_details_only: true) + [response, nil] end private diff --git a/app/forms/recaptcha_mock_form.rb b/app/forms/recaptcha_mock_form.rb index c4cded23c8e..4d1685c7008 100644 --- a/app/forms/recaptcha_mock_form.rb +++ b/app/forms/recaptcha_mock_form.rb @@ -11,6 +11,6 @@ def initialize(score:, **kwargs) private def recaptcha_result - RecaptchaResult.new(success: true, score:) + RecaptchaResult.new(success: true, assessment_id: SecureRandom.uuid, score:) end end diff --git a/app/javascript/packages/clipboard-button/package.json b/app/javascript/packages/clipboard-button/package.json index 4ff9deff387..86ae7f2f7b1 100644 --- a/app/javascript/packages/clipboard-button/package.json +++ b/app/javascript/packages/clipboard-button/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "dependencies": { - "@18f/identity-design-system": "^9.1.0" + "@18f/identity-design-system": "^9.2.0" }, "sideEffects": [ "./clipboard-button-element.ts" diff --git a/app/javascript/packages/document-capture/components/_file-input.scss b/app/javascript/packages/document-capture/components/_file-input.scss index 8967e4e1f9a..1b40bc4e61e 100644 --- a/app/javascript/packages/document-capture/components/_file-input.scss +++ b/app/javascript/packages/document-capture/components/_file-input.scss @@ -10,14 +10,14 @@ outline-offset: 2px; } -.usa-file-input:not(.usa-file-input--has-value):not(.usa-file-input--value-pending) +.usa-file-input:not(.usa-file-input--has-value, .usa-file-input--value-pending) .usa-file-input__target, .usa-form-group--error .usa-file-input .usa-file-input__target, .usa-form-group--success .usa-file-input .usa-file-input__target { border-width: 3px; } -.usa-file-input:not(.usa-file-input--has-value):not(.usa-file-input--value-pending) { +.usa-file-input:not(.usa-file-input--has-value, .usa-file-input--value-pending) { .usa-file-input__target { border-color: color('primary'); border-radius: 0.375rem; diff --git a/app/javascript/packages/document-capture/components/acuant-capture.scss b/app/javascript/packages/document-capture/components/acuant-capture.scss index 2ed7922a445..c8e3335590a 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture.scss +++ b/app/javascript/packages/document-capture/components/acuant-capture.scss @@ -16,16 +16,13 @@ .usa-file-input__target { align-items: center; - bottom: 0; + inset: 0; display: flex; flex-direction: column; height: 100%; justify-content: center; - left: 0; margin-top: 0; position: absolute; - right: 0; - top: 0; } &::after { diff --git a/app/javascript/packages/phone-input/package.json b/app/javascript/packages/phone-input/package.json index 4c984caf71e..530f2283179 100644 --- a/app/javascript/packages/phone-input/package.json +++ b/app/javascript/packages/phone-input/package.json @@ -4,7 +4,7 @@ "version": "1.0.0", "dependencies": { "intl-tel-input": "^17.0.19", - "libphonenumber-js": "^1.10.61" + "libphonenumber-js": "^1.11.1" }, "sideEffects": [ "./index.ts" diff --git a/app/javascript/packages/stylelint-config/CHANGELOG.md b/app/javascript/packages/stylelint-config/CHANGELOG.md index 58b43111e43..93fb812367e 100644 --- a/app/javascript/packages/stylelint-config/CHANGELOG.md +++ b/app/javascript/packages/stylelint-config/CHANGELOG.md @@ -1,3 +1,25 @@ +## 5.0.0-beta.1 + +### Breaking Changes + +- The ruleset now extends [`stylelint-config-standard-scss`](https://github.com/stylelint-scss/stylelint-config-standard-scss) instead of [`stylelint-config-recommended-scss`](https://github.com/stylelint-scss/stylelint-config-recommended-scss). This configures a number of additional rules which may identify existing issues in your code. + - This is intended to bring the ruleset into closer alignment with the [TTS Engineering CSS Coding Standards](https://guides.18f.gov/engineering/languages-runtimes/css/), which recommends the "standard" Stylelint rules. + - Many of these rules can be fixed automatically using [Stylelint's `--fix` option](https://stylelint.io/user-guide/options/#fix). + - Some rules have been disabled to permit more flexibility in code arrangement, particularly rules affecting blank line enforcement with comments and Sass `@`-rules: + - [`at-rule-empty-line-before`](https://stylelint.io/user-guide/rules/at-rule-empty-line-before/) + - [`declaration-empty-line-before`](https://stylelint.io/user-guide/rules/declaration-empty-line-before/) + - [`rule-empty-line-before`](https://stylelint.io/user-guide/rules/rule-empty-line-before/) + - [`scss/dollar-variable-empty-line-before`](https://github.com/stylelint-scss/stylelint-scss/blob/master/src/rules/dollar-variable-empty-line-before/README.md) + - [`scss/double-slash-comment-empty-line-before`](https://github.com/stylelint-scss/stylelint-scss/blob/master/src/rules/double-slash-comment-empty-line-before/README.md) + - [`color-function-notation`](https://stylelint.io/user-guide/rules/color-function-notation/) (due to [Sass incompatibilities](https://github.com/sass/sass/issues/2831)) +- The ruleset now configures [`"reportNeedlessDisables": true`](https://stylelint.io/user-guide/options/#reportneedlessdisables), which will report inline configuration that disables rules unnecessarily. + +## 4.1.0 + +### Improvements + +- The `selector-class-pattern` configuration now specifies [`resolveNestedSelectors: true`](https://stylelint.io/user-guide/rules/selector-class-pattern/#resolvenestedselectors-true--false-default-false) to resolve nested selectors using `&` interpolation. + ## 4.0.0 ### Breaking Changes diff --git a/app/javascript/packages/stylelint-config/index.js b/app/javascript/packages/stylelint-config/index.js index 122656d8dd6..16f79869db9 100644 --- a/app/javascript/packages/stylelint-config/index.js +++ b/app/javascript/packages/stylelint-config/index.js @@ -1,8 +1,14 @@ module.exports = { - extends: ['stylelint-config-recommended-scss', 'stylelint-prettier/recommended'], + extends: ['stylelint-config-standard-scss', 'stylelint-prettier/recommended'], rules: { + 'at-rule-empty-line-before': null, + 'color-function-notation': null, + 'declaration-empty-line-before': null, 'no-descending-specificity': null, + 'rule-empty-line-before': null, 'scss/comment-no-empty': null, + 'scss/dollar-variable-empty-line-before': null, + 'scss/double-slash-comment-empty-line-before': null, 'scss/no-global-function-names': null, 'scss/operator-no-newline-after': null, 'scss/operator-no-newline-before': null, @@ -12,6 +18,7 @@ module.exports = { { message: 'Class selectors should be named using "Two Dashes Style" BEM format. See: https://en.bem.info/methodology/naming-convention/#two-dashes-style', + resolveNestedSelectors: true, }, ], }, diff --git a/app/javascript/packages/stylelint-config/package.json b/app/javascript/packages/stylelint-config/package.json index a3d876607bb..48aad324190 100644 --- a/app/javascript/packages/stylelint-config/package.json +++ b/app/javascript/packages/stylelint-config/package.json @@ -1,6 +1,6 @@ { "name": "@18f/identity-stylelint-config", - "version": "4.0.0", + "version": "5.0.0-beta.1", "private": false, "description": "Stylelint shareable configuration for Login.gov CSS/SASS standards", "exports": { @@ -20,7 +20,7 @@ }, "homepage": "https://github.com/18f/identity-idp", "dependencies": { - "stylelint-config-recommended-scss": "^14.0.0", + "stylelint-config-standard-scss": "^13.1.0", "stylelint-prettier": "^5.0.0" }, "peerDependencies": { diff --git a/app/javascript/packages/verify-flow/verify-flow-step-indicator.spec.tsx b/app/javascript/packages/verify-flow/verify-flow-step-indicator.spec.tsx index ba11f29b5d2..8792b527e8e 100644 --- a/app/javascript/packages/verify-flow/verify-flow-step-indicator.spec.tsx +++ b/app/javascript/packages/verify-flow/verify-flow-step-indicator.spec.tsx @@ -29,10 +29,10 @@ describe('VerifyFlowStepIndicator', () => { it('renders step indicator for the current step', () => { const { getByText } = render(); - const current = getByText('step_indicator.flows.idv.secure_account'); + const current = getByText('step_indicator.flows.idv.re_enter_password'); expect(current.closest('.step-indicator__step--current')).to.exist(); - const previous = getByText('step_indicator.flows.idv.verify_phone_or_address'); + const previous = getByText('step_indicator.flows.idv.verify_phone'); expect(previous.closest('.step-indicator__step--complete')).to.exist(); }); diff --git a/app/javascript/packages/verify-flow/verify-flow-step-indicator.tsx b/app/javascript/packages/verify-flow/verify-flow-step-indicator.tsx index 0bcb80f65fd..8b1fa4739a4 100644 --- a/app/javascript/packages/verify-flow/verify-flow-step-indicator.tsx +++ b/app/javascript/packages/verify-flow/verify-flow-step-indicator.tsx @@ -10,11 +10,12 @@ type VerifyFlowStepIndicatorStep = | 'getting_started' | 'verify_id' | 'verify_info' - | 'verify_phone_or_address' + | 'verify_phone' | 'secure_account' | 'find_a_post_office' | 'go_to_the_post_office' - | 'get_a_letter'; + | 'get_a_letter' + | 're_enter_password'; interface VerifyFlowConfig { /** @@ -30,26 +31,20 @@ interface VerifyFlowConfig { const FLOW_STEP_PATHS: Record = { [VerifyFlowPath.DEFAULT]: { - steps: [ - 'getting_started', - 'verify_id', - 'verify_info', - 'verify_phone_or_address', - 'secure_account', - ], + steps: ['getting_started', 'verify_id', 'verify_info', 'verify_phone', 're_enter_password'], mapping: { document_capture: 'verify_id', - password_confirm: 'secure_account', - personal_key: 'secure_account', - personal_key_confirm: 'secure_account', + password_confirm: 're_enter_password', + personal_key: 're_enter_password', + personal_key_confirm: 're_enter_password', }, }, [VerifyFlowPath.IN_PERSON]: { steps: [ 'find_a_post_office', 'verify_info', - 'verify_phone_or_address', - 'secure_account', + 'verify_phone', + 're_enter_password', 'go_to_the_post_office', ], mapping: { @@ -101,11 +96,12 @@ function VerifyFlowStepIndicator({ // i18n-tasks-use t('step_indicator.flows.idv.getting_started') // i18n-tasks-use t('step_indicator.flows.idv.verify_id') // i18n-tasks-use t('step_indicator.flows.idv.verify_info') - // i18n-tasks-use t('step_indicator.flows.idv.verify_phone_or_address') + // i18n-tasks-use t('step_indicator.flows.idv.verify_phone') // i18n-tasks-use t('step_indicator.flows.idv.secure_account') // i18n-tasks-use t('step_indicator.flows.idv.find_a_post_office') // i18n-tasks-use t('step_indicator.flows.idv.go_to_the_post_office') - // i18n-tasks-use t('step_indicator.flows.idv.get_a_letter') + // i18n-tasks-use t('step_indicator.flows.idv.verify_address') + // i18n-tasks-use t('step_indicator.flows.idv.re_enter_password') return ( diff --git a/app/jobs/reports/identity_verification_report.rb b/app/jobs/reports/identity_verification_report.rb index f54bfbb4ea9..8101c0c85e8 100644 --- a/app/jobs/reports/identity_verification_report.rb +++ b/app/jobs/reports/identity_verification_report.rb @@ -26,12 +26,22 @@ def perform(report_date) email: email, subject: "Daily Identity Verification Report - #{report_date.to_date}", reports: reports, - message: preamble, + message: message, attachment_format: :xlsx, ).deliver_now end end + def message + <<~HTML.html_safe # rubocop:disable Rails/OutputSafety + #{preamble} + + + Identity Verification Metrics Definitions + + HTML + end + def preamble <<~HTML.html_safe # rubocop:disable Rails/OutputSafety

diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 7ac91a2a4fb..2a4b3ea8482 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -257,10 +257,12 @@ def automatic_fraud_rejection(fraud_rejection_at:, **extra) # Tracks when the user creates a set of backup mfa codes. # @param [Integer] enabled_mfa_methods_count number of registered mfa methods for the user - def backup_code_created(enabled_mfa_methods_count:, **extra) + # @param [Boolean] in_account_creation_flow Whether user is going through creation flow + def backup_code_created(enabled_mfa_methods_count:, in_account_creation_flow:, **extra) track_event( 'Backup Code Created', - enabled_mfa_methods_count: enabled_mfa_methods_count, + enabled_mfa_methods_count:, + in_account_creation_flow:, **extra, ) end @@ -273,19 +275,28 @@ def backup_code_regenerate_visit(in_account_creation_flow:, **extra) # Track user creating new BackupCodeSetupForm, record form submission Hash # @param [Boolean] success + # @param [Hash] mfa_method_counts Hash of MFA method with the number of that method on the account + # @param [Number] enabled_mfa_methods_count Number of enabled MFA methods on the account + # @param [Boolean] in_account_creation_flow Whether page is visited as part of account creation # @param [Hash] errors # @param [Hash] error_details def backup_code_setup_visit( success:, + mfa_method_counts:, + enabled_mfa_methods_count:, + in_account_creation_flow:, errors: nil, error_details: nil, **extra ) track_event( 'Backup Code Setup Visited', - success: success, - errors: errors, - error_details: error_details, + success:, + errors:, + error_details:, + mfa_method_counts:, + enabled_mfa_methods_count:, + in_account_creation_flow:, **extra, ) end @@ -313,6 +324,11 @@ def cancel_account_reset_recovery track_event('Account Reset: Cancel Account Recovery Options') end + # User visits the "Are you sure you want to cancel and exit" page + def completions_cancellation_visited + track_event(:completions_cancellation_visited) + end + # User was logged out due to an existing active session def concurrent_session_logout track_event(:concurrent_session_logout) @@ -3771,12 +3787,14 @@ def multi_factor_auth( # Tracks when the the user has added the MFA method phone to their account # @param [Integer] enabled_mfa_methods_count number of registered mfa methods for the user - def multi_factor_auth_added_phone(enabled_mfa_methods_count:, **extra) + # @param [Hash] recaptcha_annotation Details of reCAPTCHA annotation, if submitted + def multi_factor_auth_added_phone(enabled_mfa_methods_count:, recaptcha_annotation:, **extra) track_event( 'Multi-Factor Authentication: Added phone', { method_name: :phone, enabled_mfa_methods_count: enabled_mfa_methods_count, + recaptcha_annotation:, **extra, }.compact, ) @@ -4154,6 +4172,13 @@ def openid_connect_bearer_token(success:, ial:, client_id:, errors:, **extra) end # Tracks when openid authorization request is made + # @param [Boolean] success Whether form validations were succcessful + # @param [Hash] errors Errors resulting from form validation + # @param [String] prompt OIDC prompt parameter + # @param [Boolean] allow_prompt_login Whether service provider is configured to allow prompt=login + # @param [Boolean] code_challenge_present Whether code challenge is present + # @param [Boolean, nil] service_provider_pkce Whether service provider is configured with PKCE + # @param [String, nil] referer Request referer # @param [String] client_id # @param [String] scope # @param [Array] acr_values @@ -4162,6 +4187,13 @@ def openid_connect_bearer_token(success:, ial:, client_id:, errors:, **extra) # @param [Boolean] unauthorized_scope # @param [Boolean] user_fully_authenticated def openid_connect_request_authorization( + success:, + errors:, + prompt:, + allow_prompt_login:, + code_challenge_present:, + service_provider_pkce:, + referer:, client_id:, scope:, acr_values:, @@ -4173,13 +4205,20 @@ def openid_connect_request_authorization( ) track_event( 'OpenID Connect: authorization request', - client_id: client_id, - scope: scope, - acr_values: acr_values, - vtr: vtr, - vtr_param: vtr_param, - unauthorized_scope: unauthorized_scope, - user_fully_authenticated: user_fully_authenticated, + success:, + errors:, + prompt:, + allow_prompt_login:, + code_challenge_present:, + service_provider_pkce:, + referer:, + client_id:, + scope:, + acr_values:, + vtr:, + vtr_param:, + unauthorized_scope:, + user_fully_authenticated:, **extra, ) end @@ -4256,9 +4295,11 @@ def password_changed(success:, errors:, **extra) # @param [Boolean] success # @param [Hash] errors + # @param [String] user_id UUID of the user + # @param [Boolean] request_id_present Whether request_id URL parameter is present # The user added a password after verifying their email for account creation - def password_creation(success:, errors:, **extra) - track_event('Password Creation', success: success, errors: errors, **extra) + def password_creation(success:, errors:, user_id:, request_id_present:, **extra) + track_event('Password Creation', success:, errors:, user_id:, request_id_present:, **extra) end # The user got their password incorrect the max number of times, their session was terminated @@ -5071,6 +5112,7 @@ def sp_revoke_consent_visited(issuer:, **extra) # @param [Hash] telephony_response # @param [:test, :pinpoint] adapter which adapter the OTP was delivered with # @param [Boolean] success + # @param [Hash] recaptcha_annotation Details of reCAPTCHA annotation, if submitted # A phone one-time password send was attempted def telephony_otp_sent( area_code:, @@ -5082,6 +5124,7 @@ def telephony_otp_sent( telephony_response:, adapter:, success:, + recaptcha_annotation: nil, **extra ) track_event( @@ -5096,6 +5139,7 @@ def telephony_otp_sent( telephony_response: telephony_response, adapter: adapter, success: success, + recaptcha_annotation:, **extra, }, ) @@ -5221,6 +5265,7 @@ def user_registration_2fa_setup_visit( # @param [String] service_provider_name # @param [String] page_occurence # @param [String] needs_completion_screen_reason + # @param [Boolean] in_account_creation_flow Whether user is going through account creation # @param [Array] sp_request_requested_attributes # @param [Array] sp_session_requested_attributes def user_registration_agency_handoff_page_visit( @@ -5228,6 +5273,7 @@ def user_registration_agency_handoff_page_visit( service_provider_name:, page_occurence:, needs_completion_screen_reason:, + in_account_creation_flow:, sp_session_requested_attributes:, sp_request_requested_attributes: nil, ialmax: nil, @@ -5235,13 +5281,14 @@ def user_registration_agency_handoff_page_visit( ) track_event( 'User registration: agency handoff visited', - ial2: ial2, - ialmax: ialmax, - service_provider_name: service_provider_name, - page_occurence: page_occurence, - needs_completion_screen_reason: needs_completion_screen_reason, - sp_request_requested_attributes: sp_request_requested_attributes, - sp_session_requested_attributes: sp_session_requested_attributes, + ial2:, + ialmax:, + service_provider_name:, + page_occurence:, + needs_completion_screen_reason:, + in_account_creation_flow:, + sp_request_requested_attributes:, + sp_session_requested_attributes:, **extra, ) end diff --git a/app/services/encrypted_document_storage/document_writer.rb b/app/services/encrypted_document_storage/document_writer.rb deleted file mode 100644 index 7861bd4ad57..00000000000 --- a/app/services/encrypted_document_storage/document_writer.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -module EncryptedDocumentStorage - class DocumentWriter - def encrypt_and_write_document( - front_image:, - front_image_content_type:, - back_image:, - back_image_content_type: - ) - key = SecureRandom.bytes(32) - encrypted_front_image = aes_cipher.encrypt(front_image, key) - encrypted_back_image = aes_cipher.encrypt(back_image, key) - - front_filename = build_filename_for_content_type(front_image_content_type) - back_filename = build_filename_for_content_type(back_image_content_type) - - storage.write_image(encrypted_image: encrypted_front_image, name: front_filename) - storage.write_image(encrypted_image: encrypted_back_image, name: back_filename) - - WriteDocumentResult.new( - front_filename: front_filename, - back_filename: back_filename, - encryption_key: Base64.strict_encode64(key), - ) - end - - def storage - @storage ||= begin - if Rails.env.production? - S3Storage.new - else - LocalStorage.new - end - end - end - - def aes_cipher - @aes_cipher ||= Encryption::AesCipher.new - end - - # @return [String] A new, unique S3 key for an image of the given content type. - def build_filename_for_content_type(content_type) - ext = Rack::Mime::MIME_TYPES.rassoc(content_type)&.first - "#{SecureRandom.uuid}#{ext}" - end - end -end diff --git a/app/services/encrypted_document_storage/local_storage.rb b/app/services/encrypted_document_storage/local_storage.rb deleted file mode 100644 index ba94ad7165a..00000000000 --- a/app/services/encrypted_document_storage/local_storage.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -module EncryptedDocumentStorage - class LocalStorage - # Used in tests to verify results - def read_image(name:) - filepath = tmp_document_storage_dir.join(name) - File.read(filepath) - end - - def write_image(encrypted_image:, name:) - FileUtils.mkdir_p(tmp_document_storage_dir) - filepath = tmp_document_storage_dir.join(name) - File.write(filepath, encrypted_image) - end - - def tmp_document_storage_dir - Rails.root.join('tmp', 'encrypted_doc_storage') - end - end -end diff --git a/app/services/encrypted_document_storage/s3_storage.rb b/app/services/encrypted_document_storage/s3_storage.rb deleted file mode 100644 index 664908b6535..00000000000 --- a/app/services/encrypted_document_storage/s3_storage.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module EncryptedDocumentStorage - class S3Storage - def write_image(encrypted_image:, name:) - s3_client.put_object( - bucket: IdentityConfig.store.encrypted_document_storage_s3_bucket, - body: encrypted_image, - key: name, - ) - end - - private - - def s3_client - Aws::S3::Client.new( - http_open_timeout: 5, - http_read_timeout: 5, - compute_checksums: false, - ) - end - end -end diff --git a/app/services/encrypted_document_storage/write_document_result.rb b/app/services/encrypted_document_storage/write_document_result.rb deleted file mode 100644 index 7c5bfc8d008..00000000000 --- a/app/services/encrypted_document_storage/write_document_result.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -module EncryptedDocumentStorage - WriteDocumentResult = Struct.new( - :front_filename, - :back_filename, - :encryption_key, - keyword_init: true, - ) -end diff --git a/app/services/idv/flows/in_person_flow.rb b/app/services/idv/flows/in_person_flow.rb index f6b95105694..823dfc7c891 100644 --- a/app/services/idv/flows/in_person_flow.rb +++ b/app/services/idv/flows/in_person_flow.rb @@ -17,16 +17,16 @@ class InPersonFlow < Flow::BaseFlow STEP_INDICATOR_STEPS = [ { name: :find_a_post_office }, { name: :verify_info }, - { name: :verify_phone_or_address }, - { name: :secure_account }, + { name: :verify_phone }, + { name: :re_enter_password }, { name: :go_to_the_post_office }, ].freeze STEP_INDICATOR_STEPS_GPO = [ { name: :find_a_post_office }, { name: :verify_info }, + { name: :verify_address }, { name: :secure_account }, - { name: :get_a_letter }, { name: :go_to_the_post_office }, ].freeze diff --git a/app/services/idv/phone_confirmation_session.rb b/app/services/idv/phone_confirmation_session.rb index 5653bede092..fa269c829a1 100644 --- a/app/services/idv/phone_confirmation_session.rb +++ b/app/services/idv/phone_confirmation_session.rb @@ -2,36 +2,54 @@ module Idv class PhoneConfirmationSession - attr_reader :code, :phone, :sent_at, :delivery_method + attr_reader :code, :phone, :sent_at, :delivery_method, :user - def self.generate_code - OtpCodeGenerator.generate_alphanumeric_digits( - TwoFactorAuthenticatable::PROOFING_DIRECT_OTP_LENGTH, - ) + def self.generate_code(user:, delivery_method:) + bucket = AbTests::IDV_TEN_DIGIT_OTP.bucket(user&.uuid) + if delivery_method == :voice && bucket == :ten_digit_otp + OtpCodeGenerator.generate_digits(10) + else # original, bucket defaults to :six_alphanumeric_otp + OtpCodeGenerator.generate_alphanumeric_digits( + TwoFactorAuthenticatable::PROOFING_DIRECT_OTP_LENGTH, + ) + end end - def initialize(code:, phone:, sent_at:, delivery_method:) + def initialize(code:, phone:, sent_at:, delivery_method:, user:) @code = code @phone = phone @sent_at = sent_at @delivery_method = delivery_method.to_sym + @user = user end - def self.start(phone:, delivery_method:) + def self.start(phone:, delivery_method:, user:) new( - code: generate_code, + code: generate_code(user: user, delivery_method: delivery_method), phone: phone, sent_at: Time.zone.now, delivery_method: delivery_method, + user: user, ) end + def ab_test_analytics_args + return {} unless IdentityConfig.store.ab_testing_idv_ten_digit_otp_enabled + + { + AbTests::IDV_TEN_DIGIT_OTP.experiment_name => { + bucket: AbTests::IDV_TEN_DIGIT_OTP.bucket(user.uuid), + }, + } + end + def regenerate_otp self.class.new( - code: self.class.generate_code, + code: self.class.generate_code(user: user, delivery_method: delivery_method), phone: phone, sent_at: Time.zone.now, delivery_method: delivery_method, + user: user, ) end @@ -62,6 +80,7 @@ def to_h phone: phone, sent_at: sent_at.to_i, delivery_method: delivery_method, + user_id: user&.id, } end @@ -71,6 +90,7 @@ def self.from_h(hash) phone: hash[:phone], sent_at: Time.zone.at(hash[:sent_at]), delivery_method: hash[:delivery_method].to_sym, + user: hash[:user_id].nil? ? nil : User.find(hash[:user_id]), ) end end diff --git a/app/services/idv/phone_step.rb b/app/services/idv/phone_step.rb index 0bd91802d14..b12946bcf77 100644 --- a/app/services/idv/phone_step.rb +++ b/app/services/idv/phone_step.rb @@ -143,6 +143,7 @@ def start_phone_confirmation_session idv_session.user_phone_confirmation_session = Idv::PhoneConfirmationSession.start( phone: PhoneFormatter.format(applicant[:phone]), delivery_method: otp_delivery_preference, + user: idv_session.current_user, # needed for 10-digit A/B test ) end diff --git a/app/services/idv/send_phone_confirmation_otp.rb b/app/services/idv/send_phone_confirmation_otp.rb index a32b078c3f2..036ed2e6672 100644 --- a/app/services/idv/send_phone_confirmation_otp.rb +++ b/app/services/idv/send_phone_confirmation_otp.rb @@ -66,7 +66,8 @@ def send_otp otp: code, to: phone, expiration: TwoFactorAuthenticatable::DIRECT_OTP_VALID_FOR_MINUTES, - otp_format: I18n.t('telephony.format_type.character'), + otp_format: I18n.t("telephony.format_type.#{format}"), + otp_length: I18n.t("telephony.format_length.#{length}"), channel: delivery_method, domain: IdentityConfig.store.domain_name, country_code: parsed_phone.country, @@ -79,6 +80,24 @@ def send_otp otp_sent_response end + def bucket + @bucket ||= AbTests::IDV_TEN_DIGIT_OTP.bucket( + idv_session.user_phone_confirmation_session.user.uuid, + ) + end + + def format + return 'digit' if delivery_method == :voice && bucket == :ten_digit_otp + + 'character' + end + + def length + return 'ten' if delivery_method == :voice && bucket == :ten_digit_otp + + 'six' + end + def otp_sent_response FormResponse.new( success: telephony_response.success?, extra: extra_analytics_attributes, diff --git a/app/services/recaptcha_annotator.rb b/app/services/recaptcha_annotator.rb new file mode 100644 index 00000000000..d9fea16d6b5 --- /dev/null +++ b/app/services/recaptcha_annotator.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +class RecaptchaAnnotator + attr_reader :assessment_id, :analytics + + # See: https://cloud.google.com/recaptcha-enterprise/docs/reference/rest/v1/projects.assessments/annotate#reason + module AnnotationReasons + INITIATED_TWO_FACTOR = :INITIATED_TWO_FACTOR + PASSED_TWO_FACTOR = :PASSED_TWO_FACTOR + end + + # See: https://cloud.google.com/recaptcha-enterprise/docs/reference/rest/v1/projects.assessments/annotate#annotation + module Annotations + LEGITIMATE = :LEGITIMATE + FRAUDULENT = :FRAUDULENT + end + + class << self + def annotate(assessment_id:, reason: nil, annotation: nil) + return if assessment_id.blank? + + if FeatureManagement.recaptcha_enterprise? + submit_annotation(assessment_id:, reason:, annotation:) + end + + { assessment_id:, reason:, annotation: } + end + + private + + def submit_annotation(assessment_id:, reason:, annotation:) + request_body = { annotation:, reasons: reason && [reason] }.compact + faraday.post(annotation_url(assessment_id:), request_body) do |request| + request.options.context = { service_name: 'recaptcha_annotate' } + end + rescue Faraday::Error => error + NewRelic::Agent.notice_error(error) + end + + def faraday + Faraday.new do |conn| + conn.request :instrumentation, name: 'request_log.faraday' + conn.request :json + conn.response :json + end + end + + def annotation_url(assessment_id:) + UriService.add_params( + format( + '%{base_endpoint}/%{assessment_id}:annotate', + base_endpoint: BASE_ENDPOINT, + project_id: IdentityConfig.store.recaptcha_enterprise_project_id, + assessment_id:, + ), + key: IdentityConfig.store.recaptcha_enterprise_api_key, + ) + end + end + + private + + BASE_ENDPOINT = 'https://recaptchaenterprise.googleapis.com/v1' +end diff --git a/app/views/accounts/_badges.html.erb b/app/views/accounts/_badges.html.erb index c0a8fcc27c0..1dfdaf4f1c0 100644 --- a/app/views/accounts/_badges.html.erb +++ b/app/views/accounts/_badges.html.erb @@ -1,7 +1,7 @@ <% if @presenter.show_unphishable_badge? %> - <%= render BadgeComponent.new(icon: :unphishable).with_content(t('headings.account.unphishable')) %> + <%= render BadgeComponent.new(icon: :lock).with_content(t('headings.account.unphishable')) %> <% end %> <% if @presenter.show_verified_badge? %> - <%= render BadgeComponent.new(icon: :success).with_content(t('headings.account.verified_account')) %> + <%= render BadgeComponent.new(icon: :check_circle).with_content(t('headings.account.verified_account')) %> <% end %> diff --git a/app/views/completions_cancellation/show.html.erb b/app/views/completions_cancellation/show.html.erb new file mode 100644 index 00000000000..47a34fecc00 --- /dev/null +++ b/app/views/completions_cancellation/show.html.erb @@ -0,0 +1,41 @@ +<% self.title = t('titles.cancel_exit_login', app_name: APP_NAME) %> + +<%= render StatusPageComponent.new(status: :warning) do |c| %> + <% c.with_header { t('headings.cancellations.login_cancel_prompt', app_name: APP_NAME) } %> + +

+ <%= t('login_cancel.header', app_name: APP_NAME) %> +

+ +
    +
  • <%= t('login_cancel.bullet1_html', app_name: APP_NAME, sp_name: decorated_sp_session.sp_name) %>
  • +
  • + <%= t( + 'login_cancel.bullet2_html', app_name: APP_NAME, + link_html: link_to( + t('login_cancel.account_page'), + account_path, + ) + ) %> +
  • +
+ +
+
+ <%= render ButtonComponent.new( + url: sign_up_completed_path, + big: true, + wide: true, + class: 'margin-bottom-2 margin-top-3', + ).with_content(t('login_cancel.keep_going')) %> + + <%= render ButtonComponent.new( + url: return_to_sp_cancel_path(step: :sign_up), + big: true, + wide: true, + outline: true, + ).with_content(t('login_cancel.exit', app_name: APP_NAME)) %> +
+
+ +<% end %> diff --git a/app/views/idv/by_mail/enter_code/index.html.erb b/app/views/idv/by_mail/enter_code/index.html.erb index 98d858879e4..db906f3bc91 100644 --- a/app/views/idv/by_mail/enter_code/index.html.erb +++ b/app/views/idv/by_mail/enter_code/index.html.erb @@ -1,7 +1,7 @@ <% content_for(:pre_flash_content) do %> <%= render StepIndicatorComponent.new( steps: step_indicator_steps, - current_step: :get_a_letter, + current_step: :verify_address, locale_scope: 'idv', class: 'margin-x-neg-2 margin-top-neg-4 tablet:margin-x-neg-6 tablet:margin-top-neg-4', ) %> diff --git a/app/views/idv/by_mail/enter_code_rate_limited/index.html.erb b/app/views/idv/by_mail/enter_code_rate_limited/index.html.erb index de3a88411f6..d6cc5b2e304 100644 --- a/app/views/idv/by_mail/enter_code_rate_limited/index.html.erb +++ b/app/views/idv/by_mail/enter_code_rate_limited/index.html.erb @@ -30,7 +30,7 @@ app_name: APP_NAME, sp_name: decorated_sp_session.sp_name, ), return_to_sp_failure_to_proof_path( - step: 'get_a_letter', + step: 'verify_address', location: request.params[:action], ), ) %> @@ -41,7 +41,7 @@ app_name: APP_NAME, ), return_to_sp_failure_to_proof_path( - step: 'get_a_letter', + step: 'verify_address', location: request.params[:action], ), ) %> diff --git a/app/views/idv/by_mail/letter_enqueued/show.html.erb b/app/views/idv/by_mail/letter_enqueued/show.html.erb index 927be4030f8..e374f2c6a85 100644 --- a/app/views/idv/by_mail/letter_enqueued/show.html.erb +++ b/app/views/idv/by_mail/letter_enqueued/show.html.erb @@ -1,7 +1,7 @@ <% content_for(:pre_flash_content) do %> <%= render StepIndicatorComponent.new( steps: step_indicator_steps, - current_step: :get_a_letter, + current_step: :verify_address, locale_scope: 'idv', class: 'margin-x-neg-2 margin-top-neg-4 tablet:margin-x-neg-6 tablet:margin-top-neg-4', ) %> @@ -36,7 +36,7 @@ <% if decorated_sp_session.sp_name.present? %> <%= link_to( t('idv.cancel.actions.exit', app_name: APP_NAME), - return_to_sp_cancel_path(step: :get_a_letter, location: :come_back_later), + return_to_sp_cancel_path(step: :verify_address, location: :come_back_later), class: 'usa-button usa-button--big usa-button--wide', ) %> <% else %> diff --git a/app/views/idv/by_mail/request_letter/index.html.erb b/app/views/idv/by_mail/request_letter/index.html.erb index 36ff8925eaa..4f3cc18f59a 100644 --- a/app/views/idv/by_mail/request_letter/index.html.erb +++ b/app/views/idv/by_mail/request_letter/index.html.erb @@ -3,7 +3,7 @@ <% content_for(:pre_flash_content) do %> <%= render StepIndicatorComponent.new( steps: step_indicator_steps, - current_step: @step_indicator_current_step, + current_step: :verify_address, locale_scope: 'idv', class: 'margin-x-neg-2 margin-top-neg-4 tablet:margin-x-neg-6 tablet:margin-top-neg-4', ) %> diff --git a/app/views/idv/in_person/state_id/show.html.erb b/app/views/idv/in_person/state_id/show.html.erb index fa3a4b8fccd..d7f03c7ec35 100644 --- a/app/views/idv/in_person/state_id/show.html.erb +++ b/app/views/idv/in_person/state_id/show.html.erb @@ -237,7 +237,7 @@ <% end %> <% end %> <% if updating_state_id %> - <%= render 'idv/shared/back', action: 'cancel_update_state_id' %> + <%= render 'idv/shared/back', fallback_path: idv_in_person_verify_info_path %> <% else %> <%= render 'idv/doc_auth/cancel', step: 'state_id' %> <% end %> diff --git a/app/views/idv/otp_verification/show.html.erb b/app/views/idv/otp_verification/show.html.erb index b0c444118d1..90e43a76ba9 100644 --- a/app/views/idv/otp_verification/show.html.erb +++ b/app/views/idv/otp_verification/show.html.erb @@ -1,7 +1,7 @@ <% content_for(:pre_flash_content) do %> <%= render StepIndicatorComponent.new( steps: step_indicator_steps, - current_step: :verify_phone_or_address, + current_step: :verify_phone, locale_scope: 'idv', class: 'margin-x-neg-2 margin-top-neg-4 tablet:margin-x-neg-6 tablet:margin-top-neg-4', ) %> @@ -21,7 +21,7 @@ value: @code, numeric: false, autofocus: true, - code_length: TwoFactorAuthenticatable::PROOFING_DIRECT_OTP_LENGTH, + code_length: @otp_code_length, optional_prefix: '#', class: 'margin-bottom-5', ) %> diff --git a/app/views/idv/personal_key/show.html.erb b/app/views/idv/personal_key/show.html.erb index 2f75f1aa9da..ff2cbc94206 100644 --- a/app/views/idv/personal_key/show.html.erb +++ b/app/views/idv/personal_key/show.html.erb @@ -1,7 +1,7 @@ <% content_for(:pre_flash_content) do %> <%= render StepIndicatorComponent.new( steps: step_indicator_steps, - current_step: StepIndicatorComponent::ALL_STEPS_COMPLETE, + current_step: step_indicator_step, locale_scope: 'idv', class: 'margin-x-neg-2 margin-top-neg-4 tablet:margin-x-neg-6 tablet:margin-top-neg-4', ) %> diff --git a/app/views/idv/phone/new.html.erb b/app/views/idv/phone/new.html.erb index 5b75d108c96..f87d4f579d2 100644 --- a/app/views/idv/phone/new.html.erb +++ b/app/views/idv/phone/new.html.erb @@ -1,7 +1,7 @@ <% content_for(:pre_flash_content) do %> <%= render StepIndicatorComponent.new( steps: step_indicator_steps, - current_step: :verify_phone_or_address, + current_step: :verify_phone, locale_scope: 'idv', class: 'margin-x-neg-2 margin-top-neg-4 tablet:margin-x-neg-6 tablet:margin-top-neg-4', ) %> diff --git a/app/views/idv/phone_errors/_warning.html.erb b/app/views/idv/phone_errors/_warning.html.erb index 1db68d18f5d..2148f16a020 100644 --- a/app/views/idv/phone_errors/_warning.html.erb +++ b/app/views/idv/phone_errors/_warning.html.erb @@ -14,7 +14,7 @@ locals: text: t('idv.failure.button.warning'), url: idv_phone_path, }, - current_step: :verify_phone_or_address, + current_step: :verify_phone, options: [ local_assigns[:contact_support_option] && { url: MarketingSite.contact_url, diff --git a/app/views/idv/phone_errors/failure.html.erb b/app/views/idv/phone_errors/failure.html.erb index 899290424e2..52384e59624 100644 --- a/app/views/idv/phone_errors/failure.html.erb +++ b/app/views/idv/phone_errors/failure.html.erb @@ -2,7 +2,7 @@ 'idv/shared/error', title: t('titles.failure.phone_verification'), heading: t('idv.failure.phone.rate_limited.heading'), - current_step: :verify_phone_or_address, + current_step: :verify_phone, options: [ { url: MarketingSite.contact_url, @@ -44,4 +44,3 @@ <%= render PageFooterComponent.new do %> <%= link_to(t('links.cancel'), idv_cancel_path(step: :phone_error)) %> <% end %> - diff --git a/app/views/idv/phone_errors/warning.html.erb b/app/views/idv/phone_errors/warning.html.erb index a088ebd3b77..b22229997f2 100644 --- a/app/views/idv/phone_errors/warning.html.erb +++ b/app/views/idv/phone_errors/warning.html.erb @@ -3,7 +3,7 @@ type: :warning, title: t('titles.failure.phone_verification'), heading: t('idv.failure.phone.warning.heading'), - current_step: :verify_phone_or_address, + current_step: :verify_phone, ) do %> <% if @phone %> diff --git a/app/views/idv/please_call/show.html.erb b/app/views/idv/please_call/show.html.erb index 71b455203e4..fc25e90525e 100644 --- a/app/views/idv/please_call/show.html.erb +++ b/app/views/idv/please_call/show.html.erb @@ -2,7 +2,7 @@ <% content_for(:pre_flash_content) do %> <%= render StepIndicatorComponent.new( steps: Idv::StepIndicatorConcern::STEP_INDICATOR_STEPS, - current_step: :secure_account, + current_step: :re_enter_password, locale_scope: 'idv', class: 'margin-x-neg-2 margin-top-neg-4 tablet:margin-x-neg-6 tablet:margin-top-neg-4', ) %> diff --git a/app/views/sign_up/completions/show.html.erb b/app/views/sign_up/completions/show.html.erb index 86d6e4bf864..0ddd6e6dfa2 100644 --- a/app/views/sign_up/completions/show.html.erb +++ b/app/views/sign_up/completions/show.html.erb @@ -57,5 +57,5 @@ <% end %> <%= render PageFooterComponent.new do %> - <%= link_to t('links.cancel'), return_to_sp_cancel_path(step: :sign_up) %> + <%= link_to t('links.cancel'), sign_up_completed_cancel_path %> <% end %> diff --git a/config/application.yml.default b/config/application.yml.default index dcdbf572266..228d7775106 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -20,6 +20,8 @@ aamva_cert_enabled: true aamva_supported_jurisdictions: '["AL","AR","AZ","CO","CT","DC","DE","FL","GA","HI","IA","ID","IL","IN","KS","KY","MA","MD","ME","MI","MO","MS","MT","NC","ND","NE","NJ","NM","NV","OH","OR","PA","RI","SC","SD","TN","TX","VA","VT","WA","WI","WV","WY"]' aamva_verification_request_timeout: 5.0 aamva_verification_url: https://example.org:12345/verification/url +ab_testing_idv_ten_digit_otp_enabled: false +ab_testing_idv_ten_digit_otp_percent: 0 all_redirect_uris_cache_duration_minutes: 2 allowed_ialmax_providers: '[]' allowed_verified_within_providers: '[]' @@ -105,8 +107,6 @@ enable_load_testing_mode: false enable_rate_limiting: true enable_test_routes: true enable_usps_verification: true -encrypted_document_storage_enabled: false -encrypted_document_storage_s3_bucket: '' event_disavowal_expiration_hours: 240 feature_idv_force_gpo_verification_enabled: false feature_idv_hybrid_flow_enabled: true diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb index 096647a52c4..feb9607c48c 100644 --- a/config/initializers/ab_tests.rb +++ b/config/initializers/ab_tests.rb @@ -30,4 +30,15 @@ module AbTests 0, }, ).freeze + + IDV_TEN_DIGIT_OTP = AbTestBucket.new( + experiment_name: 'idv_ten_digit_otp', + default_bucket: :six_alphanumeric_otp, + buckets: { + ten_digit_otp: + IdentityConfig.store.ab_testing_idv_ten_digit_otp_enabled ? + IdentityConfig.store.ab_testing_idv_ten_digit_otp_percent : + 0, + }, + ).freeze end diff --git a/config/locales/doc_auth/es.yml b/config/locales/doc_auth/es.yml index f1f4b4fc517..d9553a752b0 100644 --- a/config/locales/doc_auth/es.yml +++ b/config/locales/doc_auth/es.yml @@ -245,7 +245,8 @@ es: too_many_faces: Demasiados rostros ssn: Necesitamos su número de Seguro Social para verificar su nombre, fecha de nacimiento y dirección. - stepping_up_html: Verifica su identidad de nuevo para acceder a este servicio. %{link_html} + stepping_up_html: Verifique su identidad de nuevo para acceder a este servicio. + %{link_html} tag: Recomendado upload_from_computer: '¿No tiene un teléfono? Cargue fotos de su identificación desde esta computadora.' diff --git a/config/locales/headings/en.yml b/config/locales/headings/en.yml index 89dad46edc7..50d423f8076 100644 --- a/config/locales/headings/en.yml +++ b/config/locales/headings/en.yml @@ -18,6 +18,7 @@ en: add_info: phone: Add a phone number cancellations: + login_cancel_prompt: Are you sure you want to cancel and exit %{app_name}? prompt: Are you sure you want to cancel? create_account_new_users: Create an account for new users create_account_with_sp: diff --git a/config/locales/headings/es.yml b/config/locales/headings/es.yml index 893fe6e34d6..6bad5062a74 100644 --- a/config/locales/headings/es.yml +++ b/config/locales/headings/es.yml @@ -18,7 +18,8 @@ es: add_info: phone: Agregar un número de teléfono cancellations: - prompt: '¿Está seguro de que desea cancelar?' + login_cancel_prompt: ¿Está seguro de que desea cancelar y salir de %{app_name}? + prompt: '¿Estas seguro que quieres cancelar?' create_account_new_users: Crear una cuenta para usuarios nuevos create_account_with_sp: sp_text: está usando %{app_name} para permitirle iniciar sesión en su cuenta de diff --git a/config/locales/headings/fr.yml b/config/locales/headings/fr.yml index a9ab58eaee4..054857153a0 100644 --- a/config/locales/headings/fr.yml +++ b/config/locales/headings/fr.yml @@ -18,7 +18,8 @@ fr: add_info: phone: Ajouter un numéro de téléphone cancellations: - prompt: Êtes-vous sûr de vouloir annuler ? + login_cancel_prompt: Êtes-vous sûr de vouloir annuler et quitter %{app_name}? + prompt: Es-tu sûre de vouloir annuler? create_account_new_users: Créer un compte pour les nouveaux utilisateurs create_account_with_sp: sp_text: utilise %{app_name} pour vous permettre de vous connecter à votre diff --git a/config/locales/headings/zh.yml b/config/locales/headings/zh.yml index e3c10322476..c51ece82d09 100644 --- a/config/locales/headings/zh.yml +++ b/config/locales/headings/zh.yml @@ -18,6 +18,7 @@ zh: add_info: phone: 添加一个电话号码 cancellations: + login_cancel_prompt: 你确定要取消并退出 %{app_name} 吗? prompt: 你确定要取消吗? create_account_new_users: 为新用户创建一个账户 create_account_with_sp: diff --git a/config/locales/login_cancel/en.yml b/config/locales/login_cancel/en.yml new file mode 100644 index 00000000000..5552a35d773 --- /dev/null +++ b/config/locales/login_cancel/en.yml @@ -0,0 +1,11 @@ +--- +en: + login_cancel: + account_page: account page + bullet1_html: You won’t be able to use %{app_name} to access + %{sp_name} until you share your information with them. + bullet2_html: You’ll still have a %{app_name} account which you can manage or + delete on your %{link_html}. + exit: Exit %{app_name} + header: 'If you exit %{app_name} now:' + keep_going: No, keep going diff --git a/config/locales/login_cancel/es.yml b/config/locales/login_cancel/es.yml new file mode 100644 index 00000000000..d9728a5baa5 --- /dev/null +++ b/config/locales/login_cancel/es.yml @@ -0,0 +1,12 @@ +--- +es: + login_cancel: + account_page: página de su cuenta + bullet1_html: No podrá usar %{app_name} para acceder a + %{sp_name} hasta que dé a conocer su información a esa + agencia. + bullet2_html: Seguirá teniendo una cuenta de %{app_name} que puede administrar o + eliminar en la %{link_html}. + exit: Salir de %{app_name} + header: 'Si sale de %{app_name} ahora:' + keep_going: No, continuar diff --git a/config/locales/login_cancel/fr.yml b/config/locales/login_cancel/fr.yml new file mode 100644 index 00000000000..c2bf4a49e52 --- /dev/null +++ b/config/locales/login_cancel/fr.yml @@ -0,0 +1,12 @@ +--- +fr: + login_cancel: + account_page: page de votre compte + bullet1_html: Vous ne pourrez pas utiliser %{app_name} pour accéder à + %{sp_name} tant que vous ne leur communiquerez pas les + renseignements vous concernant. + bullet2_html: Vous aurez néanmoins toujours un compte %{app_name} que vous + pourrez gérer ou supprimer la %{link_html}. + exit: Quitter %{app_name} + header: 'Si vous quittez %{app_name} maintenant:' + keep_going: Non, continuer diff --git a/config/locales/login_cancel/zh.yml b/config/locales/login_cancel/zh.yml new file mode 100644 index 00000000000..bb072169813 --- /dev/null +++ b/config/locales/login_cancel/zh.yml @@ -0,0 +1,9 @@ +--- +zh: + login_cancel: + account_page: 账户页面 + bullet1_html: 在你与 %{app_name} 合作伙伴机构分享信息之前,将无法通过 %{sp_name} 访问该机构。 + bullet2_html: 在你%{link_html},将仍有一个 %{app_name} 账户,你可以管理或删除。 + exit: 退出 %{app_name} + header: '如果你现在退出 %{app_name}:' + keep_going: 不,继续下去 diff --git a/config/locales/step_indicator/en.yml b/config/locales/step_indicator/en.yml index 40572148fad..2e1d5bb56a6 100644 --- a/config/locales/step_indicator/en.yml +++ b/config/locales/step_indicator/en.yml @@ -5,13 +5,14 @@ en: flows: idv: find_a_post_office: Find a Post Office - get_a_letter: Get a letter in the mail getting_started: Getting started go_to_the_post_office: Go to the Post Office - secure_account: Re-enter your password + re_enter_password: Re-enter your password + secure_account: Secure your account + verify_address: Verify your address verify_id: Verify your ID verify_info: Verify your information - verify_phone_or_address: Verify your phone number + verify_phone: Verify your phone number status: complete: Completed current: Current step diff --git a/config/locales/step_indicator/es.yml b/config/locales/step_indicator/es.yml index 710e2cd369c..72c6f7b4a0c 100644 --- a/config/locales/step_indicator/es.yml +++ b/config/locales/step_indicator/es.yml @@ -5,13 +5,14 @@ es: flows: idv: find_a_post_office: Busque una oficina de correos - get_a_letter: Obtenga una carta por correo getting_started: Inicio go_to_the_post_office: Vaya a la oficina de correos - secure_account: Vuelve a ingresar tu contraseña + re_enter_password: Vuelva a ingresar su contraseña + secure_account: Proteja su cuenta + verify_address: Verifique su dirección verify_id: Verifique su identidad verify_info: Verifique su información - verify_phone_or_address: Verifique su número de teléfono + verify_phone: Verifique su número de teléfono status: complete: Completado current: Este paso diff --git a/config/locales/step_indicator/fr.yml b/config/locales/step_indicator/fr.yml index e6c2f743e31..288417fff7a 100644 --- a/config/locales/step_indicator/fr.yml +++ b/config/locales/step_indicator/fr.yml @@ -5,13 +5,14 @@ fr: flows: idv: find_a_post_office: Trouver un bureau de poste - get_a_letter: Recevoir une lettre par la poste getting_started: Démarrer go_to_the_post_office: Se rendre au bureau de poste - secure_account: Saisir à nouveau votre mot de passe - verify_id: Vérifier votre identité + re_enter_password: Saisir à nouveau votre mot de passe + secure_account: Sécuriser votre compte + verify_address: Vérifier votre adresse + verify_id: Confirmer votre identité verify_info: Vérifier vos informations - verify_phone_or_address: Vérifier votre numéro de téléphone + verify_phone: Vérifier votre numéro de téléphone status: complete: Étape effectuée current: Étape en cours diff --git a/config/locales/step_indicator/zh.yml b/config/locales/step_indicator/zh.yml index 425f91f461a..e45e8058ee6 100644 --- a/config/locales/step_indicator/zh.yml +++ b/config/locales/step_indicator/zh.yml @@ -5,13 +5,14 @@ zh: flows: idv: find_a_post_office: 找一个邮局 - get_a_letter: 接收一封信 getting_started: 开始 go_to_the_post_office: 去邮局 - secure_account: 保护你的账户安全 + re_enter_password: 重新输入你的密码 + secure_account: 保护你账户的安全 + verify_address: 验证你的地址 verify_id: 验证你的身份证件 verify_info: 验证你的信息 - verify_phone_or_address: 验证电话或地址 + verify_phone: 验证你的电话号码 status: complete: 完成了 current: 目前步骤 diff --git a/config/locales/telephony/en.yml b/config/locales/telephony/en.yml index 2b5517c100b..129515e6c60 100644 --- a/config/locales/telephony/en.yml +++ b/config/locales/telephony/en.yml @@ -22,9 +22,9 @@ en: %{app_name}: Your one-time code is %{code}. It expires in %{expiration} minutes. Don't share this code with anyone. @%{domain} #%{code} - voice: Hello! Your 6-%{format_type} %{app_name} one-time code is, %{code}. Your - one-time code is, %{code}. Again, your one-time code is %{code}. This - code expires in %{expiration} minutes. + voice: Hello! Your %{format_length}-%{format_type} %{app_name} one-time code is, + %{code}. Your one-time code is, %{code}. Again, your one-time code is + %{code}. This code expires in %{expiration} minutes. doc_auth_link: |- %{app_name}: %{link} You're verifying your identity to access %{sp_or_app_name}. Take a photo of your ID to continue. error: @@ -49,6 +49,9 @@ en: unknown_failure: We are experiencing technical difficulties. Please try again later. voice_unsupported: Invalid phone number. Check that you’ve entered the correct country code or area code. + format_length: + six: '6' + ten: '10' format_type: character: character digit: digit diff --git a/config/locales/telephony/es.yml b/config/locales/telephony/es.yml index dcf8f19afff..ad96bf227f8 100644 --- a/config/locales/telephony/es.yml +++ b/config/locales/telephony/es.yml @@ -23,9 +23,10 @@ es: %{app_name}: La contraseña es %{code}. Esta contraseña puede usarse una vez y se vence en %{expiration} minutos. No la comparta con nadie. @%{domain} #%{code} - voice: 'Hola: Su código de un solo uso de seis %{format_type} de %{app_name} es - %{code}. Su código de un solo uso es %{code}. De nuevo, su código de un - solo uso es %{code}. Este código vence en %{expiration} minutos.' + voice: 'Hola: Su código de un solo uso de %{format_length} %{format_type} de + %{app_name} es %{code}. Su código de un solo uso es %{code}. De nuevo, + su código de un solo uso es %{code}. Este código vence en %{expiration} + minutos.' doc_auth_link: '%{app_name}: %{link} Está verificando su identidad para acceder a %{sp_or_app_name}. Tome una foto de su identificación para continuar.' error: @@ -53,6 +54,9 @@ es: inténtelo de nuevo más tarde. voice_unsupported: Número de teléfono no válido. Verifique haber ingresado el código de país o de área correcto. + format_length: + six: seis + ten: diez format_type: character: carácter digit: dígito diff --git a/config/locales/telephony/fr.yml b/config/locales/telephony/fr.yml index 41b46b14f3b..3075cd54a03 100644 --- a/config/locales/telephony/fr.yml +++ b/config/locales/telephony/fr.yml @@ -25,8 +25,8 @@ fr: %{app_name} : Votre code à usage unique est %{code}. Il expire dans %{expiration} minutes. Ne partagez ce code avec personne. @%{domain} #%{code} - voice: Bonjour ! Votre code à usage unique de six %{format_type} pour - %{app_name} est %{code}. Votre code à usage unique est %{code}. De + voice: Bonjour ! Votre code à usage unique de %{format_length} %{format_type} + pour %{app_name} est %{code}. Votre code à usage unique est %{code}. De nouveau, votre code à usage unique est %{code}. Ce code expire dans %{expiration} minutes. doc_auth_link: "%{app_name} : %{link} Vous êtes en train de confirmer votre @@ -57,6 +57,9 @@ fr: plus tard. voice_unsupported: Numéro de téléphone non valide. Vérifiez que vous avez saisi le bon indicatif pays ou régional. + format_length: + six: six + ten: dix format_type: character: caractère digit: chiffre diff --git a/config/locales/telephony/zh.yml b/config/locales/telephony/zh.yml index bc581007251..a3754af6aab 100644 --- a/config/locales/telephony/zh.yml +++ b/config/locales/telephony/zh.yml @@ -19,8 +19,9 @@ zh: %{app_name}: 你的一次性代码是 %{code}。此代码在 %{expiration} 分钟后作废。请勿与任何人分享此代码。 @%{domain} #%{code} - voice: 你好! 你的 6-%{format_type} %{app_name} 一次性代码是 %{code}。你的一次性代码是 - ,%{code}。重复一下,你的一次性代码是 %{code}。此代码 %{expiration} 分钟后会作废。 + voice: 你好! 你的 %{format_length}-%{format_type} %{app_name} 一次性代码是 + %{code}。你的一次性代码是 ,%{code}。重复一下,你的一次性代码是 %{code}。此代码 %{expiration} + 分钟后会作废。 doc_auth_link: |- %{app_name}: %{link} 你在验证身份以访问 %{sp_or_app_name}。拍张你身份证件的照片以继续。 error: @@ -38,6 +39,9 @@ zh: timeout: 服务器反应时间过长。请再试一次。 unknown_failure: 我们目前遇到技术困难。 请稍后再试。 voice_unsupported: 电话号码有误。检查一下你是否输入了正确的国家代码或区域代码。 + format_length: + six: '6' + ten: '10' format_type: character: 字符 digit: 数码 diff --git a/config/locales/titles/en.yml b/config/locales/titles/en.yml index 35e42c0ed31..f5e6b68de18 100644 --- a/config/locales/titles/en.yml +++ b/config/locales/titles/en.yml @@ -6,6 +6,7 @@ en: add_info: phone: Add a phone number backup_codes: Don’t lose your backup codes + cancel_exit_login: Are you sure you want to cancel and exit %{app_name}? confirmations: delete: Please confirm show: Choose a password diff --git a/config/locales/titles/es.yml b/config/locales/titles/es.yml index db5c6dc40d8..444dca7c3b9 100644 --- a/config/locales/titles/es.yml +++ b/config/locales/titles/es.yml @@ -6,6 +6,7 @@ es: add_info: phone: Agregar un número de teléfono backup_codes: No pierda sus códigos de recuperación + cancel_exit_login: ¿Está seguro de que desea cancelar y salir de %{app_name}? confirmations: delete: Confirme show: Elija una contraseña diff --git a/config/locales/titles/fr.yml b/config/locales/titles/fr.yml index 7ebf628037d..32194b6f48e 100644 --- a/config/locales/titles/fr.yml +++ b/config/locales/titles/fr.yml @@ -6,6 +6,7 @@ fr: add_info: phone: Ajouter un numéro de téléphone backup_codes: Ne perdez pas vos codes de sauvegarde + cancel_exit_login: Êtes-vous sûr de vouloir annuler et quitter %{app_name}? confirmations: delete: Veuillez confirmer show: Choisir un mot de passe diff --git a/config/locales/titles/zh.yml b/config/locales/titles/zh.yml index b524168e886..547d7060505 100644 --- a/config/locales/titles/zh.yml +++ b/config/locales/titles/zh.yml @@ -6,6 +6,7 @@ zh: add_info: phone: 添加电话号码 backup_codes: 别丢了你的备用代码 + cancel_exit_login: 你确定要取消并退出 %{app_name} 吗? confirmations: delete: 请确认 show: 选择一个密码 diff --git a/config/routes.rb b/config/routes.rb index 50c54282757..d35856e3b40 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -317,6 +317,7 @@ get '/redirect/help_center' => 'redirect/help_center#show', as: :help_center_redirect get '/redirect/contact/' => 'redirect/contact#show', as: :contact_redirect get '/redirect/policy/' => 'redirect/policy#show', as: :policy_redirect + get '/sign_up/completed/cancel/' => 'completions_cancellation#show' match '/sign_out' => 'sign_out#destroy', via: %i[get post delete] @@ -396,7 +397,9 @@ # during the deprecation process. get '/in_person_proofing/address' => redirect('/verify/in_person/address', status: 307) put '/in_person_proofing/address' => redirect('/verify/in_person/address', status: 307) + get '/in_person_proofing/state_id' => 'in_person/state_id#show' + put '/in_person_proofing/state_id' => 'in_person/state_id#update' get '/in_person' => 'in_person#index' get '/in_person/ready_to_verify' => 'in_person/ready_to_verify#show', diff --git a/lib/feature_management.rb b/lib/feature_management.rb index 4531cf1a513..413260ffc55 100644 --- a/lib/feature_management.rb +++ b/lib/feature_management.rb @@ -153,6 +153,6 @@ def self.idv_by_mail_only? end def self.idv_allow_selfie_check? - !(Identity::Hostdata.env == 'prod') && IdentityConfig.store.doc_auth_selfie_capture_enabled + IdentityConfig.store.doc_auth_selfie_capture_enabled end end diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 5dee8f89ef1..de4ed292e4f 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -35,6 +35,8 @@ def self.store config.add(:aamva_supported_jurisdictions, type: :json) config.add(:aamva_verification_request_timeout, type: :float) config.add(:aamva_verification_url) + config.add(:ab_testing_idv_ten_digit_otp_enabled, type: :boolean) + config.add(:ab_testing_idv_ten_digit_otp_percent, type: :integer) config.add(:account_reset_token_valid_for_days, type: :integer) config.add(:account_reset_wait_period_days, type: :integer) config.add(:account_reset_fraud_user_wait_period_days, type: :integer, allow_nil: true) @@ -139,8 +141,6 @@ def self.store config.add(:enable_rate_limiting, type: :boolean) config.add(:enable_test_routes, type: :boolean) config.add(:enable_usps_verification, type: :boolean) - config.add(:encrypted_document_storage_enabled, type: :boolean) - config.add(:encrypted_document_storage_s3_bucket, type: :string) config.add(:event_disavowal_expiration_hours, type: :integer) config.add(:feature_idv_force_gpo_verification_enabled, type: :boolean) config.add(:feature_idv_hybrid_flow_enabled, type: :boolean) diff --git a/lib/reporting/identity_verification_report.rb b/lib/reporting/identity_verification_report.rb index ce3c29d93f1..b4f10346945 100644 --- a/lib/reporting/identity_verification_report.rb +++ b/lib/reporting/identity_verification_report.rb @@ -85,6 +85,7 @@ def identity_verification_emailable_report ) end + # rubocop:disable Layout/LineLength def as_csv csv = [] @@ -95,9 +96,9 @@ def as_csv csv << [] csv << ['Metric', '# of Users'] csv << [] - csv << ['Started IdV Verification', idv_started] - csv << ['Submitted welcome page', idv_doc_auth_welcome_submitted] - csv << ['Images uploaded', idv_doc_auth_image_vendor_submitted] + csv << ['IDV started', idv_started] + csv << ['Welcome Submitted', idv_doc_auth_welcome_submitted] + csv << ['Image Submitted', idv_doc_auth_image_vendor_submitted] csv << [] csv << ['Workflow completed', idv_final_resolution] csv << ['Workflow completed - Verified', idv_final_resolution_verified] @@ -106,12 +107,17 @@ def as_csv csv << ['Workflow completed - In-Person Pending', idv_final_resolution_in_person] csv << ['Workflow completed - Fraud Review Pending', idv_final_resolution_fraud_review] csv << [] - csv << ['Successfully verified', successfully_verified_users] - csv << ['Successfully verified - Inline', idv_final_resolution_verified] - csv << ['Successfully verified - GPO Code Entry', gpo_verification_submitted] - csv << ['Successfully verified - In Person', usps_enrollment_status_updated] - csv << ['Successfully verified - Passed Fraud Review', fraud_review_passed] + csv << ['Successfully Verified', successfully_verified_users] + csv << ['Successfully Verified - With phone number', idv_final_resolution_verified] + csv << ['Successfully Verified - With mailed code', gpo_verification_submitted] + csv << ['Successfully Verified - In Person', usps_enrollment_status_updated] + csv << ['Successfully Verified - Passed fraud review', fraud_review_passed] + csv << ['Blanket Proofing Rate (IDV Started to Successfully Verified)', blanket_proofing_rates] + csv << ['Intent Proofing Rate (Welcome Submitted to Successfully Verified)', intent_proofing_rates] + csv << ['Actual Proofing Rate (Image Submitted to Successfully Verified)', actual_proofing_rates] + csv << ['Industry Proofing Rate (Verified minus IDV Rejected)', industry_proofing_rates] end + # rubocop:enable Layout/LineLength def to_csv CSV.generate do |csv| @@ -136,6 +142,22 @@ def merge(other) ) end + def blanket_proofing_rates + successfully_verified_users.to_f / idv_started + end + + def intent_proofing_rates + successfully_verified_users.to_f / idv_doc_auth_welcome_submitted + end + + def actual_proofing_rates + successfully_verified_users.to_f / idv_doc_auth_image_vendor_submitted + end + + def industry_proofing_rates + successfully_verified_users.to_f / (successfully_verified_users + idv_doc_auth_rejected) + end + def idv_final_resolution data[Events::IDV_FINAL_RESOLUTION].count end diff --git a/lib/reporting/proofing_rate_report.rb b/lib/reporting/proofing_rate_report.rb index b249cd62aa6..634cf11eb68 100644 --- a/lib/reporting/proofing_rate_report.rb +++ b/lib/reporting/proofing_rate_report.rb @@ -62,10 +62,10 @@ def as_csv csv << ['IDV Rejected (Non-Fraud)', *reports.map(&:idv_doc_auth_rejected)] csv << ['IDV Rejected (Fraud)', *reports.map(&:idv_fraud_rejected)] - csv << ['Blanket Proofing Rate (IDV Started to Successfully Verified)', *blanket_proofing_rates(reports)] - csv << ['Intent Proofing Rate (Welcome Submitted to Successfully Verified)', *intent_proofing_rates(reports)] - csv << ['Actual Proofing Rate (Image Submitted to Successfully Verified)', *actual_proofing_rates(reports)] - csv << ['Industry Proofing Rate (Verified minus IDV Rejected)', *industry_proofing_rates(reports)] + csv << ['Blanket Proofing Rate (IDV Started to Successfully Verified)', *reports.map(&:blanket_proofing_rates)] + csv << ['Intent Proofing Rate (Welcome Submitted to Successfully Verified)', *reports.map(&:intent_proofing_rates)] + csv << ['Actual Proofing Rate (Image Submitted to Successfully Verified)', *reports.map(&:actual_proofing_rates)] + csv << ['Industry Proofing Rate (Verified minus IDV Rejected)', *reports.map(&:industry_proofing_rates)] csv rescue Aws::CloudWatchLogs::Errors::ThrottlingException => err @@ -125,40 +125,6 @@ def reports end end - # @param [Array] reports - # @return [Array] - def blanket_proofing_rates(reports) - reports.map do |report| - report.successfully_verified_users.to_f / report.idv_started - end - end - - # @param [Array] reports - # @return [Array] - def intent_proofing_rates(reports) - reports.map do |report| - report.successfully_verified_users.to_f / report.idv_doc_auth_welcome_submitted - end - end - - # @param [Array] reports - # @return [Array] - def actual_proofing_rates(reports) - reports.map do |report| - report.successfully_verified_users.to_f / report.idv_doc_auth_image_vendor_submitted - end - end - - # @param [Array] reports - # @return [Array] - def industry_proofing_rates(reports) - reports.map do |report| - report.successfully_verified_users.to_f / ( - report.successfully_verified_users + report.idv_doc_auth_rejected - ) - end - end - def cloudwatch_client @cloudwatch_client ||= Reporting::CloudwatchClient.new( ensure_complete_logs: true, diff --git a/lib/telephony.rb b/lib/telephony.rb index 5ae161b5a1e..ed0b9cdff7a 100644 --- a/lib/telephony.rb +++ b/lib/telephony.rb @@ -60,13 +60,14 @@ def self.send_authentication_otp(to:, otp:, expiration:, otp_format:, ).send_authentication_otp end - def self.send_confirmation_otp(to:, otp:, expiration:, otp_format:, + def self.send_confirmation_otp(to:, otp:, expiration:, otp_format:, otp_length:, channel:, domain:, country_code:, extra_metadata:) OtpSender.new( to: to, otp: otp, expiration: expiration, otp_format: otp_format, + otp_length: otp_length, channel: channel, domain: domain, country_code: country_code, diff --git a/lib/telephony/otp_sender.rb b/lib/telephony/otp_sender.rb index cb12ee43882..7453e2be81a 100644 --- a/lib/telephony/otp_sender.rb +++ b/lib/telephony/otp_sender.rb @@ -2,14 +2,16 @@ module Telephony class OtpSender - attr_reader :recipient_phone, :otp, :expiration, :otp_format, :channel, :domain, :country_code, - :extra_metadata + attr_reader :recipient_phone, :otp, :expiration, :otp_format, :otp_length, :channel, + :domain, :country_code, :extra_metadata def initialize(to:, otp:, expiration:, otp_format:, - channel:, domain:, country_code:, extra_metadata:) + channel:, domain:, country_code:, extra_metadata:, + otp_length: I18n.t('telephony.format_length.six')) @recipient_phone = to @otp = otp @otp_format = otp_format + @otp_length = otp_length @expiration = expiration @channel = channel.to_sym @domain = domain @@ -58,6 +60,7 @@ def confirmation_message "telephony.confirmation_otp.#{channel}", app_name: APP_NAME, code: otp_transformed_for_channel, + format_length: otp_length, format_type: otp_format, expiration: expiration, domain: domain, diff --git a/package.json b/package.json index 481dec58304..643fd7f2833 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "build:css": "build-sass app/assets/stylesheets/*.css.scss app/components/*.scss --load-path=app/assets/stylesheets --out-dir=app/assets/builds" }, "dependencies": { - "@18f/identity-design-system": "^9.1.0", + "@18f/identity-design-system": "^9.2.0", "@babel/core": "^7.20.7", "@babel/preset-env": "^7.15.6", "@babel/preset-react": "^7.14.5", diff --git a/spec/components/badge_component_spec.rb b/spec/components/badge_component_spec.rb index b7a648fbbb2..356213a6a69 100644 --- a/spec/components/badge_component_spec.rb +++ b/spec/components/badge_component_spec.rb @@ -26,12 +26,12 @@ end context 'with valid icon' do - let(:icon) { :success } + let(:icon) { :check_circle } it 'renders badge with icon and content' do - expect(rendered).to have_css('.lg-verification-badge') - expect(rendered).to have_css('img[src^="/assets/alerts/success-"]') - expect(rendered).to have_content(content) + expect(rendered).to have_css('.lg-verification-badge .usa-icon.text-success') + inline_icon_style = rendered.at_css('.usa-icon style').text.strip + expect(inline_icon_style).to match(%r{url\([^)]+?/check_circle-\w+\.svg\)}) end context 'with extra tag options' do diff --git a/spec/components/previews/badge_component_preview.rb b/spec/components/previews/badge_component_preview.rb index 5da8047a045..3df85d751f7 100644 --- a/spec/components/previews/badge_component_preview.rb +++ b/spec/components/previews/badge_component_preview.rb @@ -1,13 +1,13 @@ class BadgeComponentPreview < BaseComponentPreview # @!group Preview def default - render(BadgeComponent.new(icon: :success).with_content('Verified Account')) + render(BadgeComponent.new(icon: :check_circle).with_content('Verified Account')) end # @!endgroup - # @param icon select [success,unphishable] + # @param icon select [check_circle,lock] # @param content text - def workbench(icon: :success, content: 'Verified Account') + def workbench(icon: :check_circle, content: 'Verified Account') render(BadgeComponent.new(icon: icon&.to_sym).with_content(content)) end end diff --git a/spec/controllers/completions_cancellation_controller_spec.rb b/spec/controllers/completions_cancellation_controller_spec.rb new file mode 100644 index 00000000000..75f32d9d4fa --- /dev/null +++ b/spec/controllers/completions_cancellation_controller_spec.rb @@ -0,0 +1,19 @@ +require 'rails_helper' + +RSpec.describe CompletionsCancellationController do + let(:user) { build_stubbed(:user, :fully_registered) } + + before do + stub_sign_in(user) + end + + it 'renders the show template' do + stub_analytics + + get :show + + expect(@analytics).to have_logged_event(:completions_cancellation_visited) + + expect(response).to render_template :show + end +end diff --git a/spec/controllers/concerns/idv/document_capture_concern_spec.rb b/spec/controllers/concerns/idv/document_capture_concern_spec.rb index 5b2375acfc8..3bd448c797e 100644 --- a/spec/controllers/concerns/idv/document_capture_concern_spec.rb +++ b/spec/controllers/concerns/idv/document_capture_concern_spec.rb @@ -19,20 +19,22 @@ def show context 'selfie checks enabled' do before do - decorated_sp_session = instance_double(ServiceProviderSession) - allow(decorated_sp_session).to receive(:biometric_comparison_required?). - and_return(biometric_comparison_required) - allow(controller).to receive(:decorated_sp_session).and_return(decorated_sp_session) + allow(FeatureManagement).to receive(:idv_allow_selfie_check?).and_return(true) + stored_result = instance_double(DocumentCaptureSessionResult) allow(stored_result).to receive(:selfie_check_performed?).and_return(selfie_check_performed) allow(controller).to receive(:stored_result).and_return(stored_result) + + resolution_result = Vot::Parser.new(vector_of_trust: vot).parse + allow(controller).to receive(:resolved_authn_context_result).and_return(resolution_result) end context 'SP requires biometric_comparison' do - let(:biometric_comparison_required) { true } + let(:vot) { 'Pb' } context 'selfie check performed' do let(:selfie_check_performed) { true } + it 'returns true' do expect(controller.selfie_requirement_met?).to eq(true) end @@ -40,6 +42,7 @@ def show context 'selfie check not performed' do let(:selfie_check_performed) { false } + it 'returns false' do expect(controller.selfie_requirement_met?).to eq(false) end @@ -47,10 +50,11 @@ def show end context 'SP does not require biometric_comparison' do - let(:biometric_comparison_required) { false } + let(:vot) { 'P1' } context 'selfie check performed' do let(:selfie_check_performed) { true } + it 'returns true' do expect(controller.selfie_requirement_met?).to eq(true) end diff --git a/spec/controllers/concerns/idv/step_indicator_concern_spec.rb b/spec/controllers/concerns/idv/step_indicator_concern_spec.rb index 998b74c649a..1b782827d32 100644 --- a/spec/controllers/concerns/idv/step_indicator_concern_spec.rb +++ b/spec/controllers/concerns/idv/step_indicator_concern_spec.rb @@ -26,8 +26,8 @@ def force_gpo { name: :getting_started }, { name: :verify_id }, { name: :verify_info }, - { name: :verify_phone_or_address }, - { name: :secure_account }, + { name: :verify_phone }, + { name: :re_enter_password }, ] end @@ -36,7 +36,7 @@ def force_gpo { name: :getting_started }, { name: :verify_id }, { name: :verify_info }, - { name: :get_a_letter }, + { name: :verify_address }, { name: :secure_account }, ] end @@ -69,8 +69,8 @@ def force_gpo [ { name: :find_a_post_office }, { name: :verify_info }, - { name: :verify_phone_or_address }, - { name: :secure_account }, + { name: :verify_phone }, + { name: :re_enter_password }, { name: :go_to_the_post_office }, ] end @@ -79,8 +79,8 @@ def force_gpo [ { name: :find_a_post_office }, { name: :verify_info }, + { name: :verify_address }, { name: :secure_account }, - { name: :get_a_letter }, { name: :go_to_the_post_office }, ] end diff --git a/spec/controllers/idv/by_mail/request_letter_controller_spec.rb b/spec/controllers/idv/by_mail/request_letter_controller_spec.rb index 1ee2a617877..a4bed8bbea9 100644 --- a/spec/controllers/idv/by_mail/request_letter_controller_spec.rb +++ b/spec/controllers/idv/by_mail/request_letter_controller_spec.rb @@ -76,12 +76,6 @@ expect(response).to be_ok end - it 'assigns the current step indicator step as "verify phone or address"' do - get :index - - expect(assigns(:step_indicator_current_step)).to eq(:verify_phone_or_address) - end - context 'with letter already sent' do before do allow_any_instance_of(Idv::ByMail::RequestLetterPresenter). @@ -98,18 +92,6 @@ end end - context 'resending a letter' do - before do - allow(controller).to receive(:resend_requested?).and_return(true) - end - - it 'assigns the current step indicator step as "get a letter"' do - get :index - - expect(assigns(:step_indicator_current_step)).to eq(:get_a_letter) - end - end - context 'user has a pending profile' do let(:profile_created_at) { Time.zone.now } let(:pending_profile) do @@ -274,7 +256,7 @@ def expect_resend_letter_to_send_letter_and_redirect(otp:) allow(Pii::Cacher).to receive(:new).and_return(pii_cacher) service_provider = create(:service_provider, issuer: '123abc') - session[:sp] = { issuer: service_provider.issuer } + session[:sp] = { issuer: service_provider.issuer, vtr: ['C1'] } gpo_confirmation_maker = instance_double(GpoConfirmationMaker) allow(GpoConfirmationMaker).to receive(:new). diff --git a/spec/controllers/idv/document_capture_controller_spec.rb b/spec/controllers/idv/document_capture_controller_spec.rb index 984b89f40dc..6d04ae0305c 100644 --- a/spec/controllers/idv/document_capture_controller_spec.rb +++ b/spec/controllers/idv/document_capture_controller_spec.rb @@ -30,8 +30,15 @@ stub_up_to(:hybrid_handoff, idv_session: subject.idv_session) stub_analytics subject.idv_session.document_capture_session_uuid = document_capture_session_uuid - allow(controller.decorated_sp_session).to receive(:biometric_comparison_required?). - and_return(doc_auth_selfie_capture_enabled && sp_selfie_enabled) + + vot = (doc_auth_selfie_capture_enabled && sp_selfie_enabled) ? 'Pb' : 'P1' + resolved_authn_context = Vot::Parser.new(vector_of_trust: vot).parse + + allow(FeatureManagement).to receive(:idv_allow_selfie_check?). + and_return(doc_auth_selfie_capture_enabled) + + allow(controller).to receive(:resolved_authn_context_result). + and_return(resolved_authn_context) subject.idv_session.flow_path = flow_path allow(subject).to receive(:ab_test_analytics_buckets).and_return(ab_test_args) end @@ -40,10 +47,13 @@ it 'returns a valid StepInfo object' do expect(Idv::DocumentCaptureController.step_info).to be_valid end + context 'when selfie feature is enabled system wide' do let(:doc_auth_selfie_capture_enabled) { true } + describe 'with sp selfie disabled' do let(:sp_selfie_enabled) { false } + it 'does not satisfy precondition' do expect(Idv::DocumentCaptureController.step_info.preconditions.is_a?(Proc)) expect(subject).to receive(:render). @@ -52,16 +62,21 @@ expect(response).to render_template :show end end + describe 'with sp selfie enabled' do let(:sp_selfie_enabled) { true } + before do allow(IdentityConfig.store).to receive(:doc_auth_selfie_desktop_test_mode). and_return(false) end + it 'does satisfy precondition' do expect(Idv::DocumentCaptureController.step_info.preconditions.is_a?(Proc)) expect(subject).not_to receive(:render).with(:show, locals: an_instance_of(Hash)) + get :show + expect(response).to redirect_to(idv_hybrid_handoff_path) end end @@ -255,15 +270,19 @@ end context 'ipp disabled for sp' do + let(:sp_selfie_enabled) { true } + let(:doc_auth_selfie_capture_enabled) { true } + before do allow(IdentityConfig.store).to receive(:doc_auth_selfie_desktop_test_mode).and_return(false) allow(Idv::InPersonConfig).to receive(:enabled_for_issuer?).with(anything).and_return(false) - allow(subject.decorated_sp_session).to receive(:biometric_comparison_required?). - and_return(true) end + it 'redirect back when accessed from handoff' do subject.idv_session.skip_hybrid_handoff = nil + get :show, params: { step: 'hybrid_handoff' } + expect(response).to redirect_to(idv_hybrid_handoff_url) expect(subject.idv_session.skip_doc_auth_from_handoff).to_not eq(true) end diff --git a/spec/controllers/idv/enter_password_controller_spec.rb b/spec/controllers/idv/enter_password_controller_spec.rb index ac97bc9d329..4b5058ced49 100644 --- a/spec/controllers/idv/enter_password_controller_spec.rb +++ b/spec/controllers/idv/enter_password_controller_spec.rb @@ -139,7 +139,7 @@ def show it 'uses the correct step indicator step' do indicator_step = subject.step_indicator_step - expect(indicator_step).to eq(:secure_account) + expect(indicator_step).to eq(:re_enter_password) end context 'user is in gpo flow' do @@ -170,7 +170,7 @@ def show it 'uses the correct step indicator step' do indicator_step = subject.step_indicator_step - expect(indicator_step).to eq(:get_a_letter) + expect(indicator_step).to eq(:verify_address) end end diff --git a/spec/controllers/idv/hybrid_handoff_controller_spec.rb b/spec/controllers/idv/hybrid_handoff_controller_spec.rb index 35c86c9fa4c..e16b188831b 100644 --- a/spec/controllers/idv/hybrid_handoff_controller_spec.rb +++ b/spec/controllers/idv/hybrid_handoff_controller_spec.rb @@ -25,8 +25,14 @@ stub_attempts_tracker allow(subject).to receive(:ab_test_analytics_buckets).and_return(ab_test_args) allow(subject.idv_session).to receive(:service_provider).and_return(service_provider) - allow(subject.decorated_sp_session).to receive(:biometric_comparison_required?). - and_return(sp_selfie_enabled && doc_auth_selfie_capture_enabled) + + resolved_authn_context_result = sp_selfie_enabled && doc_auth_selfie_capture_enabled ? + Vot::Parser.new(vector_of_trust: 'Pb').parse : + Vot::Parser.new(vector_of_trust: 'P1').parse + + allow(subject).to receive(:resolved_authn_context_result). + and_return(resolved_authn_context_result) + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled) { in_person_proofing } allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled) { ipp_opt_in_enabled @@ -261,8 +267,10 @@ context 'with selfie enabled system wide' do let(:doc_auth_selfie_capture_enabled) { true } + describe 'when selfie is enabled for sp' do let(:sp_selfie_enabled) { true } + it 'pass on correct flags and states and logs correct info' do get :show expect(response).to render_template :show @@ -270,8 +278,10 @@ expect(subject.idv_session.selfie_check_required).to eq(true) end end + describe 'when selfie is disabled for sp' do let(:sp_selfie_enabled) { false } + it 'pass on correct flags and states and logs correct info' do get :show expect(response).to render_template :show diff --git a/spec/controllers/idv/hybrid_mobile/document_capture_controller_spec.rb b/spec/controllers/idv/hybrid_mobile/document_capture_controller_spec.rb index 4a260d4ff14..035cf5527c7 100644 --- a/spec/controllers/idv/hybrid_mobile/document_capture_controller_spec.rb +++ b/spec/controllers/idv/hybrid_mobile/document_capture_controller_spec.rb @@ -58,8 +58,8 @@ flow_path: 'hybrid', irs_reproofing: false, step: 'document_capture', - liveness_checking_required: false, - selfie_check_required: boolean, + selfie_check_required: false, + liveness_checking_required: boolean, }.merge(ab_test_args) end @@ -76,30 +76,26 @@ expect(response).to render_template :show end - context 'when a selfie is requested' do + context 'when selfie is required' do before do - allow(subject).to receive(:decorated_sp_session). - and_return( - double( - 'decorated_session', - { biometric_comparison_required?: true, sp_name: 'sp' }, - ), - ) + allow(FeatureManagement).to receive(:idv_allow_selfie_check?).and_return(true) + + authn_context_result = Vot::Parser.new(vector_of_trust: 'Pb').parse + allow(subject).to receive(:resolved_authn_context_result).and_return(authn_context_result) end - context 'when selfie is required by sp session' do - it 'requests FE to display selfie' do - expect(subject).to receive(:render).with( - :show, - locals: hash_including( - document_capture_session_uuid: document_capture_session_uuid, - doc_auth_selfie_capture: true, - ), - ).and_call_original - get :show + it 'requests FE to display selfie' do + expect(subject).to receive(:render).with( + :show, + locals: hash_including( + document_capture_session_uuid: document_capture_session_uuid, + doc_auth_selfie_capture: true, + ), + ).and_call_original - expect(response).to render_template :show - end + get :show + + expect(response).to render_template :show end end diff --git a/spec/controllers/idv/image_uploads_controller_spec.rb b/spec/controllers/idv/image_uploads_controller_spec.rb index ec09b8904af..aa036df6249 100644 --- a/spec/controllers/idv/image_uploads_controller_spec.rb +++ b/spec/controllers/idv/image_uploads_controller_spec.rb @@ -30,10 +30,7 @@ end let(:json) { JSON.parse(response.body, symbolize_names: true) } - let(:store_encrypted_images) { false } - before do - allow(controller).to receive(:store_encrypted_images?).and_return(store_encrypted_images) Funnel::DocAuth::RegisterStep.new(user.id, '').call('welcome', :view, true) allow(IdentityConfig.store).to receive(:idv_acuant_sdk_upgrade_a_b_testing_enabled). and_return(false) @@ -336,8 +333,10 @@ let(:selfie_img) { DocAuthImageFixtures.selfie_image_multipart } before do - allow(controller.decorated_sp_session).to receive(:biometric_comparison_required?). - and_return(true) + resolved_authn_context_result = Vot::Parser.new(vector_of_trust: 'Pb').parse + + allow(controller).to receive(:resolved_authn_context_result). + and_return(resolved_authn_context_result) end it 'returns a successful response and modifies the session' do @@ -484,26 +483,6 @@ expect_funnel_update_counts(user, 1) end - context 'encrypted document storage is enabled' do - let(:store_encrypted_images) { true } - - it 'includes image fields in attempts api event' do - stub_attempts_tracker - - expect(@irs_attempts_api_tracker).to receive(:track_event).with( - :idv_document_upload_submitted, - hash_including( - success: true, - document_back_image_filename: match(document_filename_regex), - document_front_image_filename: match(document_filename_regex), - document_image_encryption_key: match(base64_regex), - ), - ) - - action - end - end - context 'but doc_pii validation fails' do let(:first_name) { 'FAKEY' } let(:last_name) { 'MCFAKERSON' } @@ -558,7 +537,6 @@ end context 'encrypted document storage is enabled' do - let(:store_encrypted_images) { true } let(:first_name) { nil } it 'includes image references in attempts api' do @@ -575,9 +553,9 @@ last_name: 'MCFAKERSON', date_of_birth: '10/06/1938', address: address1, - document_back_image_filename: match(document_filename_regex), - document_front_image_filename: match(document_filename_regex), - document_image_encryption_key: match(base64_regex), + document_back_image_filename: nil, + document_front_image_filename: nil, + document_image_encryption_key: nil, ) action @@ -1240,8 +1218,10 @@ context 'the frontend requests a selfie' do before do - allow(controller).to receive(:decorated_sp_session). - and_return(double('decorated_session', { biometric_comparison_required?: true })) + authn_context_result = Vot::Parser.new(vector_of_trust: 'Pb').parse + allow(controller).to( + receive(:resolved_authn_context_result).and_return(authn_context_result), + ) end let(:back_image) { DocAuthImageFixtures.portrait_match_success_yaml } diff --git a/spec/controllers/idv/in_person/state_id_controller_spec.rb b/spec/controllers/idv/in_person/state_id_controller_spec.rb index 5bd6d2babd3..b952d38c9e2 100644 --- a/spec/controllers/idv/in_person/state_id_controller_spec.rb +++ b/spec/controllers/idv/in_person/state_id_controller_spec.rb @@ -112,4 +112,257 @@ ) end end + + describe '#update' do + let(:first_name) { 'Natalya' } + let(:last_name) { 'Rostova' } + let(:dob) { InPersonHelper::GOOD_DOB } + # residential + let(:address1) { InPersonHelper::GOOD_ADDRESS1 } + let(:address2) { InPersonHelper::GOOD_ADDRESS2 } + let(:city) { InPersonHelper::GOOD_CITY } + let(:state) { InPersonHelper::GOOD_STATE } + let(:zipcode) { InPersonHelper::GOOD_ZIPCODE } + # identity_doc_ + let(:state_id_number) { 'ABC123234' } + let(:state_id_jurisdiction) { 'AL' } + let(:identity_doc_address1) { InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1 } + let(:identity_doc_address2) { InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2 } + let(:identity_doc_city) { InPersonHelper::GOOD_IDENTITY_DOC_CITY } + let(:identity_doc_address_state) { InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS_STATE } + let(:identity_doc_zipcode) { InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE } + context 'with values submitted' do + let(:params) do + { state_id: { + first_name:, + last_name:, + same_address_as_id: 'true', # value on submission + identity_doc_address1:, + identity_doc_address2:, + identity_doc_city:, + state_id_jurisdiction:, + state_id_number:, + identity_doc_address_state:, + identity_doc_zipcode:, + dob:, + } } + end + let(:analytics_name) { 'IdV: in person proofing state_id submitted' } + let(:analytics_args) do + { + success: true, + errors: {}, + analytics_id: 'In Person Proofing', + flow_path: 'standard', + irs_reproofing: false, + step: 'state_id', + opted_in_to_in_person_proofing: nil, + pii_like_keypaths: [[:same_address_as_id], + [:proofing_results, :context, :stages, :state_id, + :state_id_jurisdiction]], + same_address_as_id: true, + }.merge(ab_test_args) + end + + it 'logs idv_in_person_proofing_state_id_visited' do + put :update, params: params + + expect(@analytics).to have_received( + :track_event, + ).with(analytics_name, analytics_args) + end + + it 'invalidates future steps, but does not clear ssn' do + subject.idv_session.ssn = '123-45-6789' + expect(subject).to receive(:clear_future_steps_from!).and_call_original + + expect { put :update, params: params }.not_to change { subject.idv_session.ssn } + end + + it 'sets values in flow session' do + Idv::StateIdForm::ATTRIBUTES.each do |attr| + expect(subject.user_session['idv/in_person'][:pii_from_user]).to_not have_key attr + end + + put :update, params: params + + pii_from_user = subject.user_session['idv/in_person'][:pii_from_user] + expect(pii_from_user[:first_name]).to eq first_name + expect(pii_from_user[:last_name]).to eq last_name + expect(pii_from_user[:dob]).to eq dob + expect(pii_from_user[:identity_doc_address_state]).to eq identity_doc_address_state + expect(pii_from_user[:state_id_number]).to eq state_id_number + end + + context 'receives hash dob' do + let(:dob) do + { + day: '3', + month: '9', + year: '1988', + } + end + + it 'converts the date when setting it in flow session' do + expect(subject.user_session['idv/in_person'][:pii_from_user]).to_not have_key :dob + + put :update, params: params + + expect(subject.user_session['idv/in_person'][:pii_from_user][:dob]).to eq '1988-09-03' + end + end + end + + context 'when same_address_as_id is...' do + let(:pii_from_user) { subject.user_session['idv/in_person'][:pii_from_user] } + + context 'changed from "true" to "false"' do + let(:params) do + { + state_id: { + first_name:, + last_name:, + same_address_as_id: 'false', # value on submission + identity_doc_address1:, + identity_doc_address2:, + identity_doc_city:, + state_id_jurisdiction:, + state_id_number:, + identity_doc_address_state:, + identity_doc_zipcode:, + dob:, + }, + } + end + + it 'retains identity_doc_ attrs/value but removes addr attr in flow session' do + Idv::StateIdForm::ATTRIBUTES.each do |attr| + expect(subject.user_session['idv/in_person'][:pii_from_user]).to_not have_key attr + end + + make_pii + + # pii includes address attrs on re-visiting state id pg + expect(subject.user_session['idv/in_person'][:pii_from_user]).to include( + address1:, + address2:, + city:, + state:, + zipcode:, + ) + + # On Verify, user changes response from "Yes,..." to + # "No, I live at a different address", see submitted_values above + put :update, params: params + + # retains identity_doc_ attributes and values in flow session + expect(subject.user_session['idv/in_person'][:pii_from_user]).to include( + identity_doc_address1:, + identity_doc_address2:, + identity_doc_city:, + identity_doc_address_state:, + identity_doc_zipcode:, + ) + + # removes address attributes (non identity_doc_ attributes) in flow session + expect(subject.user_session['idv/in_person'][:pii_from_user]).not_to include( + address1:, + address2:, + city:, + state:, + zipcode:, + ) + end + end + + context 'changed from "false" to "true"' do + let(:params) do + { state_id: { + first_name:, + last_name:, + same_address_as_id: 'true', # value on submission + identity_doc_address1:, + identity_doc_address2:, + identity_doc_city:, + state_id_jurisdiction:, + state_id_number:, + identity_doc_address_state:, + identity_doc_zipcode:, + dob:, + } } + end + + it 'retains identity_doc_ attrs/value ands addr attr + with same value as identity_doc in flow session' do + Idv::StateIdForm::ATTRIBUTES.each do |attr| + expect(subject.user_session['idv/in_person'][:pii_from_user]).to_not have_key attr + end + + make_pii(same_address_as_id: 'false') + + # On Verify, user changes response from "No,..." to + # "Yes, I live at the address on my state-issued ID + put :update, params: params + # expect addr attr values to the same as the identity_doc attr values + expect(pii_from_user[:address1]).to eq identity_doc_address1 + expect(pii_from_user[:address2]).to eq identity_doc_address2 + expect(pii_from_user[:city]).to eq identity_doc_city + expect(pii_from_user[:state]).to eq identity_doc_address_state + expect(pii_from_user[:zipcode]).to eq identity_doc_zipcode + end + end + + context 'not changed from "false"' do + let(:params) do + { state_id: { + dob:, + same_address_as_id: 'false', + address1:, + address2:, + city:, + state:, + zipcode:, + identity_doc_address1:, + identity_doc_address2:, + identity_doc_city:, + identity_doc_address_state:, + identity_doc_zipcode:, + } } + end + it 'retains identity_doc_ and addr attrs/value in flow session' do + Idv::StateIdForm::ATTRIBUTES.each do |attr| + expect(subject.user_session['idv/in_person'][:pii_from_user]).to_not have_key attr + end + + # User picks "No, I live at a different address" on state ID + make_pii(same_address_as_id: 'false') + + # On Verify, user does not changes response "No,..." + put :update, params: params + + # retains identity_doc_ & addr attributes and values in flow session + expect(subject.user_session['idv/in_person'][:pii_from_user]).to include( + identity_doc_address1:, + identity_doc_address2:, + identity_doc_city:, + identity_doc_address_state:, + identity_doc_zipcode:, + address1:, + address2:, + city:, + state:, + zipcode:, + ) + + # those values are different + pii_from_user = subject.user_session['idv/in_person'][:pii_from_user] + expect(pii_from_user[:address1]).to_not eq identity_doc_address1 + expect(pii_from_user[:address2]).to_not eq identity_doc_address2 + expect(pii_from_user[:city]).to_not eq identity_doc_city + expect(pii_from_user[:state]).to_not eq identity_doc_address_state + expect(pii_from_user[:zipcode]).to_not eq identity_doc_zipcode + end + end + end + end end diff --git a/spec/controllers/idv/otp_verification_controller_spec.rb b/spec/controllers/idv/otp_verification_controller_spec.rb index 9f11d4505bb..b4834386c63 100644 --- a/spec/controllers/idv/otp_verification_controller_spec.rb +++ b/spec/controllers/idv/otp_verification_controller_spec.rb @@ -9,16 +9,13 @@ let(:user_phone_confirmation) { false } let(:phone_confirmation_otp_code) { '777777' } let(:phone_confirmation_otp_sent_at) { Time.zone.now } - let(:phone_confirmation_session_properties) do - { - code: phone_confirmation_otp_code, - phone: phone, - delivery_method: :sms, - } - end + let(:delivery_method) { :sms } let(:user_phone_confirmation_session) do Idv::PhoneConfirmationSession.new( - **phone_confirmation_session_properties, + code: phone_confirmation_otp_code, + phone: phone, + delivery_method: delivery_method, + user: user, sent_at: phone_confirmation_otp_sent_at, ) end @@ -140,13 +137,7 @@ end context 'the user uses voice otp' do - let(:phone_confirmation_session_properties) do - { - code: phone_confirmation_otp_code, - phone: phone, - delivery_method: :voice, - } - end + let(:delivery_method) { :voice } it 'does not save the phone number if the feature flag is off' do put :update, params: otp_code_param @@ -213,15 +204,9 @@ end context 'when the phone otp code has expired' do - let(:expired_phone_confirmation_otp_sent_at) do + let(:phone_confirmation_otp_sent_at) do # Set time to a long time ago - phone_confirmation_otp_sent_at - 900000000 - end - let(:user_phone_confirmation_session) do - Idv::PhoneConfirmationSession.new( - **phone_confirmation_session_properties, - sent_at: expired_phone_confirmation_otp_sent_at, - ) + Time.zone.now - 900000000 end it 'captures failure event' do diff --git a/spec/controllers/idv/personal_key_controller_spec.rb b/spec/controllers/idv/personal_key_controller_spec.rb index 1257197223f..492b12376fb 100644 --- a/spec/controllers/idv/personal_key_controller_spec.rb +++ b/spec/controllers/idv/personal_key_controller_spec.rb @@ -420,7 +420,10 @@ def assert_personal_key_generated_for_profiles(*profile_pii_pairs) describe '#update' do context 'user selected phone verification' do it 'redirects to sign up completed for an sp' do - subject.session[:sp] = { issuer: create(:service_provider).issuer } + subject.session[:sp] = { + issuer: create(:service_provider).issuer, + vtr: ['C1'], + } patch :update expect(response).to redirect_to sign_up_completed_url diff --git a/spec/controllers/idv/resend_otp_controller_spec.rb b/spec/controllers/idv/resend_otp_controller_spec.rb index 51b922161ee..fd937efe100 100644 --- a/spec/controllers/idv/resend_otp_controller_spec.rb +++ b/spec/controllers/idv/resend_otp_controller_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' RSpec.describe Idv::ResendOtpController do - let(:user) { build(:user) } + let(:user) { create(:user) } let(:phone) { '+1 (225) 555-5000' } let(:user_phone_confirmation) { false } @@ -10,6 +10,7 @@ Idv::PhoneConfirmationSession.start( phone: phone, delivery_method: delivery_method, + user: user, ) end diff --git a/spec/controllers/idv/verify_info_controller_spec.rb b/spec/controllers/idv/verify_info_controller_spec.rb index c39ed2b71e6..ec3c449fe43 100644 --- a/spec/controllers/idv/verify_info_controller_spec.rb +++ b/spec/controllers/idv/verify_info_controller_spec.rb @@ -414,7 +414,7 @@ it 'modifies pii as expected' do app_id = 'hello-world' sp = create(:service_provider, app_id: app_id) - sp_session = { issuer: sp.issuer } + sp_session = { issuer: sp.issuer, vtr: ['C1'] } allow(controller).to receive(:sp_session).and_return(sp_session) expect(Idv::Agent).to receive(:new).with( diff --git a/spec/controllers/openid_connect/authorization_controller_spec.rb b/spec/controllers/openid_connect/authorization_controller_spec.rb index 55fba516256..9f27bd1abe1 100644 --- a/spec/controllers/openid_connect/authorization_controller_spec.rb +++ b/spec/controllers/openid_connect/authorization_controller_spec.rb @@ -13,7 +13,7 @@ let(:client_id) { 'urn:gov:gsa:openidconnect:test' } let(:service_provider) { build(:service_provider, issuer: client_id) } let(:prompt) { 'select_account' } - let(:acr_values) { Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF } + let(:acr_values) { nil } let(:vtr) { nil } let(:params) do { @@ -26,7 +26,7 @@ scope: 'openid profile', state: SecureRandom.hex, vtr: vtr, - } + }.compact end describe '#index' do @@ -54,7 +54,10 @@ session[:sign_in_flow] = sign_in_flow end - context 'with valid params' do + context 'acr with valid params' do + let(:acr_values) { Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF } + let(:vtr) { nil } + it 'redirects back to the client app with a code if server-side redirect is enabled' do allow(IdentityConfig.store).to receive(:openid_connect_redirect). and_return('server_side') @@ -105,37 +108,6 @@ context 'with ial1 requested using acr_values' do it 'tracks IAL1 authentication event' do stub_analytics - expect(@analytics).to receive(:track_event). - with('OpenID Connect: authorization request', - success: true, - client_id: client_id, - prompt: 'select_account', - referer: nil, - allow_prompt_login: true, - errors: {}, - unauthorized_scope: true, - user_fully_authenticated: true, - acr_values: 'http://idmanagement.gov/ns/assurance/ial/1', - code_challenge_present: false, - service_provider_pkce: nil, - scope: 'openid', - vtr: nil, - vtr_param: '') - expect(@analytics).to receive(:track_event). - with('OpenID Connect: authorization request handoff', - success: true, - client_id: client_id, - user_sp_authorized: true, - code_digest: kind_of(String)) - expect(@analytics).to receive(:track_event). - with( - 'SP redirect initiated', - ial: 1, - billed_ial: 1, - sign_in_flow:, - acr_values: 'http://idmanagement.gov/ns/assurance/ial/1', - vtr: nil, - ) IdentityLinker.new(user, service_provider).link_identity(ial: 1) user.identities.last.update!(verified_attributes: %w[given_name family_name birthdate]) @@ -144,6 +116,39 @@ sp_return_log = SpReturnLog.find_by(issuer: client_id) expect(sp_return_log.ial).to eq(1) + + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request', + success: true, + client_id: client_id, + prompt: 'select_account', + referer: nil, + allow_prompt_login: true, + errors: {}, + unauthorized_scope: true, + user_fully_authenticated: true, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/1', + code_challenge_present: false, + service_provider_pkce: nil, + scope: 'openid', + vtr: nil, + vtr_param: nil, + ) + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request handoff', + success: true, + client_id: client_id, + user_sp_authorized: true, + code_digest: kind_of(String), + ) + expect(@analytics).to have_logged_event( + 'SP redirect initiated', + ial: 1, + billed_ial: 1, + sign_in_flow:, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/1', + vtr: nil, + ) end end @@ -157,37 +162,6 @@ it 'tracks IAL1 authentication event' do stub_analytics - expect(@analytics).to receive(:track_event). - with('OpenID Connect: authorization request', - success: true, - client_id: client_id, - prompt: 'select_account', - referer: nil, - allow_prompt_login: true, - errors: {}, - unauthorized_scope: true, - user_fully_authenticated: true, - acr_values: '', - code_challenge_present: false, - service_provider_pkce: nil, - scope: 'openid', - vtr: ['C1'], - vtr_param: ['C1'].to_json) - expect(@analytics).to receive(:track_event). - with('OpenID Connect: authorization request handoff', - success: true, - client_id: client_id, - user_sp_authorized: true, - code_digest: kind_of(String)) - expect(@analytics).to receive(:track_event). - with( - 'SP redirect initiated', - ial: 1, - billed_ial: 1, - sign_in_flow:, - acr_values: '', - vtr: ['C1'], - ) IdentityLinker.new(user, service_provider).link_identity(ial: 1) user.identities.last.update!(verified_attributes: %w[given_name family_name birthdate]) @@ -196,11 +170,46 @@ sp_return_log = SpReturnLog.find_by(issuer: client_id) expect(sp_return_log.ial).to eq(1) + + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request', + success: true, + client_id: client_id, + prompt: 'select_account', + referer: nil, + allow_prompt_login: true, + errors: {}, + unauthorized_scope: true, + user_fully_authenticated: true, + acr_values: '', + code_challenge_present: false, + service_provider_pkce: nil, + scope: 'openid', + vtr: ['C1'], + vtr_param: ['C1'].to_json, + ) + + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request handoff', + success: true, + client_id: client_id, + user_sp_authorized: true, + code_digest: kind_of(String), + ) + + expect(@analytics).to have_logged_event( + 'SP redirect initiated', + ial: 1, + billed_ial: 1, + sign_in_flow:, + acr_values: '', + vtr: ['C1'], + ) end end - context 'with ial2 requested' do - before { params[:acr_values] = Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF } + context 'with ial2 requested using acr values' do + let(:acr_values) { Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF } context 'account is already verified' do let(:user) do @@ -346,37 +355,6 @@ it 'tracks IAL2 authentication event' do stub_analytics - expect(@analytics).to receive(:track_event). - with('OpenID Connect: authorization request', - success: true, - client_id: client_id, - prompt: 'select_account', - referer: nil, - allow_prompt_login: true, - errors: {}, - unauthorized_scope: false, - user_fully_authenticated: true, - acr_values: 'http://idmanagement.gov/ns/assurance/ial/2', - code_challenge_present: false, - service_provider_pkce: nil, - scope: 'openid profile', - vtr: nil, - vtr_param: '') - expect(@analytics).to receive(:track_event). - with('OpenID Connect: authorization request handoff', - success: true, - client_id: client_id, - user_sp_authorized: true, - code_digest: kind_of(String)) - expect(@analytics).to receive(:track_event). - with( - 'SP redirect initiated', - ial: 2, - billed_ial: 2, - sign_in_flow:, - acr_values: 'http://idmanagement.gov/ns/assurance/ial/2', - vtr: nil, - ) IdentityLinker.new(user, service_provider).link_identity(ial: 2) user.identities.last.update!( @@ -387,13 +365,104 @@ sp_return_log = SpReturnLog.find_by(issuer: client_id) expect(sp_return_log.ial).to eq(2) + + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request', + success: true, + client_id: client_id, + prompt: 'select_account', + referer: nil, + allow_prompt_login: true, + errors: {}, + unauthorized_scope: false, + user_fully_authenticated: true, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/2', + code_challenge_present: false, + service_provider_pkce: nil, + scope: 'openid profile', + vtr: nil, + vtr_param: nil, + ) + + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request handoff', + success: true, + client_id: client_id, + user_sp_authorized: true, + code_digest: kind_of(String), + ) + + expect(@analytics).to have_logged_event( + 'SP redirect initiated', + ial: 2, + billed_ial: 2, + sign_in_flow:, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/2', + vtr: nil, + ) + end + + context 'SP requests biometric_comparison_required' do + let(:selfie_capture_enabled) { true } + let(:vtr) { ['Pb'].to_json } + + before do + expect(FeatureManagement).to receive(:idv_allow_selfie_check?).at_least(:once). + and_return(selfie_capture_enabled) + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') + IdentityLinker.new(user, service_provider).link_identity(ial: 3) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + allow(controller).to receive(:pii_requested_but_locked?).and_return(false) + end + + context 'selfie check was performed' do + it 'redirects to the redirect_uri immediately when pii is unlocked if client-side redirect is disabled' do + user.active_profile.idv_level = :unsupervised_with_selfie + + action + + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + end + end + + context 'selfie check was not performed' do + it 'redirects to have the user verify their account' do + action + expect(controller).to redirect_to(idv_url) + end + end + + context 'selfie capture not enabled, biometric comparison required' do + let(:selfie_capture_enabled) { false } + let(:vtr) { ['Pb'].to_json } + + it 'returns status not_acceptable' do + action + + expect(response.status).to eq(406) + end + end + + context 'selfie capture not enabled, biometric comparison not required' do + let(:selfie_capture_enabled) { false } + let(:vtr) { ['P1'].to_json } + + it 'redirects to the service provider' do + action + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + end + end end context 'SP has a vector of trust that includes a biometric comparison' do let(:selfie_capture_enabled) { true } + let(:acr_values) { nil } + let(:vtr) { ['Pb'].to_json } + before do - params[:acr_values] = nil - params[:vtr] = ['C1.C2.P1.Pb'].to_json expect(FeatureManagement).to receive(:idv_allow_selfie_check?).at_least(:once). and_return(selfie_capture_enabled) allow(IdentityConfig.store).to receive(:openid_connect_redirect). @@ -486,11 +555,11 @@ context 'sp requests biometrics' do let(:selfie_capture_enabled) { true } let(:user) { create(:profile, :active, :verified).user } + let(:vtr) { ['C1.C2.P1.Pb'].to_json } before do expect(FeatureManagement).to receive(:idv_allow_selfie_check?).at_least(:once). and_return(selfie_capture_enabled) - params[:vtr] = ['C1.C2.P1.Pb'].to_json end it 'redirects to gpo enter code page' do @@ -660,37 +729,6 @@ it 'tracks IAL2 authentication event' do stub_analytics - expect(@analytics).to receive(:track_event). - with('OpenID Connect: authorization request', - success: true, - client_id: client_id, - prompt: 'select_account', - referer: nil, - allow_prompt_login: true, - errors: {}, - unauthorized_scope: false, - user_fully_authenticated: true, - acr_values: 'http://idmanagement.gov/ns/assurance/ial/0', - code_challenge_present: false, - service_provider_pkce: nil, - scope: 'openid profile', - vtr: nil, - vtr_param: '') - expect(@analytics).to receive(:track_event). - with('OpenID Connect: authorization request handoff', - success: true, - client_id: client_id, - user_sp_authorized: true, - code_digest: kind_of(String)) - expect(@analytics).to receive(:track_event). - with( - 'SP redirect initiated', - ial: 0, - billed_ial: 2, - sign_in_flow:, - acr_values: 'http://idmanagement.gov/ns/assurance/ial/0', - vtr: nil, - ) IdentityLinker.new(user, service_provider).link_identity(ial: 2) user.identities.last.update!( @@ -701,6 +739,41 @@ sp_return_log = SpReturnLog.find_by(issuer: client_id) expect(sp_return_log.ial).to eq(2) + + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request', + success: true, + client_id: client_id, + prompt: 'select_account', + referer: nil, + allow_prompt_login: true, + errors: {}, + unauthorized_scope: false, + user_fully_authenticated: true, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/0', + code_challenge_present: false, + service_provider_pkce: nil, + scope: 'openid profile', + vtr: nil, + vtr_param: nil, + ) + + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request handoff', + success: true, + client_id: client_id, + user_sp_authorized: true, + code_digest: kind_of(String), + ) + + expect(@analytics).to have_logged_event( + 'SP redirect initiated', + ial: 0, + billed_ial: 2, + sign_in_flow:, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/0', + vtr: nil, + ) end end @@ -748,36 +821,6 @@ it 'tracks IAL1 authentication event' do stub_analytics - expect(@analytics).to receive(:track_event). - with('OpenID Connect: authorization request', - success: true, - client_id: client_id, - prompt: 'select_account', - referer: nil, - allow_prompt_login: true, - errors: {}, - unauthorized_scope: false, - user_fully_authenticated: true, - acr_values: 'http://idmanagement.gov/ns/assurance/ial/0', - code_challenge_present: false, - service_provider_pkce: nil, - scope: 'openid profile', - vtr: nil, - vtr_param: '') - expect(@analytics).to receive(:track_event). - with('OpenID Connect: authorization request handoff', - success: true, - client_id: client_id, - user_sp_authorized: true, - code_digest: kind_of(String)) - expect(@analytics).to receive(:track_event).with( - 'SP redirect initiated', - ial: 0, - billed_ial: 1, - sign_in_flow:, - acr_values: 'http://idmanagement.gov/ns/assurance/ial/0', - vtr: nil, - ) IdentityLinker.new(user, service_provider).link_identity(ial: 1) user.identities.last.update!( @@ -787,6 +830,41 @@ sp_return_log = SpReturnLog.find_by(issuer: client_id) expect(sp_return_log.ial).to eq(1) + + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request', + success: true, + client_id: client_id, + prompt: 'select_account', + referer: nil, + allow_prompt_login: true, + errors: {}, + unauthorized_scope: false, + user_fully_authenticated: true, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/0', + code_challenge_present: false, + service_provider_pkce: nil, + scope: 'openid profile', + vtr: nil, + vtr_param: nil, + ) + + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request handoff', + success: true, + client_id: client_id, + user_sp_authorized: true, + code_digest: kind_of(String), + ) + + expect(@analytics).to have_logged_event( + 'SP redirect initiated', + ial: 0, + billed_ial: 1, + sign_in_flow:, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/0', + vtr: nil, + ) end end @@ -837,36 +915,6 @@ it 'tracks IAL1 authentication event' do stub_analytics - expect(@analytics).to receive(:track_event). - with('OpenID Connect: authorization request', - success: true, - client_id: client_id, - prompt: 'select_account', - referer: nil, - allow_prompt_login: true, - errors: {}, - unauthorized_scope: false, - user_fully_authenticated: true, - acr_values: 'http://idmanagement.gov/ns/assurance/ial/0', - code_challenge_present: false, - service_provider_pkce: nil, - scope: 'openid profile', - vtr: nil, - vtr_param: '') - expect(@analytics).to receive(:track_event). - with('OpenID Connect: authorization request handoff', - success: true, - client_id: client_id, - user_sp_authorized: true, - code_digest: kind_of(String)) - expect(@analytics).to receive(:track_event).with( - 'SP redirect initiated', - ial: 0, - billed_ial: 1, - sign_in_flow:, - acr_values: 'http://idmanagement.gov/ns/assurance/ial/0', - vtr: nil, - ) IdentityLinker.new(user, service_provider).link_identity(ial: 1) user.identities.last.update!( @@ -876,6 +924,41 @@ sp_return_log = SpReturnLog.find_by(issuer: client_id) expect(sp_return_log.ial).to eq(1) + + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request', + success: true, + client_id: client_id, + prompt: 'select_account', + referer: nil, + allow_prompt_login: true, + errors: {}, + unauthorized_scope: false, + user_fully_authenticated: true, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/0', + code_challenge_present: false, + service_provider_pkce: nil, + scope: 'openid profile', + vtr: nil, + vtr_param: nil, + ) + + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request handoff', + success: true, + client_id: client_id, + user_sp_authorized: true, + code_digest: kind_of(String), + ) + + expect(@analytics).to have_logged_event( + 'SP redirect initiated', + ial: 0, + billed_ial: 1, + sign_in_flow:, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/0', + vtr: nil, + ) end end end @@ -943,26 +1026,29 @@ end end - context 'with invalid params that do not interfere with the redirect_uri' do - before { params[:prompt] = '' } + context 'vtr with valid params' do + let(:vtr) { ['C1'].to_json } - it 'redirects the user with an invalid request if client-side redirect is disabled' do + it 'redirects back to the client app with a code if server-side redirect is enabled' do allow(IdentityConfig.store).to receive(:openid_connect_redirect). and_return('server_side') + IdentityLinker.new(user, service_provider).link_identity(ial: 1) + user.identities.last.update!(verified_attributes: %w[given_name family_name birthdate]) action expect(response).to redirect_to(/^#{params[:redirect_uri]}/) redirect_params = UriService.params(response.location) - expect(redirect_params[:error]).to eq('invalid_request') - expect(redirect_params[:error_description]).to be_present + expect(redirect_params[:code]).to be_present expect(redirect_params[:state]).to eq(params[:state]) end - it 'renders client-side redirect with an invalid request if client-side redirect is enabled' do + it 'renders a client-side redirect back to the client app with a code if it is enabled' do allow(IdentityConfig.store).to receive(:openid_connect_redirect). and_return('client_side') + IdentityLinker.new(user, service_provider).link_identity(ial: 1) + user.identities.last.update!(verified_attributes: %w[given_name family_name birthdate]) action expect(controller).to render_template('openid_connect/shared/redirect') @@ -970,14 +1056,15 @@ redirect_params = UriService.params(assigns(:oidc_redirect_uri)) - expect(redirect_params[:error]).to eq('invalid_request') - expect(redirect_params[:error_description]).to be_present + expect(redirect_params[:code]).to be_present expect(redirect_params[:state]).to eq(params[:state]) end - it 'renders JS client-side redirect with an invalid request if JS client-side redirect is enabled' do + it 'renders a JS client-side redirect back to the client app with a code if it is enabled' do allow(IdentityConfig.store).to receive(:openid_connect_redirect). and_return('client_side_js') + IdentityLinker.new(user, service_provider).link_identity(ial: 1) + user.identities.last.update!(verified_attributes: %w[given_name family_name birthdate]) action expect(controller).to render_template('openid_connect/shared/redirect_js') @@ -985,218 +1072,1386 @@ redirect_params = UriService.params(assigns(:oidc_redirect_uri)) - expect(redirect_params[:error]).to eq('invalid_request') - expect(redirect_params[:error_description]).to be_present + expect(redirect_params[:code]).to be_present expect(redirect_params[:state]).to eq(params[:state]) end - it 'redirects the user with an invalid request if UUID is in server-side redirect list' do - allow(IdentityConfig.store).to receive(:openid_connect_redirect). - and_return('client_side') - allow(IdentityConfig.store).to receive(:openid_connect_redirect_uuid_override_map). - and_return({ user.uuid => 'server_side' }) - action + context 'with ial1 requested using acr_values' do + let(:acr_values) { Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF } + let(:vtr) { nil } - expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + it 'tracks IAL1 authentication event' do + stub_analytics + IdentityLinker.new(user, service_provider).link_identity(ial: 1) + user.identities.last.update!(verified_attributes: %w[given_name family_name birthdate]) - redirect_params = UriService.params(response.location) + action - expect(redirect_params[:error]).to eq('invalid_request') - expect(redirect_params[:error_description]).to be_present - expect(redirect_params[:state]).to eq(params[:state]) - end + sp_return_log = SpReturnLog.find_by(issuer: client_id) + expect(sp_return_log.ial).to eq(1) - it 'renders client-side redirect with an invalid request if UUID is overriden for client-side redirect' do - allow(IdentityConfig.store).to receive(:openid_connect_redirect). - and_return('server_side') - allow(IdentityConfig.store).to receive(:openid_connect_redirect_uuid_override_map). - and_return({ user.uuid => 'client_side' }) - action + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request', + success: true, + client_id: client_id, + prompt: 'select_account', + referer: nil, + allow_prompt_login: true, + errors: {}, + unauthorized_scope: true, + user_fully_authenticated: true, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/1', + code_challenge_present: false, + service_provider_pkce: nil, + scope: 'openid', + vtr: nil, + vtr_param: nil, + ) + + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request handoff', + success: true, + client_id: client_id, + user_sp_authorized: true, + code_digest: kind_of(String), + ) + + expect(@analytics).to have_logged_event( + 'SP redirect initiated', + ial: 1, + billed_ial: 1, + sign_in_flow:, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/1', + vtr: nil, + ) + end + end - expect(controller).to render_template('openid_connect/shared/redirect') - expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + context 'with ial1 requested using vtr' do + let(:acr_values) { nil } + let(:vtr) { ['C1'].to_json } - redirect_params = UriService.params(assigns(:oidc_redirect_uri)) + before do + allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true) + end - expect(redirect_params[:error]).to eq('invalid_request') - expect(redirect_params[:error_description]).to be_present - expect(redirect_params[:state]).to eq(params[:state]) - end + it 'tracks IAL1 authentication event' do + stub_analytics - it 'renders JS client-side redirect with an invalid request if UUID is overriden for JS client-side redirect' do - allow(IdentityConfig.store).to receive(:openid_connect_redirect). - and_return('server_side') - allow(IdentityConfig.store).to receive(:openid_connect_redirect_uuid_override_map). - and_return({ user.uuid => 'client_side_js' }) - action + IdentityLinker.new(user, service_provider).link_identity(ial: 1) + user.identities.last.update!(verified_attributes: %w[given_name family_name birthdate]) - expect(controller).to render_template('openid_connect/shared/redirect_js') - expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + action - redirect_params = UriService.params(assigns(:oidc_redirect_uri)) + sp_return_log = SpReturnLog.find_by(issuer: client_id) + expect(sp_return_log.ial).to eq(1) - expect(redirect_params[:error]).to eq('invalid_request') - expect(redirect_params[:error_description]).to be_present - expect(redirect_params[:state]).to eq(params[:state]) + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request', + success: true, + client_id: client_id, + prompt: 'select_account', + referer: nil, + allow_prompt_login: true, + errors: {}, + unauthorized_scope: true, + user_fully_authenticated: true, + acr_values: '', + code_challenge_present: false, + service_provider_pkce: nil, + scope: 'openid', + vtr: ['C1'], + vtr_param: ['C1'].to_json, + ) + + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request handoff', + success: true, + client_id: client_id, + user_sp_authorized: true, + code_digest: kind_of(String), + ) + + expect(@analytics).to have_logged_event( + 'SP redirect initiated', + ial: 1, + billed_ial: 1, + sign_in_flow:, + acr_values: '', + vtr: ['C1'], + ) + end end - it 'tracks the event with errors' do - stub_analytics - expect(@analytics).to receive(:track_event). - with('OpenID Connect: authorization request', - success: false, - client_id: client_id, - prompt: '', - referer: nil, - allow_prompt_login: true, - unauthorized_scope: true, - errors: hash_including(:prompt), - error_details: hash_including(:prompt), - user_fully_authenticated: true, - acr_values: 'http://idmanagement.gov/ns/assurance/ial/1', - code_challenge_present: false, - service_provider_pkce: nil, - scope: 'openid', - vtr: nil, - vtr_param: '') - expect(@analytics).to_not receive(:track_event).with('sp redirect initiated') + context 'with ial2 requested using acr' do + let(:acr_values) { Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF } + let(:vtr) { nil } - action + context 'account is already verified' do + let(:user) do + create( + :profile, :active, :verified, proofing_components: { liveness_check: true } + ).user + end - expect(SpReturnLog.count).to eq(0) - end - end + it 'redirects to the redirect_uri immediately when pii is unlocked if client-side redirect is disabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') + IdentityLinker.new(user, service_provider).link_identity(ial: 3) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + allow(controller).to receive(:pii_requested_but_locked?).and_return(false) + action - context 'with invalid params that mean the redirect_uri is not trusted' do - before { params.delete(:client_id) } + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + end - it 'renders the error page' do - action - expect(controller).to render_template('openid_connect/authorization/error') - end + it 'renders a client-side redirect back to the client app immediately if it is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side') + IdentityLinker.new(user, service_provider).link_identity(ial: 3) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + allow(controller).to receive(:pii_requested_but_locked?).and_return(false) + action - it 'tracks the event with errors' do - stub_analytics - expect(@analytics).to receive(:track_event). - with('OpenID Connect: authorization request', - success: false, - client_id: nil, - prompt: 'select_account', - referer: nil, - allow_prompt_login: nil, - unauthorized_scope: true, - errors: hash_including(:client_id), - error_details: hash_including(:client_id), - user_fully_authenticated: true, - acr_values: 'http://idmanagement.gov/ns/assurance/ial/1', - code_challenge_present: false, - service_provider_pkce: nil, - scope: 'openid', - vtr: nil, - vtr_param: '') - expect(@analytics).to_not receive(:track_event).with('SP redirect initiated') + expect(controller).to render_template('openid_connect/shared/redirect') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + end - action + it 'renders a JS client-side redirect back to the client app immediately if it is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side_js') + IdentityLinker.new(user, service_provider).link_identity(ial: 3) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + allow(controller).to receive(:pii_requested_but_locked?).and_return(false) + action + + expect(controller).to render_template('openid_connect/shared/redirect_js') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + end + + it 'redirects back to the client app immediately if UUID is overridden to server-side redirect' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side') + allow(IdentityConfig.store).to receive(:openid_connect_redirect_uuid_override_map). + and_return({ user.uuid => 'server_side' }) + IdentityLinker.new(user, service_provider).link_identity(ial: 3) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + allow(controller).to receive(:pii_requested_but_locked?).and_return(false) + action + + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + end + + it 'renders a client-side redirect back to the client app immediately if UUID is overridden to client-side redirect' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') + allow(IdentityConfig.store).to receive(:openid_connect_redirect_uuid_override_map). + and_return({ user.uuid => 'client_side' }) + IdentityLinker.new(user, service_provider).link_identity(ial: 3) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + allow(controller).to receive(:pii_requested_but_locked?).and_return(false) + action + + expect(controller).to render_template('openid_connect/shared/redirect') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + end + + it 'renders a JS client-side redirect back to the client app immediately if UUID is overridden to JS client-side redirect' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') + allow(IdentityConfig.store).to receive(:openid_connect_redirect_uuid_override_map). + and_return({ user.uuid => 'client_side_js' }) + IdentityLinker.new(user, service_provider).link_identity(ial: 3) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + allow(controller).to receive(:pii_requested_but_locked?).and_return(false) + action + + expect(controller).to render_template('openid_connect/shared/redirect_js') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + end + + it 'respects UUID redirect config when issuer config is also set' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') + allow(IdentityConfig.store).to receive(:openid_connect_redirect_issuer_override_map). + and_return({ service_provider.issuer => 'client_side' }) + allow(IdentityConfig.store).to receive(:openid_connect_redirect_uuid_override_map). + and_return({ user.uuid => 'client_side_js' }) + + IdentityLinker.new(user, service_provider).link_identity(ial: 3) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + allow(controller).to receive(:pii_requested_but_locked?).and_return(false) + action + + expect(controller).to render_template('openid_connect/shared/redirect_js') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + end + + it 'respects issuer redirect config if UUID config is not set' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') + allow(IdentityConfig.store).to receive(:openid_connect_redirect_issuer_override_map). + and_return({ service_provider.issuer => 'client_side_js' }) + + IdentityLinker.new(user, service_provider).link_identity(ial: 3) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + allow(controller).to receive(:pii_requested_but_locked?).and_return(false) + action + + expect(controller).to render_template('openid_connect/shared/redirect_js') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + end + + it 'redirects to the password capture url when pii is locked' do + IdentityLinker.new(user, service_provider).link_identity(ial: 3) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + allow(controller).to receive(:pii_requested_but_locked?).and_return(true) + action + + expect(response).to redirect_to(capture_password_url) + end + + it 'tracks IAL2 authentication event' do + stub_analytics + + IdentityLinker.new(user, service_provider).link_identity(ial: 2) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + allow(controller).to receive(:pii_requested_but_locked?).and_return(false) + action + + sp_return_log = SpReturnLog.find_by(issuer: client_id) + expect(sp_return_log.ial).to eq(2) + + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request', + success: true, + client_id: client_id, + prompt: 'select_account', + referer: nil, + allow_prompt_login: true, + errors: {}, + unauthorized_scope: false, + user_fully_authenticated: true, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/2', + code_challenge_present: false, + service_provider_pkce: nil, + scope: 'openid profile', + vtr: nil, + vtr_param: nil, + ) + + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request handoff', + success: true, + client_id: client_id, + user_sp_authorized: true, + code_digest: kind_of(String), + ) + + expect(@analytics).to have_logged_event( + 'SP redirect initiated', + ial: 2, + billed_ial: 2, + sign_in_flow:, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/2', + vtr: nil, + ) + end + + context 'SP requests biometric_comparison_required' do + let(:selfie_capture_enabled) { true } + let(:vtr) { ['Pb'].to_json } + + before do + expect(FeatureManagement).to receive(:idv_allow_selfie_check?).at_least(:once). + and_return(selfie_capture_enabled) + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') + IdentityLinker.new(user, service_provider).link_identity(ial: 3) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + allow(controller).to receive(:pii_requested_but_locked?).and_return(false) + end + + context 'selfie check was performed' do + it 'redirects to the redirect_uri immediately when pii is unlocked if client-side redirect is disabled' do + user.active_profile.idv_level = :unsupervised_with_selfie + + action + + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + end + end + + context 'selfie check was not performed' do + it 'redirects to have the user verify their account' do + action + expect(controller).to redirect_to(idv_url) + end + end + + context 'selfie capture not enabled, biometric comparison required' do + let(:selfie_capture_enabled) { false } + let(:vtr) { ['Pb'].to_json } + + it 'returns status not_acceptable' do + action + + expect(response.status).to eq(406) + end + end + + context 'selfie capture not enabled, biometric comparison not required' do + let(:selfie_capture_enabled) { false } + let(:vtr) { ['P1'].to_json } + + it 'redirects to the service provider' do + action + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + end + end + end + + context 'SP has a vector of trust that includes a biometric comparison' do + let(:selfie_capture_enabled) { true } + let(:acr_values) { nil } + let(:vtr) { ['Pb'].to_json } + + before do + expect(FeatureManagement).to receive(:idv_allow_selfie_check?).at_least(:once). + and_return(selfie_capture_enabled) + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') + allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true) + IdentityLinker.new(user, service_provider).link_identity(ial: 3) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + allow(controller).to receive(:pii_requested_but_locked?).and_return(false) + end + + context 'selfie check was performed' do + it 'redirects to the redirect_uri immediately when pii is unlocked if client-side redirect is disabled' do + user.active_profile.idv_level = :unsupervised_with_selfie + + action + + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + end + end + + context 'selfie check was not performed' do + it 'redirects to have the user verify their account' do + action + expect(controller).to redirect_to(idv_url) + end + end + + context 'biometric comparison was performed in-person' do + it 'redirects to the redirect_uri immediately when pii is unlocked if client-side redirect is disabled' do + user.active_profile.idv_level = :in_person + + action + + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + end + end + + context 'selfie capture not enabled, biometric_comparison_check requested by sp' do + let(:selfie_capture_enabled) { false } + it 'returns status not_acceptable' do + action + + expect(response.status).to eq(406) + end + end + end + end + + context 'verified non-biometric profile with pending biometric profile' do + before do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') + IdentityLinker.new(user, service_provider).link_identity(ial: 3) + user.identities.last.update!( + verified_attributes: %w[birthdate family_name given_name verified_at], + ) + allow(controller).to receive(:pii_requested_but_locked?).and_return(false) + end + + context 'sp does not request biometrics' do + let(:selfie_capture_enabled) { true } + let(:user) { create(:profile, :active, :verified).user } + + before do + expect(FeatureManagement).to receive(:idv_allow_selfie_check?).at_least(:once). + and_return(selfie_capture_enabled) + end + + it 'redirects to the redirect_uri immediately when pii is unlocked if client-side redirect is disabled' do + create(:profile, :verify_by_mail_pending, :with_pii, idv_level: :unsupervised_with_selfie, user: user) + user.active_profile.idv_level = :legacy_unsupervised + + action + + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + expect(user.identities.last.verified_attributes).to eq(%w[birthdate family_name given_name verified_at]) + end + + it 'redirects to please call page if user has a fraudualent profile' do + create(:profile, :fraud_review_pending, :with_pii, idv_level: :unsupervised_with_selfie, user: user) + + action + + expect(response).to redirect_to(idv_please_call_url) + end + end + + context 'sp requests biometrics' do + let(:selfie_capture_enabled) { true } + let(:user) { create(:profile, :active, :verified).user } + let(:vtr) { ['C1.C2.P1.Pb'].to_json } + + before do + expect(FeatureManagement).to receive(:idv_allow_selfie_check?).at_least(:once). + and_return(selfie_capture_enabled) + end + + it 'redirects to gpo enter code page' do + create(:profile, :verify_by_mail_pending, idv_level: :unsupervised_with_selfie, user: user) + + action + + expect(controller).to redirect_to(idv_verify_by_mail_enter_code_url) + end + end + end + + context 'account is not already verified' do + it 'redirects to have the user verify their account' do + action + expect(controller).to redirect_to(idv_url) + end + + context 'user has a pending profile' do + context 'user has a gpo pending profile' do + let(:user) { create(:profile, :verify_by_mail_pending).user } + + it 'redirects to gpo verify page' do + action + expect(controller).to redirect_to(idv_verify_by_mail_enter_code_url) + end + end + + context 'user has an in person pending profile' do + let(:user) { create(:profile, :in_person_verification_pending).user } + + it 'redirects to in person ready to verify page' do + action + expect(controller).to redirect_to(idv_in_person_ready_to_verify_url) + end + end + + context 'user is under fraud review' do + let(:user) { create(:profile, :fraud_review_pending).user } + + it 'redirects to fraud review page if fraud review is pending' do + action + expect(controller).to redirect_to(idv_please_call_url) + end + end + + context 'user is rejected due to fraud' do + let(:user) { create(:profile, :fraud_rejection).user } + + it 'redirects to fraud rejection page if user is fraud rejected ' do + action + expect(controller).to redirect_to(idv_not_verified_url) + end + end + + context 'user has two pending reasons' do + context 'user has gpo and fraud review pending' do + let(:user) do + create( + :profile, + :verify_by_mail_pending, + :fraud_review_pending, + ).user + end + + it 'redirects to gpo verify page' do + action + expect(controller).to redirect_to(idv_verify_by_mail_enter_code_url) + end + end + + context 'user has gpo and in person pending' do + let(:user) do + create( + :profile, + :verify_by_mail_pending, + :in_person_verification_pending, + ).user + end + + it 'redirects to gpo verify page' do + action + expect(controller).to redirect_to(idv_verify_by_mail_enter_code_url) + end + end + end + end + end + + context 'profile is reset' do + let(:user) { create(:profile, :verified, :password_reset).user } + + it 'redirects to have the user enter their personal key' do + action + expect(controller).to redirect_to(reactivate_account_url) + end + end + end + + context 'with ialmax requested' do + context 'provider is on the ialmax allow list' do + let(:acr_values) { Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF } + let(:vtr) { nil } + + before do + allow(IdentityConfig.store).to receive(:allowed_ialmax_providers) { [client_id] } + end + + context 'account is already verified' do + let(:user) do + create( + :profile, :active, :verified, proofing_components: { liveness_check: true } + ).user + end + + it 'redirects to the redirect_uri immediately when pii is unlocked if server-side redirect is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') + IdentityLinker.new(user, service_provider).link_identity(ial: 3) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + allow(controller).to receive(:pii_requested_but_locked?).and_return(false) + action + + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + end + + it 'renders client-side redirect to the client app immediately if PII is unlocked and it is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side') + + IdentityLinker.new(user, service_provider).link_identity(ial: 3) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + allow(controller).to receive(:pii_requested_but_locked?).and_return(false) + action + + expect(controller).to render_template('openid_connect/shared/redirect') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + end + + it 'renders JS client-side redirect to the client app immediately if PII is unlocked and it is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side_js') + + IdentityLinker.new(user, service_provider).link_identity(ial: 3) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + allow(controller).to receive(:pii_requested_but_locked?).and_return(false) + action + + expect(controller).to render_template('openid_connect/shared/redirect_js') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + end + + it 'redirects to the password capture url when pii is locked' do + IdentityLinker.new(user, service_provider).link_identity(ial: 3) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + allow(controller).to receive(:pii_requested_but_locked?).and_return(true) + action + + expect(response).to redirect_to(capture_password_url) + end + + it 'tracks IAL2 authentication event' do + stub_analytics + + IdentityLinker.new(user, service_provider).link_identity(ial: 2) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + allow(controller).to receive(:pii_requested_but_locked?).and_return(false) + action + + sp_return_log = SpReturnLog.find_by(issuer: client_id) + expect(sp_return_log.ial).to eq(2) + + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request', + success: true, + client_id: client_id, + prompt: 'select_account', + referer: nil, + allow_prompt_login: true, + errors: {}, + unauthorized_scope: false, + user_fully_authenticated: true, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/0', + code_challenge_present: false, + service_provider_pkce: nil, + scope: 'openid profile', + vtr: nil, + vtr_param: nil, + ) + + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request handoff', + success: true, + client_id: client_id, + user_sp_authorized: true, + code_digest: kind_of(String), + ) + + expect(@analytics).to have_logged_event( + 'SP redirect initiated', + ial: 0, + billed_ial: 2, + sign_in_flow:, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/0', + vtr: nil, + ) + end + end + + context 'account is not already verified' do + it 'redirects to the redirect_uri immediately without proofing if server-side redirect is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') + IdentityLinker.new(user, service_provider).link_identity(ial: 1) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + + action + + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + end + + it 'renders client-side redirect to the client app immediately if client-side redirect is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side') + + IdentityLinker.new(user, service_provider).link_identity(ial: 1) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + + action + expect(controller).to render_template('openid_connect/shared/redirect') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + end + + it 'renders JS client-side redirect to the client app immediately if JS client-side redirect is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side_js') + + IdentityLinker.new(user, service_provider).link_identity(ial: 1) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + + action + expect(controller).to render_template('openid_connect/shared/redirect_js') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + end + + it 'tracks IAL1 authentication event' do + stub_analytics + + IdentityLinker.new(user, service_provider).link_identity(ial: 1) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + action + + sp_return_log = SpReturnLog.find_by(issuer: client_id) + expect(sp_return_log.ial).to eq(1) + + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request', + success: true, + client_id: client_id, + prompt: 'select_account', + referer: nil, + allow_prompt_login: true, + errors: {}, + unauthorized_scope: false, + user_fully_authenticated: true, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/0', + code_challenge_present: false, + service_provider_pkce: nil, + scope: 'openid profile', + vtr: nil, + vtr_param: nil, + ) + + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request handoff', + success: true, + client_id: client_id, + user_sp_authorized: true, + code_digest: kind_of(String), + ) + + expect(@analytics).to have_logged_event( + 'SP redirect initiated', + ial: 0, + billed_ial: 1, + sign_in_flow:, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/0', + vtr: nil, + ) + end + end + + context 'profile is reset' do + let(:user) { create(:profile, :verified, :password_reset).user } + + it 'redirects to the redirect_uri immediately without proofing if server-side redirect is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') + + IdentityLinker.new(user, service_provider).link_identity(ial: 1) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + + action + + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + end + + it 'renders client-side redirect to the client app immediately if client-side redirect is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side') + + IdentityLinker.new(user, service_provider).link_identity(ial: 1) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + + action + expect(controller).to render_template('openid_connect/shared/redirect') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + end + + it 'renders JS client-side redirect to the client app immediately if JS client-side redirect is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side_js') + + IdentityLinker.new(user, service_provider).link_identity(ial: 1) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + + action + expect(controller).to render_template('openid_connect/shared/redirect_js') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + end + + it 'tracks IAL1 authentication event' do + stub_analytics + + IdentityLinker.new(user, service_provider).link_identity(ial: 1) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + action + + sp_return_log = SpReturnLog.find_by(issuer: client_id) + expect(sp_return_log.ial).to eq(1) + + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request', + success: true, + client_id: client_id, + prompt: 'select_account', + referer: nil, + allow_prompt_login: true, + errors: {}, + unauthorized_scope: false, + user_fully_authenticated: true, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/0', + code_challenge_present: false, + service_provider_pkce: nil, + scope: 'openid profile', + vtr: nil, + vtr_param: nil, + ) + + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request handoff', + success: true, + client_id: client_id, + user_sp_authorized: true, + code_digest: kind_of(String), + ) + + expect(@analytics).to have_logged_event( + 'SP redirect initiated', + ial: 0, + billed_ial: 1, + sign_in_flow:, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/0', + vtr: nil, + ) + end + end + end + end + + context 'user has not approved this application' do + it 'redirects verify shared attributes page' do + action + + expect(response).to redirect_to(sign_up_completed_url) + end + + it 'does not link identity to the user' do + action + expect(user.identities.count).to eq(0) + end + end + + context 'user has already approved this application' do + before do + IdentityLinker.new(user, service_provider).link_identity + user.identities.last.update!(verified_attributes: %w[given_name family_name birthdate]) + end + + it 'redirects back to the client app with a code if client-side redirect is disabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') + action + + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + + redirect_params = UriService.params(response.location) + + expect(redirect_params[:code]).to be_present + expect(redirect_params[:state]).to eq(params[:state]) + end + + it 'renders a client-side redirect back to the client app with a code if it is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side') + + action + + expect(controller).to render_template('openid_connect/shared/redirect') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + + redirect_params = UriService.params(assigns(:oidc_redirect_uri)) + expect(redirect_params[:code]).to be_present + expect(redirect_params[:state]).to eq(params[:state]) + end + + it 'renders a JS client-side redirect back to the client app with a code if it is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side_js') + + action + + expect(controller).to render_template('openid_connect/shared/redirect_js') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + + redirect_params = UriService.params(assigns(:oidc_redirect_uri)) + expect(redirect_params[:code]).to be_present + expect(redirect_params[:state]).to eq(params[:state]) + end + end + end + + context 'acr with invalid params that do not interfere with the redirect_uri' do + let(:acr_values) { Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF } + let(:vtr) { nil } + + before { params[:prompt] = '' } + + it 'redirects the user with an invalid request if client-side redirect is disabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') + action + + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + + redirect_params = UriService.params(response.location) + + expect(redirect_params[:error]).to eq('invalid_request') + expect(redirect_params[:error_description]).to be_present + expect(redirect_params[:state]).to eq(params[:state]) + end + + it 'renders client-side redirect with an invalid request if client-side redirect is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side') + action + + expect(controller).to render_template('openid_connect/shared/redirect') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + + redirect_params = UriService.params(assigns(:oidc_redirect_uri)) + + expect(redirect_params[:error]).to eq('invalid_request') + expect(redirect_params[:error_description]).to be_present + expect(redirect_params[:state]).to eq(params[:state]) + end + + it 'renders JS client-side redirect with an invalid request if JS client-side redirect is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side_js') + action + + expect(controller).to render_template('openid_connect/shared/redirect_js') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + + redirect_params = UriService.params(assigns(:oidc_redirect_uri)) + + expect(redirect_params[:error]).to eq('invalid_request') + expect(redirect_params[:error_description]).to be_present + expect(redirect_params[:state]).to eq(params[:state]) + end + + it 'redirects the user with an invalid request if UUID is in server-side redirect list' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side') + allow(IdentityConfig.store).to receive(:openid_connect_redirect_uuid_override_map). + and_return({ user.uuid => 'server_side' }) + action + + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + + redirect_params = UriService.params(response.location) + + expect(redirect_params[:error]).to eq('invalid_request') + expect(redirect_params[:error_description]).to be_present + expect(redirect_params[:state]).to eq(params[:state]) + end + + it 'renders client-side redirect with an invalid request if UUID is overriden for client-side redirect' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') + allow(IdentityConfig.store).to receive(:openid_connect_redirect_uuid_override_map). + and_return({ user.uuid => 'client_side' }) + action + + expect(controller).to render_template('openid_connect/shared/redirect') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + + redirect_params = UriService.params(assigns(:oidc_redirect_uri)) + + expect(redirect_params[:error]).to eq('invalid_request') + expect(redirect_params[:error_description]).to be_present + expect(redirect_params[:state]).to eq(params[:state]) + end + + it 'renders JS client-side redirect with an invalid request if UUID is overriden for JS client-side redirect' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') + allow(IdentityConfig.store).to receive(:openid_connect_redirect_uuid_override_map). + and_return({ user.uuid => 'client_side_js' }) + action + + expect(controller).to render_template('openid_connect/shared/redirect_js') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + + redirect_params = UriService.params(assigns(:oidc_redirect_uri)) + + expect(redirect_params[:error]).to eq('invalid_request') + expect(redirect_params[:error_description]).to be_present + expect(redirect_params[:state]).to eq(params[:state]) + end + + it 'tracks the event with errors' do + stub_analytics + + action + + expect(SpReturnLog.count).to eq(0) + + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request', + success: false, + client_id: client_id, + prompt: '', + referer: nil, + allow_prompt_login: true, + unauthorized_scope: true, + errors: hash_including(:prompt), + error_details: hash_including(:prompt), + user_fully_authenticated: true, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/1', + code_challenge_present: false, + service_provider_pkce: nil, + scope: 'openid', + vtr: nil, + vtr_param: nil, + ) + + expect(@analytics).to_not have_logged_event('SP redirect initiated') + end + end + + context 'vtr with invalid params that do not interfere with the redirect_uri' do + let(:acr_values) { nil } + let(:vtr) { ['C1'].to_json } + + before { params[:prompt] = '' } + + it 'redirects the user with an invalid request if client-side redirect is disabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') + + action + + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + + redirect_params = UriService.params(response.location) + + expect(redirect_params[:error]).to eq('invalid_request') + expect(redirect_params[:error_description]).to be_present + expect(redirect_params[:state]).to eq(params[:state]) + end + + it 'renders client-side redirect with an invalid request if client-side redirect is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side') + action + + expect(controller).to render_template('openid_connect/shared/redirect') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + + redirect_params = UriService.params(assigns(:oidc_redirect_uri)) + + expect(redirect_params[:error]).to eq('invalid_request') + expect(redirect_params[:error_description]).to be_present + expect(redirect_params[:state]).to eq(params[:state]) + end + + it 'renders JS client-side redirect with an invalid request if JS client-side redirect is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side_js') + action + + expect(controller).to render_template('openid_connect/shared/redirect_js') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + + redirect_params = UriService.params(assigns(:oidc_redirect_uri)) + + expect(redirect_params[:error]).to eq('invalid_request') + expect(redirect_params[:error_description]).to be_present + expect(redirect_params[:state]).to eq(params[:state]) + end + + it 'redirects the user with an invalid request if UUID is in server-side redirect list' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side') + allow(IdentityConfig.store).to receive(:openid_connect_redirect_uuid_override_map). + and_return({ user.uuid => 'server_side' }) + action + + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + + redirect_params = UriService.params(response.location) + + expect(redirect_params[:error]).to eq('invalid_request') + expect(redirect_params[:error_description]).to be_present + expect(redirect_params[:state]).to eq(params[:state]) + end + + it 'renders client-side redirect with an invalid request if UUID is overriden for client-side redirect' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') + allow(IdentityConfig.store).to receive(:openid_connect_redirect_uuid_override_map). + and_return({ user.uuid => 'client_side' }) + + action + + expect(controller).to render_template('openid_connect/shared/redirect') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + + redirect_params = UriService.params(assigns(:oidc_redirect_uri)) + + expect(redirect_params[:error]).to eq('invalid_request') + expect(redirect_params[:error_description]).to be_present + expect(redirect_params[:state]).to eq(params[:state]) + end + + it 'renders JS client-side redirect with an invalid request if UUID is overriden for JS client-side redirect' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') + allow(IdentityConfig.store).to receive(:openid_connect_redirect_uuid_override_map). + and_return({ user.uuid => 'client_side_js' }) + action + + expect(controller).to render_template('openid_connect/shared/redirect_js') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + + redirect_params = UriService.params(assigns(:oidc_redirect_uri)) + + expect(redirect_params[:error]).to eq('invalid_request') + expect(redirect_params[:error_description]).to be_present + expect(redirect_params[:state]).to eq(params[:state]) + end + + it 'tracks the event with errors' do + stub_analytics + + action + + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request', + success: false, + client_id: client_id, + prompt: '', + referer: nil, + allow_prompt_login: true, + unauthorized_scope: true, + errors: hash_including(:prompt), + error_details: hash_including(:prompt), + user_fully_authenticated: true, + acr_values: '', + code_challenge_present: false, + service_provider_pkce: nil, + scope: 'openid', + vtr: ['C1'], + vtr_param: '["C1"]', + ) + + expect(@analytics).to_not have_logged_event('SP redirect initiated') expect(SpReturnLog.count).to eq(0) end end - end - context 'user is not signed in' do - context 'without valid acr_values' do - before { params.delete(:acr_values) } + context 'acr with invalid params that mean the redirect_uri is not trusted' do + let(:acr_values) { Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF } + let(:vtr) { nil } - it 'handles the error and does not blow up when server-side redirect is enabled' do - allow(IdentityConfig.store).to receive(:openid_connect_redirect). - and_return('server_side') - action + before { params.delete(:client_id) } - expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + it 'renders the error page' do + action + expect(controller).to render_template('openid_connect/authorization/error') end - it 'handles the error and does not blow up when client-side redirect is enabled' do - allow(IdentityConfig.store).to receive(:openid_connect_redirect). - and_return('client_side') + it 'tracks the event with errors' do + stub_analytics + action - expect(controller).to render_template('openid_connect/shared/redirect') - expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) - end + expect(SpReturnLog.count).to eq(0) - it 'handles the error and does not blow up when client-side redirect is enabled' do - allow(IdentityConfig.store).to receive(:openid_connect_redirect). - and_return('client_side_js') - action + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request', + success: false, + client_id: nil, + prompt: 'select_account', + referer: nil, + allow_prompt_login: nil, + unauthorized_scope: true, + errors: hash_including(:client_id), + error_details: hash_including(:client_id), + user_fully_authenticated: true, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/1', + code_challenge_present: false, + service_provider_pkce: nil, + scope: 'openid', + vtr: nil, + vtr_param: nil, + ) - expect(controller).to render_template('openid_connect/shared/redirect_js') - expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + expect(@analytics).to_not have_logged_event('SP redirect initiated') end end - context 'with a bad redirect_uri' do - before { params[:redirect_uri] = '!!!' } + context 'vtr with invalid params that mean the redirect_uri is not trusted' do + let(:acr_values) { nil } + let(:vtr) { ['C1'].to_json } + + before { params.delete(:client_id) } it 'renders the error page' do action expect(controller).to render_template('openid_connect/authorization/error') end - end - context 'ialmax requested when service provider is not in allowlist' do - before do - params[:acr_values] = Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF - end + it 'tracks the event with errors' do + stub_analytics - it 'redirects the user if server-side redirect is enabled' do - allow(IdentityConfig.store).to receive(:openid_connect_redirect). - and_return('server_side') action - expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + expect(SpReturnLog.count).to eq(0) - redirect_params = UriService.params(response.location) + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request', + success: false, + client_id: nil, + prompt: 'select_account', + referer: nil, + allow_prompt_login: nil, + unauthorized_scope: true, + errors: hash_including(:client_id), + error_details: hash_including(:client_id), + user_fully_authenticated: true, + acr_values: '', + code_challenge_present: false, + service_provider_pkce: nil, + scope: 'openid', + vtr: ['C1'], + vtr_param: '["C1"]', + ) - expect(redirect_params[:error]).to eq('invalid_request') - expect(redirect_params[:error_description]).to be_present - expect(redirect_params[:state]).to eq(params[:state]) + expect(@analytics).to_not have_logged_event('SP redirect initiated') end + end + end - it 'renders a client-side redirect if client-side redirect is enabled' do - allow(IdentityConfig.store).to receive(:openid_connect_redirect). - and_return('client_side') - action + context 'user is not signed in' do + context 'using acr_values' do + let(:vtr) { nil } # purely for emphasis + let(:acr_values) { Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF } - expect(controller).to render_template('openid_connect/shared/redirect') - expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + context 'without valid acr_values' do + let(:acr_values) { nil } - redirect_params = UriService.params(assigns(:oidc_redirect_uri)) + it 'handles the error and does not blow up when server-side redirect is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') + action - expect(redirect_params[:error]).to eq('invalid_request') - expect(redirect_params[:error_description]).to be_present - expect(redirect_params[:state]).to eq(params[:state]) + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + end + + it 'handles the error and does not blow up when client-side redirect is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side') + action + + expect(controller).to render_template('openid_connect/shared/redirect') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + end + + it 'handles the error and does not blow up when client-side redirect is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side_js') + action + + expect(controller).to render_template('openid_connect/shared/redirect_js') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + end end - it 'renders a JS client-side redirect if JS client-side redirect is enabled' do - allow(IdentityConfig.store).to receive(:openid_connect_redirect). - and_return('client_side_js') - action + context 'with a bad redirect_uri' do + before { params[:redirect_uri] = '!!!' } - expect(controller).to render_template('openid_connect/shared/redirect_js') - expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + it 'renders the error page' do + action + expect(controller).to render_template('openid_connect/authorization/error') + end + end - redirect_params = UriService.params(assigns(:oidc_redirect_uri)) + context 'ialmax requested when service provider is not in allowlist' do + before do + params[:acr_values] = Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF + end - expect(redirect_params[:error]).to eq('invalid_request') - expect(redirect_params[:error_description]).to be_present - expect(redirect_params[:state]).to eq(params[:state]) + it 'redirects the user if server-side redirect is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') + action + + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + + redirect_params = UriService.params(response.location) + + expect(redirect_params[:error]).to eq('invalid_request') + expect(redirect_params[:error_description]).to be_present + expect(redirect_params[:state]).to eq(params[:state]) + end + + it 'renders a client-side redirect if client-side redirect is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side') + action + + expect(controller).to render_template('openid_connect/shared/redirect') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + + redirect_params = UriService.params(assigns(:oidc_redirect_uri)) + + expect(redirect_params[:error]).to eq('invalid_request') + expect(redirect_params[:error_description]).to be_present + expect(redirect_params[:state]).to eq(params[:state]) + end + + it 'renders a JS client-side redirect if JS client-side redirect is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side_js') + action + + expect(controller).to render_template('openid_connect/shared/redirect_js') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + + redirect_params = UriService.params(assigns(:oidc_redirect_uri)) + + expect(redirect_params[:error]).to eq('invalid_request') + expect(redirect_params[:error_description]).to be_present + expect(redirect_params[:state]).to eq(params[:state]) + end end - end - it 'redirects to SP landing page with the request_id in the params' do - stub_analytics - expect(@analytics).to receive(:track_event). - with( + it 'redirects to SP landing page with the request_id in the params' do + stub_analytics + + action + sp_request_id = ServiceProviderRequestProxy.last.uuid + + expect(response).to redirect_to new_user_session_url + expect(controller.session[:sp][:request_id]).to eq(sp_request_id) + expect(@analytics).to have_logged_event( 'OpenID Connect: authorization request', success: true, client_id: client_id, @@ -1211,29 +2466,158 @@ service_provider_pkce: nil, scope: 'openid', vtr: nil, - vtr_param: '', + vtr_param: nil, ) + end - action - sp_request_id = ServiceProviderRequestProxy.last.uuid - - expect(response).to redirect_to new_user_session_url - expect(controller.session[:sp][:request_id]).to eq(sp_request_id) + it 'sets sp information in the session and does not transmit ial2 attrs for ial1' do + action + sp_request_id = ServiceProviderRequestProxy.last.uuid + + expect(session[:sp]).to eq( + acr_values: Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, + issuer: 'urn:gov:gsa:openidconnect:test', + request_id: sp_request_id, + request_url: request.original_url, + requested_attributes: %w[], + biometric_comparison_required: false, + vtr: nil, + ) + end end - it 'sets sp information in the session and does not transmit ial2 attrs for ial1' do - action - sp_request_id = ServiceProviderRequestProxy.last.uuid - - expect(session[:sp]).to eq( - acr_values: Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, - issuer: 'urn:gov:gsa:openidconnect:test', - request_id: sp_request_id, - request_url: request.original_url, - requested_attributes: %w[], - biometric_comparison_required: false, - vtr: nil, - ) + context 'using vot' do + let(:acr_values) { nil } # for emphasis + let(:vtr) { ['C1'].to_json } + + context 'without a valid vtr' do + let(:vtr) { nil } + + it 'handles the error and does not blow up when server-side redirect is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') + action + + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + end + + it 'handles the error and does not blow up when client-side redirect is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side') + action + + expect(controller).to render_template('openid_connect/shared/redirect') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + end + + it 'handles the error and does not blow up when client-side redirect is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side_js') + action + + expect(controller).to render_template('openid_connect/shared/redirect_js') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + end + end + + context 'with a bad redirect_uri' do + before { params[:redirect_uri] = '!!!' } + + it 'renders the error page' do + action + expect(controller).to render_template('openid_connect/authorization/error') + end + end + + context 'ialmax requested when service provider is not in allowlist' do + let(:vtr) { ['CaPb'].to_json } + + it 'redirects the user if server-side redirect is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') + action + + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + + redirect_params = UriService.params(response.location) + + expect(redirect_params[:error]).to eq('invalid_request') + expect(redirect_params[:error_description]).to be_present + expect(redirect_params[:state]).to eq(params[:state]) + end + + it 'renders a client-side redirect if client-side redirect is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side') + action + + expect(controller).to render_template('openid_connect/shared/redirect') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + + redirect_params = UriService.params(assigns(:oidc_redirect_uri)) + + expect(redirect_params[:error]).to eq('invalid_request') + expect(redirect_params[:error_description]).to be_present + expect(redirect_params[:state]).to eq(params[:state]) + end + + it 'renders a JS client-side redirect if JS client-side redirect is enabled' do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side_js') + action + + expect(controller).to render_template('openid_connect/shared/redirect_js') + expect(assigns(:oidc_redirect_uri)).to start_with(params[:redirect_uri]) + + redirect_params = UriService.params(assigns(:oidc_redirect_uri)) + + expect(redirect_params[:error]).to eq('invalid_request') + expect(redirect_params[:error_description]).to be_present + expect(redirect_params[:state]).to eq(params[:state]) + end + end + + it 'redirects to SP landing page with the request_id in the params' do + stub_analytics + + action + sp_request_id = ServiceProviderRequestProxy.last.uuid + + expect(response).to redirect_to new_user_session_url + expect(controller.session[:sp][:request_id]).to eq(sp_request_id) + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request', + success: true, + client_id: client_id, + prompt: 'select_account', + referer: nil, + allow_prompt_login: true, + errors: {}, + unauthorized_scope: true, + user_fully_authenticated: false, + acr_values: '', + code_challenge_present: false, + service_provider_pkce: nil, + scope: 'openid', + vtr: ['C1'], + vtr_param: ['C1'].to_json, + ) + end + + it 'sets sp information in the session and does not transmit ial2 attrs for ial1' do + action + sp_request_id = ServiceProviderRequestProxy.last.uuid + + expect(session[:sp]).to eq( + acr_values: '', + issuer: 'urn:gov:gsa:openidconnect:test', + request_id: sp_request_id, + request_url: request.original_url, + requested_attributes: %w[], + biometric_comparison_required: false, + vtr: ['C1'], + ) + end end end end diff --git a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb index 72d13471eac..331c859a614 100644 --- a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb @@ -470,29 +470,28 @@ let(:user) { create(:user, :fully_registered) } before do sign_in_as_user(user) - subject.user_session[TwoFactorAuthenticatable::NEED_AUTHENTICATION] = false - subject.user_session[:unconfirmed_phone] = '+1 (703) 555-5555' - subject.user_session[:context] = 'confirmation' + controller.user_session[TwoFactorAuthenticatable::NEED_AUTHENTICATION] = false + controller.user_session[:unconfirmed_phone] = '+1 (703) 555-5555' + controller.user_session[:context] = 'confirmation' @previous_phone_confirmed_at = - MfaContext.new(subject.current_user).phone_configurations.first&.confirmed_at + MfaContext.new(controller.current_user).phone_configurations.first&.confirmed_at - subject.current_user.create_direct_otp + controller.current_user.create_direct_otp stub_analytics stub_attempts_tracker - allow(@analytics).to receive(:track_event) - allow(subject).to receive(:create_user_event) + allow(controller).to receive(:create_user_event) @mailer = instance_double(ActionMailer::MessageDelivery, deliver_now_or_later: true) - subject.current_user.email_addresses.each do |email_address| + controller.current_user.email_addresses.each do |email_address| allow(UserMailer).to receive(:phone_added). - with(subject.current_user, email_address, disavowal_token: instance_of(String)). + with(controller.current_user, email_address, disavowal_token: instance_of(String)). and_return(@mailer) end - @previous_phone = MfaContext.new(subject.current_user).phone_configurations.first&.phone + @previous_phone = MfaContext.new(controller.current_user).phone_configurations.first&.phone end context 'user is fully authenticated and has an existing phone number' do @@ -522,8 +521,6 @@ in_account_creation_flow: true, } - expect(@analytics).to receive(:track_event). - with('Multi-Factor Authentication Setup', properties) controller.user_session[:phone_id] = phone_id expect(@irs_attempts_api_tracker).to receive(:mfa_enroll_phone_otp_submitted). @@ -536,6 +533,8 @@ otp_delivery_preference: 'sms', }, ) + + expect(@analytics).to have_logged_event('Multi-Factor Authentication Setup', properties) end it 'resets otp session data' do @@ -612,8 +611,7 @@ in_account_creation_flow: false, } - expect(@analytics).to have_received(:track_event). - with('Multi-Factor Authentication Setup', properties) + expect(@analytics).to have_logged_event('Multi-Factor Authentication Setup', properties) end context 'user enters in valid code after invalid entry' do @@ -660,16 +658,16 @@ context 'when user does not have an existing phone number' do before do - MfaContext.new(subject.current_user).phone_configurations.clear - subject.current_user.create_direct_otp + MfaContext.new(controller.current_user).phone_configurations.clear + controller.current_user.create_direct_otp end context 'when given valid code' do - before do + subject(:response) do post( :create, params: { - code: subject.current_user.direct_otp, + code: controller.current_user.direct_otp, otp_delivery_preference: 'sms', }, ) @@ -697,15 +695,41 @@ in_account_creation_flow: false, } - expect(@analytics).to have_received(:track_event). - with('Multi-Factor Authentication Setup', properties) + response - expect(subject).to have_received(:create_user_event).with(:phone_confirmed) - expect(subject).to have_received(:create_user_event).exactly(:once) + expect(@analytics).to have_logged_event('Multi-Factor Authentication Setup', properties) + + expect(controller).to have_received(:create_user_event).with(:phone_confirmed) + expect(controller).to have_received(:create_user_event).exactly(:once) + end + + it 'annotates with passed 2fa and resets a recaptcha assessment' do + assessment_id = 'projects/project-id/assessments/assessment-id' + recaptcha_annotation = { + assessment_id:, + reason: RecaptchaAnnotator::AnnotationReasons::PASSED_TWO_FACTOR, + } + + controller.user_session[:phone_recaptcha_assessment_id] = assessment_id + + expect(RecaptchaAnnotator).to receive(:annotate). + with(**recaptcha_annotation). + and_return(recaptcha_annotation) + + expect { response }. + to change { controller.user_session[:phone_recaptcha_assessment_id] }. + from(assessment_id).to(nil) + + expect(@analytics).to have_logged_event( + 'Multi-Factor Authentication: Added phone', + hash_including(recaptcha_annotation:), + ) end it 'resets context to authentication' do - expect(subject.user_session[:context]).to eq 'authentication' + response + + expect(controller.user_session[:context]).to eq 'authentication' end end diff --git a/spec/controllers/users/phone_setup_controller_spec.rb b/spec/controllers/users/phone_setup_controller_spec.rb index 20dc76dd601..a57de1f28bd 100644 --- a/spec/controllers/users/phone_setup_controller_spec.rb +++ b/spec/controllers/users/phone_setup_controller_spec.rb @@ -97,7 +97,7 @@ expect(flash[:error]).to be_blank end - context 'with recaptcha error' do + context 'with recaptcha enabled' do before do allow(FeatureManagement).to receive(:phone_recaptcha_enabled?).and_return(true) allow(IdentityConfig.store).to receive(:phone_recaptcha_country_score_overrides). @@ -105,13 +105,39 @@ allow(IdentityConfig.store).to receive(:phone_recaptcha_score_threshold).and_return(0.6) end - it 'renders form with error message' do - stub_sign_in + context 'with recaptcha success' do + it 'assigns assessment id to user session' do + recaptcha_token = 'token' + stub_sign_in + + post( + :create, + params: { + new_phone_form: { + phone: '3065550100', + international_code: 'CA', + recaptcha_token:, + recaptcha_mock_score: '0.7', + }, + }, + ) + + expect(controller.user_session[:phone_recaptcha_assessment_id]).to be_kind_of(String) + end + end - post :create, params: { new_phone_form: { phone: '3065550100', international_code: 'CA' } } + context 'with recaptcha error' do + it 'renders form with error message' do + stub_sign_in - expect(response).to render_template(:index) - expect(flash[:error]).to eq(t('errors.messages.invalid_recaptcha_token')) + post( + :create, + params: { new_phone_form: { phone: '3065550100', international_code: 'CA' } }, + ) + + expect(response).to render_template(:index) + expect(flash[:error]).to eq(t('errors.messages.invalid_recaptcha_token')) + end end end diff --git a/spec/controllers/users/two_factor_authentication_controller_spec.rb b/spec/controllers/users/two_factor_authentication_controller_spec.rb index e7704848410..5fc618b23fd 100644 --- a/spec/controllers/users/two_factor_authentication_controller_spec.rb +++ b/spec/controllers/users/two_factor_authentication_controller_spec.rb @@ -289,7 +289,7 @@ def index before do @user = create(:user, :with_phone) sign_in_before_2fa(@user) - @old_otp = subject.current_user.direct_otp + @old_otp = controller.current_user.direct_otp allow(Telephony).to receive(:send_authentication_otp).and_call_original end @@ -394,6 +394,41 @@ def index end end + context 'with recaptcha phone assessment id in session' do + let(:assessment_id) { 'projects/project-id/assessments/assessment-id' } + + subject(:response) do + get :send_code, params: { + otp_delivery_selection_form: { + **otp_preference_sms, + otp_make_default_number: nil, + }, + } + end + + before do + stub_analytics + controller.user_session[:phone_recaptcha_assessment_id] = assessment_id + end + + it 'annotates recaptcha assessment with initiated 2fa' do + recaptcha_annotation = { + assessment_id:, + reason: RecaptchaAnnotator::AnnotationReasons::INITIATED_TWO_FACTOR, + } + expect(RecaptchaAnnotator).to receive(:annotate).once. + with(**recaptcha_annotation). + and_return(recaptcha_annotation) + + response + + expect(@analytics).to have_logged_event( + 'Telephony: OTP sent', + hash_including(recaptcha_annotation:), + ) + end + end + context 'when the phone has been marked as opted out in the DB' do before do PhoneNumberOptOut.mark_opted_out(@user.phone_configurations.first.phone) @@ -549,6 +584,7 @@ def index expiration: 10, channel: :sms, otp_format: 'digit', + otp_length: '6', domain: IdentityConfig.store.domain_name, country_code: 'US', extra_metadata: { diff --git a/spec/decorators/service_provider_session_spec.rb b/spec/decorators/service_provider_session_spec.rb index 027bf9509a8..ff48afdc6b7 100644 --- a/spec/decorators/service_provider_session_spec.rb +++ b/spec/decorators/service_provider_session_spec.rb @@ -171,46 +171,6 @@ end end - describe '#selfie_required' do - before do - expect(FeatureManagement).to receive(:idv_allow_selfie_check?). - and_return(selfie_capture_enabled) - end - - context 'doc_auth_selfie_capture_enabled is true' do - let(:selfie_capture_enabled) { true } - - it 'returns true when sp biometric_comparison_required is true' do - sp_session[:biometric_comparison_required] = true - expect(subject.biometric_comparison_required?).to eq(true) - end - - it 'returns true when sp biometric_comparison_required is truthy' do - sp_session[:biometric_comparison_required] = 1 - expect(subject.biometric_comparison_required?).to eq(true) - end - - it 'returns false when sp biometric_comparison_required is false' do - sp_session[:biometric_comparison_required] = false - expect(subject.biometric_comparison_required?).to eq(false) - end - - it 'returns false when sp biometric_comparison_required is nil' do - sp_session[:biometric_comparison_required] = nil - expect(subject.biometric_comparison_required?).to eq(false) - end - end - - context 'doc_auth_selfie_capture_enabled is false' do - let(:selfie_capture_enabled) { false } - - it 'returns false' do - sp_session[:biometric_comparison_required] = true - expect(subject.biometric_comparison_required?).to eq(false) - end - end - end - describe '#cancel_link_url' do subject(:decorator) do ServiceProviderSession.new( diff --git a/spec/features/accessibility/static_pages_spec.rb b/spec/features/accessibility/static_pages_spec.rb index 00e934557d0..5d79ae1a766 100644 --- a/spec/features/accessibility/static_pages_spec.rb +++ b/spec/features/accessibility/static_pages_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' require 'axe-rspec' -RSpec.feature 'Accessibility on static pages', :js, allowed_extra_analytics: [:*] do +RSpec.feature 'Accessibility on static pages', :js do scenario 'not found page', allow_browser_log: true do visit '/non_existent_page' diff --git a/spec/features/account/backup_codes_spec.rb b/spec/features/account/backup_codes_spec.rb index e64b388c17a..6e614648321 100644 --- a/spec/features/account/backup_codes_spec.rb +++ b/spec/features/account/backup_codes_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'Backup codes', allowed_extra_analytics: [:*] do +RSpec.feature 'Backup codes' do before do sign_in_and_2fa_user(user) visit account_two_factor_authentication_path diff --git a/spec/features/account/device_spec.rb b/spec/features/account/device_spec.rb index 0df71b4ed3c..0921759c89c 100644 --- a/spec/features/account/device_spec.rb +++ b/spec/features/account/device_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe 'Devices', allowed_extra_analytics: [:*] do +RSpec.describe 'Devices' do let(:user) { create(:user, :fully_registered) } before do user = create(:user, :fully_registered, otp_delivery_preference: 'sms') diff --git a/spec/features/account/unphishable_badge_spec.rb b/spec/features/account/unphishable_badge_spec.rb index d33cf92b432..75a2413d43e 100644 --- a/spec/features/account/unphishable_badge_spec.rb +++ b/spec/features/account/unphishable_badge_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'Unphishable account badge', allowed_extra_analytics: [:*] do +RSpec.feature 'Unphishable account badge' do before do sign_in_and_2fa_user(user) end diff --git a/spec/features/account_connected_apps_spec.rb b/spec/features/account_connected_apps_spec.rb index ff8581a4ea3..7588b9dac78 100644 --- a/spec/features/account_connected_apps_spec.rb +++ b/spec/features/account_connected_apps_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe 'Account connected applications', allowed_extra_analytics: [:*] do +RSpec.describe 'Account connected applications' do let(:user) { create(:user, :fully_registered, created_at: Time.zone.now - 100.days) } let(:identity_with_link) do create( diff --git a/spec/features/account_creation/completions_cancel_spec.rb b/spec/features/account_creation/completions_cancel_spec.rb new file mode 100644 index 00000000000..7f8e9b7b477 --- /dev/null +++ b/spec/features/account_creation/completions_cancel_spec.rb @@ -0,0 +1,27 @@ +require 'rails_helper' + +RSpec.feature 'canceling at the completions screen' do + include SamlAuthHelper + + it 'redirects accordingly' do + visit_idp_from_sp_with_ial1(:oidc) + sign_up_and_set_password + select_2fa_option('backup_code') + click_continue + + expect(current_path).to eq(sign_up_completed_path) + + click_on t('links.cancel') + + expect(current_path).to eq(sign_up_completed_cancel_path) + click_on t('login_cancel.keep_going') + + expect(current_path).to eq(sign_up_completed_path) + click_on t('links.cancel') + + expect(current_path).to eq(sign_up_completed_cancel_path) + click_on t('login_cancel.exit', app_name: APP_NAME) + + expect(current_url).to start_with('http://localhost:7654/auth/result?error=access_denied') + end +end diff --git a/spec/features/account_history_spec.rb b/spec/features/account_history_spec.rb index 7d23a72ac06..a5dd94df942 100644 --- a/spec/features/account_history_spec.rb +++ b/spec/features/account_history_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe 'Account history', allowed_extra_analytics: [:*] do +RSpec.describe 'Account history' do let(:user) { create(:user, :fully_registered, created_at: Time.zone.now - 100.days) } let(:account_created_event) { create(:event, user: user, created_at: Time.zone.now - 98.days) } let(:gpo_mail_sent_event) do diff --git a/spec/features/device_tracking_spec.rb b/spec/features/device_tracking_spec.rb index 0e3795e8b0f..af02ba5a64b 100644 --- a/spec/features/device_tracking_spec.rb +++ b/spec/features/device_tracking_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe 'Device tracking', allowed_extra_analytics: [:*] do +RSpec.describe 'Device tracking' do let(:user) { create(:user, :fully_registered) } let(:now) { Time.zone.now } let(:device) { create(:device, user: user, last_ip: '4.3.2.1', last_used_at: now) } diff --git a/spec/features/idv/confirm_start_over_spec.rb b/spec/features/idv/confirm_start_over_spec.rb index 66740bd61c4..a60e75d7db6 100644 --- a/spec/features/idv/confirm_start_over_spec.rb +++ b/spec/features/idv/confirm_start_over_spec.rb @@ -36,7 +36,7 @@ expect(current_path).to eq idv_confirm_start_over_before_letter_path expect(page).to have_content(t('idv.cancel.description.gpo.start_over_new_address')) - expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_phone_or_address')) + expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_phone')) expect(fake_analytics).to have_logged_event(:idv_gpo_confirm_start_over_before_letter_visited) click_idv_continue @@ -57,7 +57,7 @@ expect(current_path).to eq idv_confirm_start_over_path expect(page).to have_content(t('idv.cancel.description.gpo.start_over')) - expect_step_indicator_current_step(t('step_indicator.flows.idv.get_a_letter')) + expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_address')) expect(fake_analytics).to have_logged_event('IdV: gpo confirm start over visited') click_idv_continue diff --git a/spec/features/idv/end_to_end_idv_spec.rb b/spec/features/idv/end_to_end_idv_spec.rb index 99671f29c0a..71f95b6b145 100644 --- a/spec/features/idv/end_to_end_idv_spec.rb +++ b/spec/features/idv/end_to_end_idv_spec.rb @@ -323,9 +323,9 @@ def validate_enter_password_submit(user) def validate_letter_enqueued_page expect(page).to have_current_path(idv_letter_enqueued_path) - expect_in_person_gpo_step_indicator_current_step(t('step_indicator.flows.idv.get_a_letter')) + expect_in_person_gpo_step_indicator_current_step(t('step_indicator.flows.idv.verify_address')) expect(page).to have_content(t('idv.titles.come_back_later')) - expect(page).not_to have_content(t('step_indicator.flows.idv.verify_phone_or_address')) + expect(page).not_to have_content(t('step_indicator.flows.idv.verify_phone')) end def validate_personal_key_page @@ -342,14 +342,14 @@ def validate_personal_key_page expect(page).to have_content(t('idv.messages.confirm')) expect(page).to have_css( '.step-indicator__step--complete', - text: t('step_indicator.flows.idv.verify_phone_or_address'), + text: t('step_indicator.flows.idv.verify_phone'), ) expect(page).to have_css( '.step-indicator__step--complete', - text: t('step_indicator.flows.idv.secure_account'), + text: t('step_indicator.flows.idv.re_enter_password'), ) expect(page).not_to have_css('.step-indicator__step--current') - expect(page).not_to have_content(t('step_indicator.flows.idv.get_a_letter')) + expect(page).not_to have_content(t('step_indicator.flows.idv.verify_address')) # Refreshing shows same page (BUT with new personal key, we should warn the user) visit current_path diff --git a/spec/features/idv/in_person_spec.rb b/spec/features/idv/in_person_spec.rb index 735a9410129..4010fb4deb7 100644 --- a/spec/features/idv/in_person_spec.rb +++ b/spec/features/idv/in_person_spec.rb @@ -83,29 +83,30 @@ # phone page expect_in_person_step_indicator_current_step( - t('step_indicator.flows.idv.verify_phone_or_address'), + t('step_indicator.flows.idv.verify_phone'), ) expect(page).to have_content(t('titles.idv.phone')) fill_out_phone_form_ok(MfaContext.new(user).phone_configurations.first.phone) click_idv_send_security_code expect_in_person_step_indicator_current_step( - t('step_indicator.flows.idv.verify_phone_or_address'), + t('step_indicator.flows.idv.verify_phone'), ) expect_in_person_step_indicator_current_step( - t('step_indicator.flows.idv.verify_phone_or_address'), + t('step_indicator.flows.idv.verify_phone'), ) fill_in_code_with_last_phone_otp click_submit_default # password confirm page - expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.secure_account')) + expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.re_enter_password')) expect(page).to have_content(t('idv.titles.session.enter_password', app_name: APP_NAME)) complete_enter_password_step(user) # personal key page - expect_in_person_step_indicator - expect(page).not_to have_css('.step-indicator__step--current') + expect_in_person_step_indicator_current_step( + t('step_indicator.flows.idv.go_to_the_post_office'), + ) expect(page).to have_content(t('titles.idv.personal_key')) deadline = nil freeze_time do @@ -274,14 +275,14 @@ begin_in_person_proofing complete_all_in_person_proofing_steps click_on t('idv.troubleshooting.options.verify_by_mail') - expect_in_person_step_indicator_current_step( - t('step_indicator.flows.idv.verify_phone_or_address'), + expect_in_person_gpo_step_indicator_current_step( + t('step_indicator.flows.idv.verify_address'), ) click_on t('idv.buttons.mail.send') - expect_in_person_gpo_step_indicator_current_step(t('step_indicator.flows.idv.get_a_letter')) + expect_in_person_gpo_step_indicator_current_step(t('step_indicator.flows.idv.verify_address')) complete_enter_password_step - expect_in_person_gpo_step_indicator_current_step(t('step_indicator.flows.idv.get_a_letter')) + expect_in_person_gpo_step_indicator_current_step(t('step_indicator.flows.idv.verify_address')) expect(page).to have_content(t('idv.titles.come_back_later')) expect(page).to have_current_path(idv_letter_enqueued_path) @@ -289,12 +290,11 @@ expect(page).to have_current_path(account_path) expect(page).not_to have_content(t('headings.account.verified_account')) click_on t('account.index.verification.reactivate_button') - expect_in_person_gpo_step_indicator_current_step(t('step_indicator.flows.idv.get_a_letter')) + expect_in_person_gpo_step_indicator_current_step(t('step_indicator.flows.idv.verify_address')) click_button t('idv.gpo.form.submit') # personal key - expect_in_person_step_indicator - expect(page).not_to have_css('.step-indicator__step--current') + expect_in_person_gpo_step_indicator_current_step(t('step_indicator.flows.idv.secure_account')) expect(page).to have_content(t('titles.idv.personal_key')) acknowledge_and_confirm_personal_key @@ -484,29 +484,30 @@ # phone page expect_in_person_step_indicator_current_step( - t('step_indicator.flows.idv.verify_phone_or_address'), + t('step_indicator.flows.idv.verify_phone'), ) expect(page).to have_content(t('titles.idv.phone')) fill_out_phone_form_ok(MfaContext.new(user).phone_configurations.first.phone) click_idv_send_security_code expect_in_person_step_indicator_current_step( - t('step_indicator.flows.idv.verify_phone_or_address'), + t('step_indicator.flows.idv.verify_phone'), ) expect_in_person_step_indicator_current_step( - t('step_indicator.flows.idv.verify_phone_or_address'), + t('step_indicator.flows.idv.verify_phone'), ) fill_in_code_with_last_phone_otp click_submit_default # password confirm page - expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.secure_account')) + expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.re_enter_password')) expect(page).to have_content(t('idv.titles.session.enter_password', app_name: APP_NAME)) complete_enter_password_step(user) # personal key page - expect_in_person_step_indicator - expect(page).not_to have_css('.step-indicator__step--current') + expect_in_person_step_indicator_current_step( + t('step_indicator.flows.idv.go_to_the_post_office'), + ) expect(page).to have_content(t('titles.idv.personal_key')) deadline = nil freeze_time do diff --git a/spec/features/idv/in_person_threatmetrix_spec.rb b/spec/features/idv/in_person_threatmetrix_spec.rb index b03e331366f..2eb65629dc0 100644 --- a/spec/features/idv/in_person_threatmetrix_spec.rb +++ b/spec/features/idv/in_person_threatmetrix_spec.rb @@ -108,29 +108,30 @@ def deactivate_profile_update_enrollment(status:) # phone page expect_in_person_step_indicator_current_step( - t('step_indicator.flows.idv.verify_phone_or_address'), + t('step_indicator.flows.idv.verify_phone'), ) expect(page).to have_content(t('titles.idv.phone')) fill_out_phone_form_ok(MfaContext.new(user).phone_configurations.first.phone) click_idv_send_security_code expect_in_person_step_indicator_current_step( - t('step_indicator.flows.idv.verify_phone_or_address'), + t('step_indicator.flows.idv.verify_phone'), ) expect_in_person_step_indicator_current_step( - t('step_indicator.flows.idv.verify_phone_or_address'), + t('step_indicator.flows.idv.verify_phone'), ) fill_in_code_with_last_phone_otp click_submit_default # password confirm page - expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.secure_account')) + expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.re_enter_password')) expect(page).to have_content(t('idv.titles.session.enter_password', app_name: APP_NAME)) complete_enter_password_step(user) # personal key page - expect_in_person_step_indicator - expect(page).not_to have_css('.step-indicator__step--current') + expect_in_person_step_indicator_current_step( + t('step_indicator.flows.idv.go_to_the_post_office'), + ) expect(page).to have_content(t('titles.idv.personal_key')) deadline = nil freeze_time do diff --git a/spec/features/idv/steps/enter_code_step_spec.rb b/spec/features/idv/steps/enter_code_step_spec.rb index 0c1d60940ed..f12874565c0 100644 --- a/spec/features/idv/steps/enter_code_step_spec.rb +++ b/spec/features/idv/steps/enter_code_step_spec.rb @@ -146,7 +146,7 @@ expect(page).to have_current_path(idv_personal_key_path) expect(page).to have_content(t('account.index.verification.success')) - expect(page).to have_content(t('step_indicator.flows.idv.get_a_letter')) + expect(page).to have_content(t('step_indicator.flows.idv.verify_address')) expect(profile.active).to be(true) expect(profile.deactivation_reason).to be(nil) 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 3ad6643edb1..187219c52c4 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 @@ -67,29 +67,32 @@ # phone page expect_in_person_step_indicator_current_step( - t('step_indicator.flows.idv.verify_phone_or_address'), + t('step_indicator.flows.idv.verify_phone'), ) expect(page).to have_content(t('titles.idv.phone')) fill_out_phone_form_ok(MfaContext.new(user).phone_configurations.first.phone) click_idv_send_security_code expect_in_person_step_indicator_current_step( - t('step_indicator.flows.idv.verify_phone_or_address'), + t('step_indicator.flows.idv.verify_phone'), ) expect_in_person_step_indicator_current_step( - t('step_indicator.flows.idv.verify_phone_or_address'), + t('step_indicator.flows.idv.verify_phone'), ) fill_in_code_with_last_phone_otp click_submit_default # password confirm page - expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.secure_account')) + expect_in_person_step_indicator_current_step( + t('step_indicator.flows.idv.re_enter_password'), + ) expect(page).to have_content(t('idv.titles.session.enter_password', app_name: APP_NAME)) complete_enter_password_step(user) # personal key page - expect_in_person_step_indicator - expect(page).not_to have_css('.step-indicator__step--current') + expect_in_person_step_indicator_current_step( + t('step_indicator.flows.idv.go_to_the_post_office'), + ) expect(page).to have_content(t('titles.idv.personal_key')) deadline = nil freeze_time do @@ -202,29 +205,30 @@ # phone page expect_in_person_step_indicator_current_step( - t('step_indicator.flows.idv.verify_phone_or_address'), + t('step_indicator.flows.idv.verify_phone'), ) expect(page).to have_content(t('titles.idv.phone')) fill_out_phone_form_ok(MfaContext.new(user).phone_configurations.first.phone) click_idv_send_security_code expect_in_person_step_indicator_current_step( - t('step_indicator.flows.idv.verify_phone_or_address'), + t('step_indicator.flows.idv.verify_phone'), ) expect_in_person_step_indicator_current_step( - t('step_indicator.flows.idv.verify_phone_or_address'), + t('step_indicator.flows.idv.verify_phone'), ) fill_in_code_with_last_phone_otp click_submit_default # password confirm page - expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.secure_account')) + expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.re_enter_password')) expect(page).to have_content(t('idv.titles.session.enter_password', app_name: APP_NAME)) complete_enter_password_step(user) # personal key page - expect_in_person_step_indicator - expect(page).not_to have_css('.step-indicator__step--current') + expect_in_person_step_indicator_current_step( + t('step_indicator.flows.idv.go_to_the_post_office'), + ) expect(page).to have_content(t('titles.idv.personal_key')) deadline = nil freeze_time do @@ -361,29 +365,30 @@ # phone page expect_in_person_step_indicator_current_step( - t('step_indicator.flows.idv.verify_phone_or_address'), + t('step_indicator.flows.idv.verify_phone'), ) expect(page).to have_content(t('titles.idv.phone')) fill_out_phone_form_ok(MfaContext.new(user).phone_configurations.first.phone) click_idv_send_security_code expect_in_person_step_indicator_current_step( - t('step_indicator.flows.idv.verify_phone_or_address'), + t('step_indicator.flows.idv.verify_phone'), ) expect_in_person_step_indicator_current_step( - t('step_indicator.flows.idv.verify_phone_or_address'), + t('step_indicator.flows.idv.verify_phone'), ) fill_in_code_with_last_phone_otp click_submit_default # password confirm page - expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.secure_account')) + expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.re_enter_password')) expect(page).to have_content(t('idv.titles.session.enter_password', app_name: APP_NAME)) complete_enter_password_step(user) # personal key page - expect_in_person_step_indicator - expect(page).not_to have_css('.step-indicator__step--current') + expect_in_person_step_indicator_current_step( + t('step_indicator.flows.idv.go_to_the_post_office'), + ) expect(page).to have_content(t('titles.idv.personal_key')) deadline = nil freeze_time do diff --git a/spec/features/load_testing/email_sign_up_spec.rb b/spec/features/load_testing/email_sign_up_spec.rb index f2b41c24919..62f3d1039de 100644 --- a/spec/features/load_testing/email_sign_up_spec.rb +++ b/spec/features/load_testing/email_sign_up_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'Email sign up', allowed_extra_analytics: [:*] do +RSpec.feature 'Email sign up' do scenario 'Load testing feature is on' do allow(IdentityConfig.store).to receive(:enable_load_testing_mode).and_return(true) email = 'test@example.com' diff --git a/spec/features/multiple_emails/email_management_spec.rb b/spec/features/multiple_emails/email_management_spec.rb index e0620dfd6c6..4018460e401 100644 --- a/spec/features/multiple_emails/email_management_spec.rb +++ b/spec/features/multiple_emails/email_management_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'managing email address', allowed_extra_analytics: [:*] do +RSpec.feature 'managing email address' do context 'show one email address if only one is configured' do scenario 'shows one email address for a user with only one' do user = create(:user, :fully_registered, :with_multiple_emails) diff --git a/spec/features/multiple_emails/reset_password_spec.rb b/spec/features/multiple_emails/reset_password_spec.rb index 0a2d5d64a66..9466e9c460d 100644 --- a/spec/features/multiple_emails/reset_password_spec.rb +++ b/spec/features/multiple_emails/reset_password_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe 'reset password with multiple emails', allowed_extra_analytics: [:*] do +RSpec.describe 'reset password with multiple emails' do scenario 'it sends the reset instruction to the email the user enters' do user = create(:user, :with_multiple_emails) email1, email2 = user.reload.email_addresses.map(&:email) diff --git a/spec/features/phone/add_phone_spec.rb b/spec/features/phone/add_phone_spec.rb index d45de1f5cb5..6d36aaa410b 100644 --- a/spec/features/phone/add_phone_spec.rb +++ b/spec/features/phone/add_phone_spec.rb @@ -222,17 +222,17 @@ expect(page).to have_content(t('errors.messages.invalid_recaptcha_token')) expect(fake_analytics).to have_logged_event( 'reCAPTCHA verify result received', - hash_including( - recaptcha_result: { - success: true, - score: 0.5, - errors: [], - reasons: [], - }, - evaluated_as_valid: false, - score_threshold: 0.6, - phone_country_code: 'AU', - ), + recaptcha_result: { + assessment_id: kind_of(String), + success: true, + score: 0.5, + errors: [], + reasons: [], + }, + evaluated_as_valid: false, + score_threshold: 0.6, + phone_country_code: 'AU', + form_class: 'RecaptchaMockForm', ) # Passing international should display OTP confirmation diff --git a/spec/features/phone/edit_phone_spec.rb b/spec/features/phone/edit_phone_spec.rb index b776d8420b3..bf47084f1e1 100644 --- a/spec/features/phone/edit_phone_spec.rb +++ b/spec/features/phone/edit_phone_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe 'editing a phone', allowed_extra_analytics: [:*] do +RSpec.describe 'editing a phone' do it 'allows a user to edit one of their phone numbers' do user = create(:user, :fully_registered) phone_configuration = user.phone_configurations.first diff --git a/spec/features/phone/remove_phone_spec.rb b/spec/features/phone/remove_phone_spec.rb index 9a688c1c8b7..292dc84e43f 100644 --- a/spec/features/phone/remove_phone_spec.rb +++ b/spec/features/phone/remove_phone_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'removing a phone number from an account', allowed_extra_analytics: [:*] do +RSpec.feature 'removing a phone number from an account' do scenario 'deleting a phone number' do user = create(:user, :fully_registered, :with_piv_or_cac) phone_configuration = user.phone_configurations.first diff --git a/spec/features/reports/monthly_gpo_letter_requests_report_spec.rb b/spec/features/reports/monthly_gpo_letter_requests_report_spec.rb index 4503e0ce51d..8498508cd2c 100644 --- a/spec/features/reports/monthly_gpo_letter_requests_report_spec.rb +++ b/spec/features/reports/monthly_gpo_letter_requests_report_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'Monthly gpo letter requests report', allowed_extra_analytics: [:*] do +RSpec.feature 'Monthly gpo letter requests report' do it 'runs when there are not entries' do results_hash = JSON.parse(Reports::MonthlyGpoLetterRequestsReport.new.perform(Time.zone.today)) expect(results_hash['total_letter_requests']).to eq(0) diff --git a/spec/features/saml/ial1/account_creation_spec.rb b/spec/features/saml/ial1/account_creation_spec.rb index 5db52287217..34f0d952511 100644 --- a/spec/features/saml/ial1/account_creation_spec.rb +++ b/spec/features/saml/ial1/account_creation_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'Canceling Account Creation', allowed_extra_analytics: [:*] do +RSpec.feature 'Canceling Account Creation' do include SamlAuthHelper context 'From the enter email page', email: true do diff --git a/spec/features/session/decryption_spec.rb b/spec/features/session/decryption_spec.rb index b961d076b03..6e63e80a5d1 100644 --- a/spec/features/session/decryption_spec.rb +++ b/spec/features/session/decryption_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'Session decryption', allowed_extra_analytics: [:*] do +RSpec.feature 'Session decryption' do context 'when there is a session decryption error' do it 'should raise an error and log the user out' do sign_in_and_2fa_user diff --git a/spec/features/session/timeout_spec.rb b/spec/features/session/timeout_spec.rb index 4468dd481b3..789db94b387 100644 --- a/spec/features/session/timeout_spec.rb +++ b/spec/features/session/timeout_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'Session Timeout', allowed_extra_analytics: [:*] do +RSpec.feature 'Session Timeout' do context 'when SP info no longer in session but request_id params exists' do it 'preserves the branded experience' do issuer = 'http://localhost:3000' diff --git a/spec/features/users/password_reset_with_pending_profile_spec.rb b/spec/features/users/password_reset_with_pending_profile_spec.rb index 5518b59ca71..84ab1af904d 100644 --- a/spec/features/users/password_reset_with_pending_profile_spec.rb +++ b/spec/features/users/password_reset_with_pending_profile_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'reset password with pending profile', allowed_extra_analytics: [:*] do +RSpec.feature 'reset password with pending profile' do include PersonalKeyHelper let(:user) { create(:user, :fully_registered) } diff --git a/spec/features/users/sign_out_spec.rb b/spec/features/users/sign_out_spec.rb index 10debc852a5..b23c74ac240 100644 --- a/spec/features/users/sign_out_spec.rb +++ b/spec/features/users/sign_out_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'Sign out', allowed_extra_analytics: [:*] do +RSpec.feature 'Sign out' do scenario 'user signs out successfully' do sign_in_and_2fa_user click_button(t('links.sign_out'), match: :first) diff --git a/spec/features/users/verify_profile_spec.rb b/spec/features/users/verify_profile_spec.rb index 1b950c4c84d..a834ab35928 100644 --- a/spec/features/users/verify_profile_spec.rb +++ b/spec/features/users/verify_profile_spec.rb @@ -21,7 +21,7 @@ it 'shows step indicator progress with current step' do sign_in_live_with_2fa(user) - expect_step_indicator_current_step(t('step_indicator.flows.idv.get_a_letter')) + expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_address')) end scenario 'valid OTP' do diff --git a/spec/features/visitors/email_language_preference_spec.rb b/spec/features/visitors/email_language_preference_spec.rb index dba37c98f64..cf45ee6d841 100644 --- a/spec/features/visitors/email_language_preference_spec.rb +++ b/spec/features/visitors/email_language_preference_spec.rb @@ -27,7 +27,7 @@ end end - it 'sends emails in the selected language', allowed_extra_analytics: [:*] do + it 'sends emails in the selected language' do email = 'test@example.com' visit sign_up_email_path diff --git a/spec/forms/idv/api_image_upload_form_spec.rb b/spec/forms/idv/api_image_upload_form_spec.rb index bf1e7cbfe56..87b2be2f466 100644 --- a/spec/forms/idv/api_image_upload_form_spec.rb +++ b/spec/forms/idv/api_image_upload_form_spec.rb @@ -19,7 +19,6 @@ service_provider: build(:service_provider, issuer: 'test_issuer'), analytics: fake_analytics, irs_attempts_api_tracker: irs_attempts_api_tracker, - store_encrypted_images: store_encrypted_images, liveness_checking_required: liveness_checking_required, ) end @@ -54,7 +53,6 @@ let(:document_capture_session_uuid) { document_capture_session.uuid } let(:fake_analytics) { FakeAnalytics.new } let(:irs_attempts_api_tracker) { IrsAttemptsApiTrackingHelper::FakeAttemptsTracker.new } - let(:store_encrypted_images) { false } describe '#valid?' do context 'with all valid images' do @@ -676,63 +674,6 @@ end end - describe 'encrypted document storage' do - context 'when encrypted image storage is enabled' do - let(:store_encrypted_images) { true } - - it 'writes encrypted documents' do - form.submit - - upload_events = irs_attempts_api_tracker.events[:idv_document_upload_submitted] - expect(upload_events).to have_attributes(length: 1) - upload_event = upload_events.first - - document_writer = form.send(:encrypted_document_storage_writer) - - front_image.rewind - back_image.rewind - - cipher = Encryption::AesCipher.new - - front_image_ciphertext = - document_writer.storage.read_image(name: upload_event[:document_front_image_filename]) - - back_image_ciphertext = - document_writer.storage.read_image(name: upload_event[:document_back_image_filename]) - - key = Base64.decode64(upload_event[:document_image_encryption_key]) - - expect(cipher.decrypt(front_image_ciphertext, key)).to eq(front_image.read) - expect(cipher.decrypt(back_image_ciphertext, key)).to eq(back_image.read) - end - end - - context 'when encrypted image storage is disabled' do - let(:store_encrypted_images) { false } - - it 'does not write images' do - document_writer = instance_double(EncryptedDocumentStorage::DocumentWriter) - allow(form).to receive(:encrypted_document_storage_writer).and_return(document_writer) - - expect(document_writer).to_not receive(:encrypt_and_write_document) - - form.submit - end - - it 'does not send image info to attempts api' do - expect(irs_attempts_api_tracker).to receive(:idv_document_upload_submitted).with( - hash_including( - document_front_image_filename: nil, - document_back_image_filename: nil, - document_image_encryption_key: nil, - ), - ) - - form.submit - end - end - end - describe 'image source' do let(:source) { nil } let(:front_image_metadata) do diff --git a/spec/forms/idv/phone_confirmation_otp_verification_form_spec.rb b/spec/forms/idv/phone_confirmation_otp_verification_form_spec.rb index e2bb44aa4ab..0071e983840 100644 --- a/spec/forms/idv/phone_confirmation_otp_verification_form_spec.rb +++ b/spec/forms/idv/phone_confirmation_otp_verification_form_spec.rb @@ -11,6 +11,7 @@ phone: phone, sent_at: phone_confirmation_otp_sent_at, delivery_method: :sms, + user: user, ) end let(:max_attempts) { 2 } diff --git a/spec/forms/new_phone_form_spec.rb b/spec/forms/new_phone_form_spec.rb index a27a231bd74..49e803504df 100644 --- a/spec/forms/new_phone_form_spec.rb +++ b/spec/forms/new_phone_form_spec.rb @@ -354,6 +354,10 @@ and_return(score_threshold) end + it 'assigns recaptcha_assessment_id value' do + expect { result }.to change { form.recaptcha_assessment_id }.from(nil).to kind_of(String) + end + context 'with invalid captcha score' do let(:recaptcha_mock_score) { score_threshold - 0.1 } @@ -380,22 +384,34 @@ let(:phone) { '3065550100' } let(:international_code) { 'CA' } let(:params) { super().merge(recaptcha_token:) } + let(:recaptcha_form_response) { FormResponse.new(success: true) } + let(:recaptcha_assessment_id) { 'projects/project-id/assessments/assessment-id' } subject(:result) { form.submit(params) } before do allow(FeatureManagement).to receive(:phone_recaptcha_enabled?).and_return(true) - allow(recaptcha_form).to receive(:submit).with(recaptcha_token) + allow(recaptcha_form).to receive(:submit).with(recaptcha_token). + and_return([recaptcha_form_response, recaptcha_assessment_id]) allow(recaptcha_form).to receive(:errors).and_return(errors) allow(form).to receive(:recaptcha_form).and_return(recaptcha_form) end context 'with valid recaptcha result' do + let(:recaptcha_form_response) { FormResponse.new(success: true) } + it 'is valid' do expect(result.success?).to eq(true) expect(result.errors).to be_blank end + it 'assigns recaptcha_assessment_id value' do + expect { result }. + to change { form.recaptcha_assessment_id }. + from(nil). + to(recaptcha_assessment_id) + end + context 'with recaptcha enterprise' do let(:recaptcha_form) do PhoneRecaptchaForm.new( diff --git a/spec/forms/recaptcha_enterprise_form_spec.rb b/spec/forms/recaptcha_enterprise_form_spec.rb index 0d134fa2c66..2e32736c455 100644 --- a/spec/forms/recaptcha_enterprise_form_spec.rb +++ b/spec/forms/recaptcha_enterprise_form_spec.rb @@ -53,19 +53,22 @@ describe '#submit' do let(:token) { nil } - subject(:response) { form.submit(token) } + subject(:result) { form.submit(token) } context 'with exemption' do before do allow(form).to receive(:exempt?).and_return(true) end - it 'is successful' do + it 'is successful without assessment id' do + response, assessment_id = result + expect(response.to_h).to eq(success: true) + expect(assessment_id).to be_nil end it 'does not log analytics' do - response + result expect(analytics).not_to have_logged_event('reCAPTCHA verify result received') end @@ -74,15 +77,18 @@ context 'with missing token' do let(:token) { nil } - it 'is unsuccessful with error for blank token' do + it 'is unsuccessful with nil assessment id and error for blank token' do + response, assessment_id = result + expect(response.to_h).to eq( success: false, error_details: { recaptcha_token: { blank: true } }, ) + expect(assessment_id).to be_nil end it 'does not log analytics' do - response + result expect(analytics).not_to have_logged_event('reCAPTCHA verify result received') end @@ -91,15 +97,18 @@ context 'with blank token' do let(:token) { '' } - it 'is unsuccessful with error for blank token' do + it 'is unsuccessful with nil assessment id and error for blank token' do + response, assessment_id = result + expect(response.to_h).to eq( success: false, error_details: { recaptcha_token: { blank: true } }, ) + expect(assessment_id).to be_nil end it 'does not log analytics' do - response + result expect(analytics).not_to have_logged_event('reCAPTCHA verify result received') end @@ -107,27 +116,32 @@ context 'with unsuccessful response from validation service' do let(:token) { 'token' } + let(:name) { 'projects/project-id/assessments/assessment-id' } before do stub_recaptcha_response( body: { tokenProperties: { valid: false, action:, invalidReason: 'EXPIRED' }, event: {}, + name:, }, action:, token:, ) end - it 'is unsuccessful with error for invalid token' do + it 'is unsuccessful with assessment id and error for invalid token' do + response, assessment_id = result + expect(response.to_h).to eq( success: false, error_details: { recaptcha_token: { invalid: true } }, ) + expect(assessment_id).to eq(name) end it 'logs analytics of the body' do - response + result expect(analytics).to have_logged_event( 'reCAPTCHA verify result received', @@ -136,6 +150,7 @@ score: nil, errors: [], reasons: ['EXPIRED'], + assessment_id: name, }, evaluated_as_valid: false, score_threshold: score_threshold, @@ -157,17 +172,21 @@ ) end - it 'is successful' do + it 'is successful with nil assessment id' do + response, assessment_id = result + expect(response.to_h).to eq(success: true) + expect(assessment_id).to be_nil end it 'logs analytics of the body' do - response + result expect(analytics).to have_logged_event( 'reCAPTCHA verify result received', recaptcha_result: { success: false, + assessment_id: nil, score: nil, errors: ['INVALID_ARGUMENT'], reasons: [], @@ -186,12 +205,15 @@ stub_request(:post, assessment_url).to_timeout end - it 'is successful' do + it 'is successful with nil assessment id' do + response, assessment_id = result + expect(response.to_h).to eq(success: true) + expect(assessment_id).to be_nil end it 'logs analytics of the body' do - response + result expect(analytics).to have_logged_event( 'reCAPTCHA verify result received', @@ -205,6 +227,7 @@ context 'with failing score from validation service' do let(:token) { 'token' } + let(:name) { 'projects/project-id/assessments/assessment-id' } let(:score) { score_threshold - 0.1 } before do @@ -213,21 +236,25 @@ tokenProperties: { valid: true, action: }, riskAnalysis: { score:, reasons: ['AUTOMATION'] }, event: {}, + name:, }, action:, token:, ) end - it 'is unsuccessful with error for invalid token' do + it 'is unsuccessful with assesment id and error for invalid token' do + response, assessment_id = result + expect(response.to_h).to eq( success: false, error_details: { recaptcha_token: { invalid: true } }, ) + expect(assessment_id).to eq(name) end it 'logs analytics of the body' do - response + result expect(analytics).to have_logged_event( 'reCAPTCHA verify result received', @@ -236,6 +263,7 @@ score:, reasons: ['AUTOMATION'], errors: [], + assessment_id: name, }, evaluated_as_valid: false, score_threshold: score_threshold, @@ -246,6 +274,7 @@ context 'with successful score from validation service' do let(:token) { 'token' } + let(:name) { 'projects/project-id/assessments/assessment-id' } let(:score) { score_threshold + 0.1 } around do |example| @@ -254,6 +283,7 @@ tokenProperties: { valid: true, action: }, riskAnalysis: { score:, reasons: ['LOW_CONFIDENCE'] }, event: {}, + name:, }, action:, token:, @@ -262,12 +292,15 @@ expect(stubbed_request).to have_been_made.once end - it 'is successful' do + it 'is successful with assessment id' do + response, assessment_id = result + expect(response.to_h).to eq(success: true) + expect(assessment_id).to eq(name) end it 'logs analytics of the body' do - response + result expect(analytics).to have_logged_event( 'reCAPTCHA verify result received', @@ -276,6 +309,7 @@ score:, reasons: ['LOW_CONFIDENCE'], errors: [], + assessment_id: name, }, evaluated_as_valid: true, score_threshold: score_threshold, @@ -284,23 +318,29 @@ end context 'with action mismatch' do + let(:name) { 'projects/project-id/assessments/assessment-id' } + before do stub_recaptcha_response( body: { tokenProperties: { valid: true, action: 'wrong' }, riskAnalysis: { score:, reasons: ['LOW_CONFIDENCE'] }, event: {}, + name:, }, action:, token:, ) end - it 'is unsuccessful with error for invalid token' do + it 'is unsuccessful with assessment id and error for invalid token' do + response, assessment_id = result + expect(response.to_h).to eq( success: false, error_details: { recaptcha_token: { invalid: true } }, ) + expect(assessment_id).to eq(name) end end @@ -308,7 +348,7 @@ let(:extra_analytics_properties) { { extra: true } } it 'logs analytics of the body' do - response + result expect(analytics).to have_logged_event( 'reCAPTCHA verify result received', @@ -317,6 +357,7 @@ score:, reasons: ['LOW_CONFIDENCE'], errors: [], + assessment_id: name, }, evaluated_as_valid: true, score_threshold: score_threshold, @@ -330,7 +371,7 @@ let(:analytics) { nil } it 'validates gracefully without analytics logging' do - response + result end end end diff --git a/spec/forms/recaptcha_form_spec.rb b/spec/forms/recaptcha_form_spec.rb index 7b5d1245247..db30028205f 100644 --- a/spec/forms/recaptcha_form_spec.rb +++ b/spec/forms/recaptcha_form_spec.rb @@ -33,7 +33,7 @@ describe '#submit' do let(:token) { nil } - subject(:response) { form.submit(token) } + subject(:result) { form.submit(token) } context 'with exemption' do before do @@ -41,11 +41,14 @@ end it 'is successful' do + response, assessment_id = result + expect(response.to_h).to eq(success: true) + expect(assessment_id).to be_nil end it 'does not log analytics' do - response + result expect(analytics).not_to have_logged_event('reCAPTCHA verify result received') end @@ -55,14 +58,17 @@ let(:token) { nil } it 'is unsuccessful with error for blank token' do + response, assessment_id = result + expect(response.to_h).to eq( success: false, error_details: { recaptcha_token: { blank: true } }, ) + expect(assessment_id).to be_nil end it 'does not log analytics' do - response + result expect(analytics).not_to have_logged_event('reCAPTCHA verify result received') end @@ -72,14 +78,17 @@ let(:token) { '' } it 'is unsuccessful with error for blank token' do + response, assessment_id = result + expect(response.to_h).to eq( success: false, error_details: { recaptcha_token: { blank: true } }, ) + expect(assessment_id).to be_nil end it 'does not log analytics' do - response + result expect(analytics).not_to have_logged_event('reCAPTCHA verify result received') end @@ -96,18 +105,22 @@ end it 'is unsuccessful with error for invalid token' do + response, assessment_id = result + expect(response.to_h).to eq( success: false, error_details: { recaptcha_token: { invalid: true } }, ) + expect(assessment_id).to be_nil end it 'logs analytics of the body' do - response + result expect(analytics).to have_logged_event( 'reCAPTCHA verify result received', recaptcha_result: { + assessment_id: nil, success: false, score: nil, reasons: ['timeout-or-duplicate'], @@ -129,15 +142,19 @@ end it 'is successful' do + response, assessment_id = result + expect(response.to_h).to eq(success: true) + expect(assessment_id).to be_nil end it 'logs analytics of the body' do - response + result expect(analytics).to have_logged_event( 'reCAPTCHA verify result received', recaptcha_result: { + assessment_id: nil, success: false, score: nil, errors: ['missing-input-secret'], @@ -159,15 +176,19 @@ end it 'is successful' do + response, assessment_id = result + expect(response.to_h).to eq(success: true) + expect(assessment_id).to be_nil end it 'logs analytics of the body' do - response + result expect(analytics).to have_logged_event( 'reCAPTCHA verify result received', recaptcha_result: { + assessment_id: nil, success: false, score: nil, errors: ['invalid-input-secret'], @@ -190,11 +211,14 @@ end it 'is successful' do + response, assessment_id = result + expect(response.to_h).to eq(success: true) + expect(assessment_id).to be_nil end it 'logs analytics of the body' do - response + result expect(analytics).to have_logged_event( 'reCAPTCHA verify result received', @@ -215,18 +239,22 @@ end it 'is unsuccessful with error for invalid token' do + response, assessment_id = result + expect(response.to_h).to eq( success: false, error_details: { recaptcha_token: { invalid: true } }, ) + expect(assessment_id).to be_nil end it 'logs analytics of the body' do - response + result expect(analytics).to have_logged_event( 'reCAPTCHA verify result received', recaptcha_result: { + assessment_id: nil, success: true, score:, errors: [], @@ -250,15 +278,19 @@ end it 'is successful' do + response, assessment_id = result + expect(response.to_h).to eq(success: true) + expect(assessment_id).to be_nil end it 'logs analytics of the body' do - response + result expect(analytics).to have_logged_event( 'reCAPTCHA verify result received', recaptcha_result: { + assessment_id: nil, success: true, score:, errors: [], @@ -274,11 +306,12 @@ let(:extra_analytics_properties) { { extra: true } } it 'logs analytics of the body' do - response + result expect(analytics).to have_logged_event( 'reCAPTCHA verify result received', recaptcha_result: { + assessment_id: nil, success: true, score:, errors: [], @@ -296,7 +329,7 @@ let(:analytics) { nil } it 'validates gracefully without analytics logging' do - response + result end end end diff --git a/spec/forms/recaptcha_mock_form_spec.rb b/spec/forms/recaptcha_mock_form_spec.rb index 4fc249809f7..3ee562f5ad2 100644 --- a/spec/forms/recaptcha_mock_form_spec.rb +++ b/spec/forms/recaptcha_mock_form_spec.rb @@ -14,24 +14,28 @@ describe '#submit' do let(:token) { 'token' } - subject(:response) { form.submit(token) } + subject(:result) { form.submit(token) } context 'with failing score from validation service' do let(:score) { score_threshold - 0.1 } it 'is unsuccessful with error for invalid token' do + response, assessment_id = result + expect(response.to_h).to eq( success: false, error_details: { recaptcha_token: { invalid: true } }, ) + expect(assessment_id).to be_kind_of(String) end it 'logs analytics of the body' do - response + result expect(analytics).to have_logged_event( 'reCAPTCHA verify result received', recaptcha_result: { + assessment_id: kind_of(String), success: true, score:, errors: [], @@ -49,15 +53,19 @@ let(:score) { score_threshold + 0.1 } it 'is successful' do + response, assessment_id = result + expect(response.to_h).to eq(success: true) + expect(assessment_id).to be_kind_of(String) end it 'logs analytics of the body' do - response + result expect(analytics).to have_logged_event( 'reCAPTCHA verify result received', recaptcha_result: { + assessment_id: kind_of(String), success: true, score:, errors: [], @@ -73,7 +81,7 @@ let(:analytics) { nil } it 'validates gracefully without analytics logging' do - response + result end end end diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb index 7380a505340..991dc4143b4 100644 --- a/spec/i18n_spec.rb +++ b/spec/i18n_spec.rb @@ -127,6 +127,8 @@ class BaseTask { key: 'notices.signed_up_but_unconfirmed.resend_confirmation_email', locales: %i[zh] }, { key: 'openid_connect.authorization.errors.no_valid_vtr', locales: %i[zh] }, { key: 'telephony.account_deleted_notice', locales: %i[zh] }, + { key: 'telephony.format_length.six', locales: %i[zh] }, + { key: 'telephony.format_length.ten', locales: %i[zh] }, { key: 'titles.idv.canceled', locales: %i[zh] }, { key: 'titles.piv_cac_setup.upsell', locales: %i[zh] }, { key: 'two_factor_authentication.auth_app.change_nickname', locales: %i[zh] }, diff --git a/spec/lib/feature_management_spec.rb b/spec/lib/feature_management_spec.rb index 937f6a93b1d..45654c30d00 100644 --- a/spec/lib/feature_management_spec.rb +++ b/spec/lib/feature_management_spec.rb @@ -513,16 +513,6 @@ it 'says to allow biometric requests' do expect(FeatureManagement.idv_allow_selfie_check?).to eq(true) end - - context 'in production' do - before do - allow(Identity::Hostdata).to receive(:env).and_return('prod') - end - - it 'says to block biometric requests' do - expect(FeatureManagement.idv_allow_selfie_check?).to eq(false) - end - end end end end diff --git a/spec/lib/reporting/identity_verification_report_spec.rb b/spec/lib/reporting/identity_verification_report_spec.rb index 2d7fede3de9..f7c5faa819a 100644 --- a/spec/lib/reporting/identity_verification_report_spec.rb +++ b/spec/lib/reporting/identity_verification_report_spec.rb @@ -57,7 +57,7 @@ allow(report).to receive(:cloudwatch_client).and_return(cloudwatch_client) end - # rubocop:enable Layout/LineLength + describe '#as_csv' do it 'renders a csv report' do expected_csv = [ @@ -67,9 +67,9 @@ [], ['Metric', '# of Users'], [], - ['Started IdV Verification', 5], - ['Submitted welcome page', 5], - ['Images uploaded', 5], + ['IDV started', 5], + ['Welcome Submitted', 5], + ['Image Submitted', 5], [], ['Workflow completed', 4], ['Workflow completed - Verified', 1], @@ -78,12 +78,17 @@ ['Workflow completed - In-Person Pending', 1], ['Workflow completed - Fraud Review Pending', 1], [], - ['Successfully verified', 4], - ['Successfully verified - Inline', 1], - ['Successfully verified - GPO Code Entry', 1], - ['Successfully verified - In Person', 1], - ['Successfully verified - Passed Fraud Review', 1], + ['Successfully Verified', 4], + ['Successfully Verified - With phone number', 1], + ['Successfully Verified - With mailed code', 1], + ['Successfully Verified - In Person', 1], + ['Successfully Verified - Passed fraud review', 1], + ['Blanket Proofing Rate (IDV Started to Successfully Verified)', 0.8], + ['Intent Proofing Rate (Welcome Submitted to Successfully Verified)', 0.8], + ['Actual Proofing Rate (Image Submitted to Successfully Verified)', 0.8], + ['Industry Proofing Rate (Verified minus IDV Rejected)', 0.8], ] + # rubocop:enable Layout/LineLength aggregate_failures do report.as_csv.zip(expected_csv).each do |actual, expected| @@ -104,9 +109,9 @@ [], ['Metric', '# of Users'], [], - ['Started IdV Verification', '5'], - ['Submitted welcome page', '5'], - ['Images uploaded', '5'], + ['IDV started', '5'], + ['Welcome Submitted', '5'], + ['Image Submitted', '5'], [], ['Workflow completed', '4'], ['Workflow completed - Verified', '1'], @@ -115,11 +120,15 @@ ['Workflow completed - In-Person Pending', '1'], ['Workflow completed - Fraud Review Pending', '1'], [], - ['Successfully verified', '4'], - ['Successfully verified - Inline', '1'], - ['Successfully verified - GPO Code Entry', '1'], - ['Successfully verified - In Person', '1'], - ['Successfully verified - Passed Fraud Review', '1'], + ['Successfully Verified', '4'], + ['Successfully Verified - With phone number', '1'], + ['Successfully Verified - With mailed code', '1'], + ['Successfully Verified - In Person', '1'], + ['Successfully Verified - Passed fraud review', '1'], + ['Blanket Proofing Rate (IDV Started to Successfully Verified)', '0.8'], + ['Intent Proofing Rate (Welcome Submitted to Successfully Verified)', '0.8'], + ['Actual Proofing Rate (Image Submitted to Successfully Verified)', '0.8'], + ['Industry Proofing Rate (Verified minus IDV Rejected)', '0.8'], ] aggregate_failures do diff --git a/spec/lib/reporting/proofing_rate_report_spec.rb b/spec/lib/reporting/proofing_rate_report_spec.rb index 352c5ee4b44..0cdf1ed0aa5 100644 --- a/spec/lib/reporting/proofing_rate_report_spec.rb +++ b/spec/lib/reporting/proofing_rate_report_spec.rb @@ -15,6 +15,10 @@ [ instance_double( 'Reporting::IdentityVerificationReport', + blanket_proofing_rates: 0.25, + intent_proofing_rates: 0.3333333333333333, + actual_proofing_rates: 0.5, + industry_proofing_rates: 0.5, idv_started: 4, idv_doc_auth_welcome_submitted: 3, idv_doc_auth_image_vendor_submitted: 2, @@ -25,6 +29,10 @@ ), instance_double( 'Reporting::IdentityVerificationReport', + blanket_proofing_rates: 0.4, + intent_proofing_rates: 0.5, + actual_proofing_rates: 0.6666666666666666, + industry_proofing_rates: 0.6666666666666666, idv_started: 5, idv_doc_auth_welcome_submitted: 4, idv_doc_auth_image_vendor_submitted: 3, @@ -35,6 +43,10 @@ ), instance_double( 'Reporting::IdentityVerificationReport', + blanket_proofing_rates: 0.5, + intent_proofing_rates: 0.6, + actual_proofing_rates: 0.75, + industry_proofing_rates: 0.75, idv_started: 6, idv_doc_auth_welcome_submitted: 5, idv_doc_auth_image_vendor_submitted: 4, diff --git a/spec/services/encrypted_document_storage/document_writer_spec.rb b/spec/services/encrypted_document_storage/document_writer_spec.rb deleted file mode 100644 index 4f410d2fc26..00000000000 --- a/spec/services/encrypted_document_storage/document_writer_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'rails_helper' - -RSpec.describe EncryptedDocumentStorage::DocumentWriter do - describe '#encrypt_and_write_document' do - it 'encrypts the document and writes it to storage' do - front_image = 'hello, i am the front image' - back_image = 'hello, i am the back image' - - result = EncryptedDocumentStorage::DocumentWriter.new.encrypt_and_write_document( - front_image: front_image, - front_image_content_type: 'image/jpeg', - back_image: back_image, - back_image_content_type: 'image/png', - ) - - front_filename = Rails.root.join('tmp', 'encrypted_doc_storage', result.front_filename) - back_filename = Rails.root.join('tmp', 'encrypted_doc_storage', result.back_filename) - key = Base64.strict_decode64(result.encryption_key) - - aes_cipher = Encryption::AesCipher.new - - written_front_image = aes_cipher.decrypt(File.read(front_filename), key) - written_back_image = aes_cipher.decrypt(File.read(back_filename), key) - - expect(written_front_image).to eq(front_image) - expect(written_back_image).to eq(back_image) - end - end - - describe '#build_filename_for_content_type' do - let(:filename) { described_class.new.build_filename_for_content_type(content_type) } - let(:content_type) { nil } - - describe 'extension assigning' do - subject { File.extname(filename) } - - context 'jpeg' do - let(:content_type) { 'image/jpeg' } - it { is_expected.to eql('.jpeg') } - end - - context 'png' do - let(:content_type) { 'image/png' } - it { is_expected.to eql('.png') } - end - - context 'nonsense' do - let(:content_type) { 'yabba/dabbadoo' } - it { is_expected.to eql('') } - end - - context nil do - it { is_expected.to eql('') } - end - end - end - - describe '#storage' do - subject { EncryptedDocumentStorage::DocumentWriter.new } - - context 'in production' do - it 'is uses S3' do - allow(Rails.env).to receive(:production?).and_return(true) - - expect(subject.storage).to be_a(EncryptedDocumentStorage::S3Storage) - end - end - - context 'outside production' do - it 'it uses the disk' do - allow(Rails.env).to receive(:production?).and_return(false) - - expect(subject.storage).to be_a(EncryptedDocumentStorage::LocalStorage) - end - end - end -end diff --git a/spec/services/encrypted_document_storage/local_storage_spec.rb b/spec/services/encrypted_document_storage/local_storage_spec.rb deleted file mode 100644 index bb1513a91fd..00000000000 --- a/spec/services/encrypted_document_storage/local_storage_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'rails_helper' - -RSpec.describe EncryptedDocumentStorage::LocalStorage do - describe '#write_image' do - it 'writes the document to the disk' do - encrypted_image = "hello, i'm the encrypted document." - name = SecureRandom.uuid - - EncryptedDocumentStorage::LocalStorage.new.write_image( - encrypted_image: encrypted_image, - name: name, - ) - - result = File.read( - Rails.root.join('tmp', 'encrypted_doc_storage', name), - ) - expect(result).to eq(encrypted_image) - end - end -end diff --git a/spec/services/encrypted_document_storage/s3_storage_spec.rb b/spec/services/encrypted_document_storage/s3_storage_spec.rb deleted file mode 100644 index 86b157f486f..00000000000 --- a/spec/services/encrypted_document_storage/s3_storage_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'rails_helper' - -RSpec.describe EncryptedDocumentStorage::S3Storage do - describe '#write_image' do - it 'writes the document to S3' do - encrypted_image = 'hello, i am the encrypted document.' - name = '123abc' - - storage = EncryptedDocumentStorage::S3Storage.new - - stubbed_s3_client = Aws::S3::Client.new(stub_responses: true) - allow(storage).to receive(:s3_client).and_return(stubbed_s3_client) - - expect(stubbed_s3_client).to receive(:put_object).and_call_original - stubbed_s3_client.stub_responses( - :put_object, - ->(context) { - params = context.params - expect(params[:bucket]).to eq(IdentityConfig.store.encrypted_document_storage_s3_bucket) - expect(params[:key]).to eq(name) - expect(params[:body]).to eq(encrypted_image) - }, - ) - - storage.write_image(encrypted_image: encrypted_image, name: name) - end - end -end diff --git a/spec/services/idv/phone_confirmation_session_spec.rb b/spec/services/idv/phone_confirmation_session_spec.rb index 1c2ceb9d648..3b883084853 100644 --- a/spec/services/idv/phone_confirmation_session_spec.rb +++ b/spec/services/idv/phone_confirmation_session_spec.rb @@ -1,14 +1,19 @@ require 'rails_helper' RSpec.describe Idv::PhoneConfirmationSession do + let(:user) { create(:user) } + let(:six_char_alphanumeric) { /[a-z0-9]{6}/i } + let(:ten_digit_numeric) { /[0-9]{10}/i } + describe '.start' do it 'starts a session for voice' do result = described_class.start( delivery_method: 'voice', phone: '+1 (202) 123-4567', + user: user, ) - expect(result.code).to match(/[a-z0-9]{6}/i) + expect(result.code).to match(six_char_alphanumeric) expect(result.phone).to eq('+1 (202) 123-4567') expect(result.sent_at).to be_within(1.second).of(Time.zone.now) expect(result.delivery_method).to eq(:voice) @@ -20,9 +25,10 @@ result = described_class.start( delivery_method: 'sms', phone: '+1 (202) 123-4567', + user: user, ) - expect(result.code).to match(/[a-z0-9]{6}/i) + expect(result.code).to match(six_char_alphanumeric) expect(result.phone).to eq('+1 (202) 123-4567') expect(result.sent_at).to be_within(1.second).of(Time.zone.now) expect(result.delivery_method).to eq(:sms) @@ -31,11 +37,73 @@ end end + describe '.generate_code' do + let(:ab_test_enabled) { false } + before do + allow(IdentityConfig.store).to receive(:ab_testing_idv_ten_digit_otp_enabled). + and_return(ab_test_enabled) + end + + context 'A/B test not enabled' do + it 'generates a six-character alphanumeric code' do + code = described_class.generate_code(user: user, delivery_method: :voice) + + expect(code).to match(six_char_alphanumeric) + end + end + context '10-digit A/B test enabled' do + let(:ab_test_enabled) { true } + + context '10-digit A/B test puts user in :six_alphanumeric_otp bucket' do + before do + stub_const( + 'AbTests::IDV_TEN_DIGIT_OTP', + FakeAbTestBucket.new.tap { |ab| ab.assign(user.uuid => :six_alphanumeric_otp) }, + ) + end + + it 'generates a six-character alphanumeric code for sms' do + code = described_class.generate_code(user: user, delivery_method: :sms) + + expect(code).to match(six_char_alphanumeric) + end + + it 'generates a six-character alphanumeric code for voice' do + code = described_class.generate_code(user: user, delivery_method: :voice) + + expect(code).to match(six_char_alphanumeric) + end + end + + context '10-digit A/B test puts user in :ten_digit_otp bucket' do + before do + stub_const( + 'AbTests::IDV_TEN_DIGIT_OTP', + FakeAbTestBucket.new.tap { |ab| ab.assign(user.uuid => :ten_digit_otp) }, + ) + end + + it 'generates a six-character alphanumeric code for sms' do + code = described_class.generate_code(user: user, delivery_method: :sms) + + expect(code).to match(six_char_alphanumeric) + end + + it 'generates a ten-digit numeric code for voice' do + code = described_class.generate_code(user: user, delivery_method: :voice) + + expect(code).to match(ten_digit_numeric) + end + end + end + end + describe '#regenerate_otp' do it 'returns a copy with a new OTP and expiration' do original_session = described_class.start( delivery_method: 'sms', phone: '+1 (202) 123-4567', + user: user, ) new_session = original_session.regenerate_otp @@ -57,6 +125,7 @@ phone: '+1 (202) 123-4567', sent_at: Time.zone.now, delivery_method: :sms, + user: user, ) end @@ -94,7 +163,11 @@ describe '#expired?' do it 'returns false if the OTP is not expired' do - otp_object = described_class.start(phone: '+1 (225) 123-4567', delivery_method: :sms) + otp_object = described_class.start( + phone: '+1 (225) 123-4567', + delivery_method: :sms, + user: user, + ) expect(otp_object.expired?).to eq(false) @@ -104,11 +177,36 @@ end it 'returns true if the OTP is expired' do - otp_object = described_class.start(phone: '+1 (225) 123-4567', delivery_method: :sms) + otp_object = described_class.start( + phone: '+1 (225) 123-4567', + delivery_method: :sms, + user: user, + ) travel_to 11.minutes.from_now do expect(otp_object.expired?).to eq(true) end end end + + describe '#to_h and .from_h' do + let(:test_session) do + described_class.new( + code: 'ABC', + phone: '4105551212', + sent_at: Time.zone.now, + delivery_method: :sms, + user: user, + ) + end + + it 'correctly restores the phone confirmation session from hash' do + deserialized_session = described_class.from_h(test_session.to_h) + expect(deserialized_session.code).to eq(test_session.code) + expect(deserialized_session.phone).to eq(test_session.phone) + expect(deserialized_session.sent_at).to be_within(1).of(test_session.sent_at) + expect(deserialized_session.delivery_method).to eq(test_session.delivery_method) + expect(deserialized_session.user.id).to eq(user.id) + end + end end diff --git a/spec/services/idv/send_phone_confirmation_otp_spec.rb b/spec/services/idv/send_phone_confirmation_otp_spec.rb index 4fd7b6dc590..78d10f72e1e 100644 --- a/spec/services/idv/send_phone_confirmation_otp_spec.rb +++ b/spec/services/idv/send_phone_confirmation_otp_spec.rb @@ -11,6 +11,7 @@ phone: phone, sent_at: Time.zone.now, delivery_method: delivery_preference, + user: user, ) end let(:idv_session) do @@ -60,6 +61,7 @@ expiration: 10, channel: :sms, otp_format: 'character', + otp_length: '6', domain: IdentityConfig.store.domain_name, country_code: 'US', extra_metadata: { @@ -93,6 +95,7 @@ expiration: 10, channel: :voice, otp_format: 'character', + otp_length: '6', domain: IdentityConfig.store.domain_name, country_code: 'US', extra_metadata: { diff --git a/spec/services/recaptcha_annotator_spec.rb b/spec/services/recaptcha_annotator_spec.rb new file mode 100644 index 00000000000..10c6a5a1e0b --- /dev/null +++ b/spec/services/recaptcha_annotator_spec.rb @@ -0,0 +1,131 @@ +require 'rails_helper' + +RSpec.describe RecaptchaAnnotator do + let(:recaptcha_enterprise_api_key) { 'recaptcha_enterprise_api_key' } + let(:recaptcha_enterprise_project_id) { 'project_id' } + let(:assessment_id) { "projects/#{recaptcha_enterprise_project_id}/assessments/assessment-id" } + let(:analytics) { FakeAnalytics.new } + let(:annotation_url) do + format( + '%{base_endpoint}/%{assessment_id}:annotate?key=%{api_key}', + base_endpoint: RecaptchaAnnotator::BASE_ENDPOINT, + assessment_id:, + api_key: recaptcha_enterprise_api_key, + ) + end + + describe '#annotate' do + let(:reason) { RecaptchaAnnotator::AnnotationReasons::INITIATED_TWO_FACTOR } + let(:annotation) { RecaptchaAnnotator::Annotations::LEGITIMATE } + subject(:annotate) { RecaptchaAnnotator.annotate(assessment_id:, reason:, annotation:) } + + context 'without recaptcha enterprise' do + before do + allow(FeatureManagement).to receive(:recaptcha_enterprise?).and_return(false) + end + + it 'does not submit annotation' do + annotate + + expect(WebMock).not_to have_requested(:post, annotation_url) + end + + it 'returns a hash describing annotation' do + expect(annotate).to eq( + assessment_id:, + reason:, + annotation:, + ) + end + + context 'with nil assessment id' do + let(:assessment_id) { nil } + + it { expect(annotate).to be_nil } + end + end + + context 'with recaptcha enterprise' do + before do + allow(FeatureManagement).to receive(:recaptcha_enterprise?).and_return(true) + allow(IdentityConfig.store).to receive(:recaptcha_enterprise_project_id). + and_return(recaptcha_enterprise_project_id) + allow(IdentityConfig.store).to receive(:recaptcha_enterprise_api_key). + and_return(recaptcha_enterprise_api_key) + stub_request(:post, annotation_url). + with do |req| + parsed_body = JSON.parse(req.body) + next if reason && parsed_body['reasons'] != [reason.to_s] + next if !reason && parsed_body.key?('reasons') + next if annotation && parsed_body['annotation'] != annotation.to_s + true + end. + to_return(headers: { 'Content-Type': 'application/json' }, body: '{}') + end + + it 'submits annotation' do + annotate + + expect(WebMock).to have_requested(:post, annotation_url) + end + + it 'logs analytics' do + annotate + + expect(annotate).to eq( + assessment_id:, + reason:, + annotation:, + ) + end + + context 'with an optional argument omitted' do + let(:annotation) { nil } + subject(:annotate) { RecaptchaAnnotator.annotate(assessment_id:, reason:) } + + it 'submits only what is provided' do + annotate + + expect(WebMock).to have_requested(:post, annotation_url). + with(body: { reasons: [reason] }.to_json) + end + + it 'returns a hash describing annotation' do + expect(annotate).to eq( + assessment_id:, + reason:, + annotation:, + ) + end + end + + context 'with nil assessment id' do + let(:assessment_id) { nil } + + it 'does not submit annotation' do + annotate + + expect(WebMock).not_to have_requested(:post, annotation_url) + end + + it { expect(annotate).to be_nil } + end + + context 'with connection error' do + before do + stub_request(:post, annotation_url).to_timeout + end + + it 'fails gracefully' do + annotate + end + + it 'notices the error to NewRelic' do + expect(NewRelic::Agent).to receive(:notice_error).with(Faraday::Error) + + annotate + end + end + end + end +end diff --git a/spec/support/features/in_person_helper.rb b/spec/support/features/in_person_helper.rb index 1a6c4712d3a..325bab5eb46 100644 --- a/spec/support/features/in_person_helper.rb +++ b/spec/support/features/in_person_helper.rb @@ -198,7 +198,7 @@ def expect_in_person_gpo_step_indicator_current_step(text) # Ensure that GPO letter step is shown in the step indicator. expect(page).to have_css( '.step-indicator__step', - text: t('step_indicator.flows.idv.get_a_letter'), + text: t('step_indicator.flows.idv.verify_address'), ) expect_in_person_step_indicator_current_step(text) diff --git a/spec/support/idv_examples/verification_code_entry.rb b/spec/support/idv_examples/verification_code_entry.rb index 766d4889a3a..6a8ef4a4ec5 100644 --- a/spec/support/idv_examples/verification_code_entry.rb +++ b/spec/support/idv_examples/verification_code_entry.rb @@ -53,7 +53,7 @@ expect(GpoConfirmationCode.count).to eq(1) click_on t('idv.messages.gpo.resend') - expect_step_indicator_current_step(t('step_indicator.flows.idv.get_a_letter')) + expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_address')) click_on t('idv.gpo.request_another_letter.button') diff --git a/spec/views/idv/by_mail/letter_enqueued/show.html.erb_spec.rb b/spec/views/idv/by_mail/letter_enqueued/show.html.erb_spec.rb index 2b5557744e5..62c6f2150ae 100644 --- a/spec/views/idv/by_mail/letter_enqueued/show.html.erb_spec.rb +++ b/spec/views/idv/by_mail/letter_enqueued/show.html.erb_spec.rb @@ -16,7 +16,7 @@ render expect(rendered).to have_link( t('idv.cancel.actions.exit', app_name: APP_NAME), - href: return_to_sp_cancel_path(step: :get_a_letter, location: :come_back_later), + href: return_to_sp_cancel_path(step: :verify_address, location: :come_back_later), ) end @@ -62,7 +62,7 @@ expect(view.content_for(:pre_flash_content)).to have_css( '.step-indicator__step--current', - text: t('step_indicator.flows.idv.get_a_letter'), + text: t('step_indicator.flows.idv.verify_address'), ) end end diff --git a/spec/views/idv/enter_password/new.html.erb_spec.rb b/spec/views/idv/enter_password/new.html.erb_spec.rb index c309a88209d..faf0034e1e0 100644 --- a/spec/views/idv/enter_password/new.html.erb_spec.rb +++ b/spec/views/idv/enter_password/new.html.erb_spec.rb @@ -11,7 +11,7 @@ allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:step_indicator_steps). and_return(Idv::StepIndicatorConcern::STEP_INDICATOR_STEPS) - allow(view).to receive(:step_indicator_step).and_return(:secure_account) + allow(view).to receive(:step_indicator_step).and_return(:re_enter_password) end context 'user goes through phone finder' do @@ -34,7 +34,7 @@ it 'shows the step indicator' do expect(view.content_for(:pre_flash_content)).to have_css( '.step-indicator__step--current', - text: t('step_indicator.flows.idv.secure_account'), + text: t('step_indicator.flows.idv.re_enter_password'), ) end end diff --git a/spec/views/idv/please_call/show.html.erb_spec.rb b/spec/views/idv/please_call/show.html.erb_spec.rb index ae2461a1488..2b1df4f41b3 100644 --- a/spec/views/idv/please_call/show.html.erb_spec.rb +++ b/spec/views/idv/please_call/show.html.erb_spec.rb @@ -14,7 +14,7 @@ it 'shows step indicator with pending status on secure account' do expect(view.content_for(:pre_flash_content)).to have_css( '.step-indicator__step--current', - text: t('step_indicator.flows.idv.secure_account'), + text: t('step_indicator.flows.idv.re_enter_password'), ) end diff --git a/spec/views/idv/shared/_error.html.erb_spec.rb b/spec/views/idv/shared/_error.html.erb_spec.rb index f9267545588..20a0fdf5a10 100644 --- a/spec/views/idv/shared/_error.html.erb_spec.rb +++ b/spec/views/idv/shared/_error.html.erb_spec.rb @@ -177,7 +177,7 @@ end context 'current_step provided' do - let(:current_step) { :verify_phone_or_address } + let(:current_step) { :verify_phone } it 'does not render a step indicator' do expect(view.content_for(:pre_flash_content)).not_to have_css('lg-step-indicator') @@ -192,7 +192,7 @@ it 'selects the correct step' do expect(view.content_for(:pre_flash_content)).to have_css( '.step-indicator__step--current .step-indicator__step-title', - text: t('step_indicator.flows.idv.verify_phone_or_address'), + text: t('step_indicator.flows.idv.verify_phone'), ) end end diff --git a/spec/views/sign_up/completions/show.html.erb_spec.rb b/spec/views/sign_up/completions/show.html.erb_spec.rb index facb9629633..41938c7ff20 100644 --- a/spec/views/sign_up/completions/show.html.erb_spec.rb +++ b/spec/views/sign_up/completions/show.html.erb_spec.rb @@ -55,7 +55,7 @@ render expect(rendered).to have_link( t('links.cancel'), - href: return_to_sp_cancel_path(step: :sign_up), + href: sign_up_completed_cancel_path, ) end diff --git a/yarn.lock b/yarn.lock index a529b51a1e1..a7b39fd89e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@18f/identity-design-system@^9.1.0": - version "9.1.0" - resolved "https://registry.yarnpkg.com/@18f/identity-design-system/-/identity-design-system-9.1.0.tgz#745a6b18ea30fe6fd7f18ebcaca777a5d51bf139" - integrity sha512-8eQBP6fHi+0MQYJu7YCF7sPtxgwDvTjnhZ2vL4XDIgjMknDOOZeyUmPrhjsnT2pAXlE11hS/eBfUm7KE8rifKw== +"@18f/identity-design-system@^9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@18f/identity-design-system/-/identity-design-system-9.2.0.tgz#36f1e4c4c68cae52c0cd4d5256ac33e6ef872763" + integrity sha512-gzzcRtxRPKdxcbdgYKBS+IEmBielCwbxB9KkUTwNyXTqDMJoWmscSODPEpmegIEB8Tg/LXwxJQfr+LyEePYewQ== dependencies: "@types/uswds__uswds" "^3.8.0" "@uswds/uswds" "^3.8.0" @@ -4665,10 +4665,10 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -libphonenumber-js@^1.10.61: - version "1.10.61" - resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.61.tgz#efd350a6283e5d6a804f0cd17dae1f563410241d" - integrity sha512-TsQsyzDttDvvzWNkbp/i0fVbzTGJIG0mUu/uNalIaRQEYeJxVQ/FPg+EJgSqfSXezREjM0V3RZ8cLVsKYhhw0Q== +libphonenumber-js@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.11.1.tgz#2596683e1876bfee74082bb49339fe0a85ae34f9" + integrity sha512-Wze1LPwcnzvcKGcRHFGFECTaLzxOtujwpf924difr5zniyYv1C2PiW0419qDR7m8lKDxsImu5mwxFuXhXpjmvw== lightningcss-darwin-arm64@1.23.0: version "1.23.0" @@ -6467,6 +6467,21 @@ stylelint-config-recommended@^14.0.0: resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-14.0.0.tgz#b395c7014838d2aaca1755eebd914d0bb5274994" integrity sha512-jSkx290CglS8StmrLp2TxAppIajzIBZKYm3IxT89Kg6fGlxbPiTiyH9PS5YUuVAFwaJLl1ikiXX0QWjI0jmgZQ== +stylelint-config-standard-scss@^13.1.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/stylelint-config-standard-scss/-/stylelint-config-standard-scss-13.1.0.tgz#2be36ca13087325a42c1f26df8267808667cc886" + integrity sha512-Eo5w7/XvwGHWkeGLtdm2FZLOMYoZl1omP2/jgFCXyl2x5yNz7/8vv4Tj6slHvMSSUNTaGoam/GAZ0ZhukvalfA== + dependencies: + stylelint-config-recommended-scss "^14.0.0" + stylelint-config-standard "^36.0.0" + +stylelint-config-standard@^36.0.0: + version "36.0.0" + resolved "https://registry.yarnpkg.com/stylelint-config-standard/-/stylelint-config-standard-36.0.0.tgz#6704c044d611edc12692d4a5e37b039a441604d4" + integrity sha512-3Kjyq4d62bYFp/Aq8PMKDwlgUyPU4nacXsjDLWJdNPRUgpuxALu1KnlAHIj36cdtxViVhXexZij65yM0uNIHug== + dependencies: + stylelint-config-recommended "^14.0.0" + stylelint-prettier@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/stylelint-prettier/-/stylelint-prettier-5.0.0.tgz#515a87800228f6bea603966462f7119755ee9b82"