diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index e5e0acd3c05..1f4413fa60a 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,8 +1,15 @@
version: 2
updates:
- - package-ecosystem: 'npm'
- directory: '/'
+ - package-ecosystem: npm
+ directory: /
schedule:
- interval: 'daily'
+ interval: daily
allow:
- dependency-name: '@18f/identity-design-system'
+ - dependency-name: libphonenumber-js
+ - package-ecosystem: bundler
+ directory: /
+ schedule:
+ interval: daily
+ allow:
+ - dependency-name: phonelib
diff --git a/Gemfile.lock b/Gemfile.lock
index a5314280bef..cc016fe2adb 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -60,70 +60,70 @@ GIT
GEM
remote: https://rubygems.org/
specs:
- actioncable (7.0.7)
- actionpack (= 7.0.7)
- activesupport (= 7.0.7)
+ actioncable (7.0.7.2)
+ actionpack (= 7.0.7.2)
+ activesupport (= 7.0.7.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
- actionmailbox (7.0.7)
- actionpack (= 7.0.7)
- activejob (= 7.0.7)
- activerecord (= 7.0.7)
- activestorage (= 7.0.7)
- activesupport (= 7.0.7)
+ actionmailbox (7.0.7.2)
+ actionpack (= 7.0.7.2)
+ activejob (= 7.0.7.2)
+ activerecord (= 7.0.7.2)
+ activestorage (= 7.0.7.2)
+ activesupport (= 7.0.7.2)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
- actionmailer (7.0.7)
- actionpack (= 7.0.7)
- actionview (= 7.0.7)
- activejob (= 7.0.7)
- activesupport (= 7.0.7)
+ actionmailer (7.0.7.2)
+ actionpack (= 7.0.7.2)
+ actionview (= 7.0.7.2)
+ activejob (= 7.0.7.2)
+ activesupport (= 7.0.7.2)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
- actionpack (7.0.7)
- actionview (= 7.0.7)
- activesupport (= 7.0.7)
+ actionpack (7.0.7.2)
+ actionview (= 7.0.7.2)
+ activesupport (= 7.0.7.2)
rack (~> 2.0, >= 2.2.4)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
- actiontext (7.0.7)
- actionpack (= 7.0.7)
- activerecord (= 7.0.7)
- activestorage (= 7.0.7)
- activesupport (= 7.0.7)
+ actiontext (7.0.7.2)
+ actionpack (= 7.0.7.2)
+ activerecord (= 7.0.7.2)
+ activestorage (= 7.0.7.2)
+ activesupport (= 7.0.7.2)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
- actionview (7.0.7)
- activesupport (= 7.0.7)
+ actionview (7.0.7.2)
+ activesupport (= 7.0.7.2)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
- activejob (7.0.7)
- activesupport (= 7.0.7)
+ activejob (7.0.7.2)
+ activesupport (= 7.0.7.2)
globalid (>= 0.3.6)
- activemodel (7.0.7)
- activesupport (= 7.0.7)
- activerecord (7.0.7)
- activemodel (= 7.0.7)
- activesupport (= 7.0.7)
+ activemodel (7.0.7.2)
+ activesupport (= 7.0.7.2)
+ activerecord (7.0.7.2)
+ activemodel (= 7.0.7.2)
+ activesupport (= 7.0.7.2)
activerecord-postgis-adapter (8.0.2)
activerecord (~> 7.0.0)
rgeo-activerecord (~> 7.0.0)
- activestorage (7.0.7)
- actionpack (= 7.0.7)
- activejob (= 7.0.7)
- activerecord (= 7.0.7)
- activesupport (= 7.0.7)
+ activestorage (7.0.7.2)
+ actionpack (= 7.0.7.2)
+ activejob (= 7.0.7.2)
+ activerecord (= 7.0.7.2)
+ activesupport (= 7.0.7.2)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
- activesupport (7.0.7)
+ activesupport (7.0.7.2)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@@ -443,7 +443,7 @@ GEM
pg (1.5.3)
pg_query (4.2.3)
google-protobuf (>= 3.22.3)
- phonelib (0.8.2)
+ phonelib (0.8.3)
pkcs11 (0.3.4)
premailer (1.21.0)
addressable
@@ -492,20 +492,20 @@ GEM
rack_session_access (0.2.0)
builder (>= 2.0.0)
rack (>= 1.0.0)
- rails (7.0.7)
- actioncable (= 7.0.7)
- actionmailbox (= 7.0.7)
- actionmailer (= 7.0.7)
- actionpack (= 7.0.7)
- actiontext (= 7.0.7)
- actionview (= 7.0.7)
- activejob (= 7.0.7)
- activemodel (= 7.0.7)
- activerecord (= 7.0.7)
- activestorage (= 7.0.7)
- activesupport (= 7.0.7)
+ rails (7.0.7.2)
+ actioncable (= 7.0.7.2)
+ actionmailbox (= 7.0.7.2)
+ actionmailer (= 7.0.7.2)
+ actionpack (= 7.0.7.2)
+ actiontext (= 7.0.7.2)
+ actionview (= 7.0.7.2)
+ activejob (= 7.0.7.2)
+ activemodel (= 7.0.7.2)
+ activerecord (= 7.0.7.2)
+ activestorage (= 7.0.7.2)
+ activesupport (= 7.0.7.2)
bundler (>= 1.15.0)
- railties (= 7.0.7)
+ railties (= 7.0.7.2)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@@ -520,9 +520,9 @@ GEM
rails-i18n (7.0.6)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8)
- railties (7.0.7)
- actionpack (= 7.0.7)
- activesupport (= 7.0.7)
+ railties (7.0.7.2)
+ actionpack (= 7.0.7.2)
+ activesupport (= 7.0.7.2)
method_source
rake (>= 12.2)
thor (~> 1.0)
diff --git a/app/components/page_footer_component.rb b/app/components/page_footer_component.rb
index 3c05b57e5f5..9cb063b3aaf 100644
--- a/app/components/page_footer_component.rb
+++ b/app/components/page_footer_component.rb
@@ -10,6 +10,6 @@ def call
end
def css_class
- ['margin-top-4 padding-top-2 border-top border-primary-light', *tag_options[:class]]
+ ['page-footer margin-top-4 padding-top-2 border-top border-primary-light', *tag_options[:class]]
end
end
diff --git a/app/controllers/concerns/forced_reauthentication_concern.rb b/app/controllers/concerns/forced_reauthentication_concern.rb
new file mode 100644
index 00000000000..470d16de6c2
--- /dev/null
+++ b/app/controllers/concerns/forced_reauthentication_concern.rb
@@ -0,0 +1,19 @@
+# This module defines an interface for storing when an issuer has forced re-authentication
+# for an active session. A request to force re-authentication that does not result
+# in the user needing to re-authenticate due to not being authenticated should be excluded.
+
+module ForcedReauthenticationConcern
+ def issuer_forced_reauthentication?(issuer:)
+ session.dig(:forced_reauthentication_sps, issuer) == true
+ end
+
+ def set_issuer_forced_reauthentication(issuer:, is_forced_reauthentication:)
+ if is_forced_reauthentication
+ session[:forced_reauthentication_sps] ||= {}
+ session[:forced_reauthentication_sps][issuer] = true
+ elsif session[:forced_reauthentication_sps]
+ session[:forced_reauthentication_sps].delete(issuer)
+ session.delete(:forced_reauthentication_sps) if session[:forced_reauthentication_sps].blank?
+ end
+ 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 874523a3503..aad080e0494 100644
--- a/app/controllers/concerns/idv/ab_test_analytics_concern.rb
+++ b/app/controllers/concerns/idv/ab_test_analytics_concern.rb
@@ -4,7 +4,12 @@ module AbTestAnalyticsConcern
include Idv::GettingStartedAbTestConcern
def ab_test_analytics_buckets
- acuant_sdk_ab_test_analytics_args.
+ buckets = {}
+ if defined?(idv_session)
+ buckets[:skip_hybrid_handoff] = idv_session&.skip_hybrid_handoff
+ end
+
+ buckets.merge(acuant_sdk_ab_test_analytics_args).
merge(getting_started_ab_test_analytics_bucket)
end
end
diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb
index 38593738669..be6c37b50e2 100644
--- a/app/controllers/concerns/idv/verify_info_concern.rb
+++ b/app/controllers/concerns/idv/verify_info_concern.rb
@@ -30,8 +30,7 @@ def shared_update
should_proof_state_id: should_use_aamva?(pii),
trace_id: amzn_trace_id,
user_id: current_user.id,
- threatmetrix_session_id:
- idv_session.threatmetrix_session_id || flow_session[:threatmetrix_session_id],
+ threatmetrix_session_id: idv_session.threatmetrix_session_id,
request_ip: request.remote_ip,
double_address_verification: capture_secondary_id_enabled,
)
@@ -195,8 +194,9 @@ def async_state_done(current_async_state)
address_edited: !!(idv_session.address_edited || flow_session['address_edited']),
address_line2_present: !pii[:address2].blank?,
pii_like_keypaths: [[:errors, :ssn], [:response_body, :first_name],
+ [:same_address_as_id],
[:state_id, :state_id_jurisdiction]],
- }.merge(ab_test_analytics_buckets),
+ },
)
log_idv_verification_submitted_event(
success: form_response.success?,
@@ -219,7 +219,7 @@ def async_state_done(current_async_state)
idv_session.invalidate_verify_info_step!
end
- analytics.idv_doc_auth_verify_proofing_results(**form_response.to_h)
+ analytics.idv_doc_auth_verify_proofing_results(**analytics_arguments, **form_response.to_h)
end
def next_step_url
diff --git a/app/controllers/concerns/saml_idp_auth_concern.rb b/app/controllers/concerns/saml_idp_auth_concern.rb
index a2db3a10958..c1b426680f1 100644
--- a/app/controllers/concerns/saml_idp_auth_concern.rb
+++ b/app/controllers/concerns/saml_idp_auth_concern.rb
@@ -1,6 +1,7 @@
module SamlIdpAuthConcern
extend ActiveSupport::Concern
extend Forwardable
+ include ForcedReauthenticationConcern
included do
# rubocop:disable Rails/LexicallyScopedActionFilter
@@ -19,9 +20,22 @@ module SamlIdpAuthConcern
private
def sign_out_if_forceauthn_is_true_and_user_is_signed_in
+ if !saml_request.force_authn?
+ set_issuer_forced_reauthentication(
+ issuer: saml_request_service_provider.issuer,
+ is_forced_reauthentication: false,
+ )
+ end
+
return unless user_signed_in? && saml_request.force_authn?
- sign_out unless sp_session[:final_auth_request]
+ if !sp_session[:final_auth_request]
+ sign_out
+ set_issuer_forced_reauthentication(
+ issuer: saml_request_service_provider.issuer,
+ is_forced_reauthentication: true,
+ )
+ end
sp_session[:final_auth_request] = false
end
diff --git a/app/controllers/frontend_log_controller.rb b/app/controllers/frontend_log_controller.rb
index a2708f4810c..813663c3272 100644
--- a/app/controllers/frontend_log_controller.rb
+++ b/app/controllers/frontend_log_controller.rb
@@ -28,8 +28,8 @@ class FrontendLogController < ApplicationController
'Multi-Factor Authentication: download backup code' => :multi_factor_auth_backup_code_download,
'Show Password button clicked' => :show_password_button_clicked,
'Sign In: IdV requirements accordion clicked' => :sign_in_idv_requirements_accordion_clicked,
- 'User prompted before navigation and still on page' => :user_prompted_before_navigation_and_still_on_page,
'User prompted before navigation' => :user_prompted_before_navigation,
+ 'User prompted before navigation and still on page' => :user_prompted_before_navigation_and_still_on_page,
}.transform_values { |method| AnalyticsEvents.instance_method(method) }.freeze
# rubocop:enable Layout/LineLength
diff --git a/app/controllers/idv/agreement_controller.rb b/app/controllers/idv/agreement_controller.rb
index 9248bcb6789..c244177a8f6 100644
--- a/app/controllers/idv/agreement_controller.rb
+++ b/app/controllers/idv/agreement_controller.rb
@@ -41,6 +41,7 @@ def analytics_arguments
{
step: 'agreement',
analytics_id: 'Doc Auth',
+ skip_hybrid_handoff: idv_session.skip_hybrid_handoff,
irs_reproofing: irs_reproofing?,
}.merge(ab_test_analytics_buckets)
end
diff --git a/app/controllers/idv/document_capture_controller.rb b/app/controllers/idv/document_capture_controller.rb
index 8db81b04648..b7ae2db42da 100644
--- a/app/controllers/idv/document_capture_controller.rb
+++ b/app/controllers/idv/document_capture_controller.rb
@@ -75,7 +75,8 @@ def analytics_arguments
analytics_id: 'Doc Auth',
irs_reproofing: irs_reproofing?,
redo_document_capture: idv_session.redo_document_capture,
- }.compact.merge(ab_test_analytics_buckets)
+ skip_hybrid_handoff: idv_session.skip_hybrid_handoff,
+ }.merge(ab_test_analytics_buckets)
end
def handle_stored_result
diff --git a/app/controllers/idv/getting_started_controller.rb b/app/controllers/idv/getting_started_controller.rb
index d4e547fbef3..f622b68aad5 100644
--- a/app/controllers/idv/getting_started_controller.rb
+++ b/app/controllers/idv/getting_started_controller.rb
@@ -46,6 +46,7 @@ def analytics_arguments
{
step: 'getting_started',
analytics_id: 'Doc Auth',
+ skip_hybrid_handoff: idv_session.skip_hybrid_handoff,
irs_reproofing: irs_reproofing?,
}.merge(ab_test_analytics_buckets)
end
diff --git a/app/controllers/idv/hybrid_handoff_controller.rb b/app/controllers/idv/hybrid_handoff_controller.rb
index 18271841804..d75ec9427a8 100644
--- a/app/controllers/idv/hybrid_handoff_controller.rb
+++ b/app/controllers/idv/hybrid_handoff_controller.rb
@@ -145,7 +145,8 @@ def analytics_arguments
analytics_id: 'Doc Auth',
irs_reproofing: irs_reproofing?,
redo_document_capture: params[:redo] ? true : nil,
- }.compact.merge(ab_test_analytics_buckets)
+ skip_hybrid_handoff: idv_session.skip_hybrid_handoff,
+ }.merge(ab_test_analytics_buckets)
end
def form_response(destination:)
diff --git a/app/controllers/idv/link_sent_controller.rb b/app/controllers/idv/link_sent_controller.rb
index cdf763a0dfa..f6c345eb50a 100644
--- a/app/controllers/idv/link_sent_controller.rb
+++ b/app/controllers/idv/link_sent_controller.rb
@@ -6,7 +6,6 @@ class LinkSentController < ApplicationController
before_action :confirm_hybrid_handoff_complete
before_action :confirm_document_capture_needed
- before_action :extend_timeout_using_meta_refresh
def show
analytics.idv_doc_auth_link_sent_visited(**analytics_arguments)
@@ -91,18 +90,5 @@ def document_capture_session_result
document_capture_session&.load_doc_auth_async_result
end
end
-
- def extend_timeout_using_meta_refresh
- max_10min_refreshes = IdentityConfig.store.doc_auth_extend_timeout_by_minutes / 10
- return if max_10min_refreshes <= 0
- meta_refresh_count = flow_session[:meta_refresh_count].to_i
- return if meta_refresh_count >= max_10min_refreshes
- do_meta_refresh(meta_refresh_count)
- end
-
- def do_meta_refresh(meta_refresh_count)
- @meta_refresh = 10 * 60
- flow_session[:meta_refresh_count] = meta_refresh_count + 1
- end
end
end
diff --git a/app/controllers/openid_connect/authorization_controller.rb b/app/controllers/openid_connect/authorization_controller.rb
index 203a104b06f..e34174ef6ff 100644
--- a/app/controllers/openid_connect/authorization_controller.rb
+++ b/app/controllers/openid_connect/authorization_controller.rb
@@ -6,6 +6,7 @@ class AuthorizationController < ApplicationController
include SecureHeadersConcern
include AuthorizationCountConcern
include BillableEventTrackable
+ include ForcedReauthenticationConcern
before_action :build_authorize_form_from_params, only: [:index]
before_action :pre_validate_authorize_form, only: [:index]
@@ -125,12 +126,22 @@ def pre_validate_authorize_form
end
def sign_out_if_prompt_param_is_login_and_user_is_signed_in
+ if @authorize_form.prompt != 'login'
+ set_issuer_forced_reauthentication(
+ issuer: @authorize_form.service_provider.issuer,
+ is_forced_reauthentication: false,
+ )
+ end
return unless user_signed_in? && @authorize_form.prompt == 'login'
return if session[:oidc_state_for_login_prompt] == @authorize_form.state
return if check_sp_handoff_bounced
unless sp_session[:request_url] == request.original_url
sign_out
session[:oidc_state_for_login_prompt] = @authorize_form.state
+ set_issuer_forced_reauthentication(
+ issuer: @authorize_form.service_provider.issuer,
+ is_forced_reauthentication: true,
+ )
end
end
diff --git a/app/controllers/users/phone_setup_controller.rb b/app/controllers/users/phone_setup_controller.rb
index 9dc28867cfb..6344fd8e60b 100644
--- a/app/controllers/users/phone_setup_controller.rb
+++ b/app/controllers/users/phone_setup_controller.rb
@@ -5,16 +5,20 @@ class PhoneSetupController < ApplicationController
include PhoneConfirmation
include MfaSetupConcern
include RecaptchaConcern
+ include ReauthenticationRequiredConcern
before_action :authenticate_user
before_action :confirm_user_authenticated_for_2fa_setup
- before_action :confirm_user_in_account_setup
before_action :set_setup_presenter
before_action :allow_csp_recaptcha_src, if: :recaptcha_enabled?
+ before_action :redirect_if_phone_vendor_outage
+ before_action :confirm_recently_authenticated_2fa
+ before_action :check_max_phone_numbers_per_account, only: %i[index create]
helper_method :in_multi_mfa_selection_flow?
def index
+ user_session[:phone_id] = nil
@new_phone_form = NewPhoneForm.new(
user: current_user,
analytics: analytics,
@@ -45,9 +49,13 @@ def recaptcha_enabled?
def track_phone_setup_visit
mfa_user = MfaContext.new(current_user)
- analytics.user_registration_phone_setup_visit(
- enabled_mfa_methods_count: mfa_user.enabled_mfa_methods_count,
- )
+ if in_multi_mfa_selection_flow?
+ analytics.user_registration_phone_setup_visit(
+ enabled_mfa_methods_count: mfa_user.enabled_mfa_methods_count,
+ )
+ else
+ analytics.add_phone_setup_visit
+ end
end
def set_setup_presenter
@@ -70,6 +78,7 @@ def handle_create_success(phone)
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,
)
else
flash[:error] = t('errors.messages.phone_duplicate')
@@ -77,6 +86,21 @@ def handle_create_success(phone)
end
end
+ def check_max_phone_numbers_per_account
+ max_phones_count = IdentityConfig.store.max_phone_numbers_per_account
+ return if current_user.phone_configurations.count < max_phones_count
+ flash[:phone_error] = t('users.phones.error_message')
+ redirect_path = request.referer.match(account_two_factor_authentication_url) ?
+ account_two_factor_authentication_url(anchor: 'phones') :
+ account_url(anchor: 'phones')
+ redirect_to redirect_path
+ end
+
+ def redirect_if_phone_vendor_outage
+ return unless OutageStatus.new.all_phone_vendor_outage?
+ redirect_to vendor_outage_path(from: :users_phones)
+ end
+
def new_phone_form_params
params.require(:new_phone_form).permit(
:phone,
@@ -88,11 +112,5 @@ def new_phone_form_params
:recaptcha_mock_score,
)
end
-
- def confirm_user_in_account_setup
- return if user_fully_authenticated? && in_multi_mfa_selection_flow?
- return unless MfaPolicy.new(current_user).two_factor_enabled?
- redirect_to account_path
- end
end
end
diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb
index c8be461e9f5..f533ae24d2b 100644
--- a/app/controllers/users/sessions_controller.rb
+++ b/app/controllers/users/sessions_controller.rb
@@ -7,6 +7,7 @@ class SessionsController < Devise::SessionsController
include RememberDeviceConcern
include Ial2ProfileConcern
include Api::CsrfTokenConcern
+ include ForcedReauthenticationConcern
rescue_from ActionController::InvalidAuthenticityToken, with: :redirect_to_signin
@@ -20,6 +21,9 @@ def new
override_csp_for_google_analytics
@ial = sp_session_ial
+ @issuer_forced_reauthentication = issuer_forced_reauthentication?(
+ issuer: decorated_session.sp_issuer,
+ )
analytics.sign_in_page_visit(
flash: flash[:alert],
stored_location: session['user_return_to'],
diff --git a/app/controllers/users/two_factor_authentication_setup_controller.rb b/app/controllers/users/two_factor_authentication_setup_controller.rb
index 2f5fd7bab98..479d06be738 100644
--- a/app/controllers/users/two_factor_authentication_setup_controller.rb
+++ b/app/controllers/users/two_factor_authentication_setup_controller.rb
@@ -5,12 +5,13 @@ class TwoFactorAuthenticationSetupController < ApplicationController
before_action :authenticate_user
before_action :confirm_user_authenticated_for_2fa_setup
- before_action :confirm_user_needs_2fa_setup
+
+ delegate :enabled_mfa_methods_count, to: :mfa_context
def index
two_factor_options_form
@presenter = two_factor_options_presenter
- analytics.user_registration_2fa_setup_visit
+ analytics.user_registration_2fa_setup_visit(enabled_mfa_methods_count:)
end
def create
@@ -42,6 +43,10 @@ def two_factor_options_form
private
+ def mfa_context
+ @mfa_context ||= MfaContext.new(current_user)
+ end
+
def submit_form
two_factor_options_form.submit(two_factor_options_form_params)
end
@@ -52,18 +57,19 @@ def two_factor_options_presenter
user: current_user,
phishing_resistant_required: service_provider_mfa_policy.phishing_resistant_required?,
piv_cac_required: service_provider_mfa_policy.piv_cac_required?,
+ show_skip_additional_mfa_link: show_skip_additional_mfa_link?,
+ after_mfa_setup_path:,
)
end
def process_valid_form
user_session[:mfa_selections] = @two_factor_options_form.selection
- redirect_to confirmation_path(user_session[:mfa_selections].first)
- end
- def confirm_user_needs_2fa_setup
- return unless mfa_policy.two_factor_enabled?
- return if service_provider_mfa_policy.user_needs_sp_auth_method_setup?
- redirect_to after_mfa_setup_path
+ if user_session[:mfa_selections].first.present?
+ redirect_to confirmation_path(user_session[:mfa_selections].first)
+ else
+ redirect_to after_mfa_setup_path
+ end
end
def two_factor_options_form_params
diff --git a/app/forms/webauthn_visit_form.rb b/app/forms/webauthn_visit_form.rb
index 74feb3f2248..d299057e0b1 100644
--- a/app/forms/webauthn_visit_form.rb
+++ b/app/forms/webauthn_visit_form.rb
@@ -25,9 +25,7 @@ def platform_authenticator?
end
def current_mfa_setup_path
- if mfa_user.two_factor_enabled? && in_mfa_selection_flow
- second_mfa_setup_path
- elsif mfa_user.two_factor_enabled?
+ if mfa_user.two_factor_enabled? && !in_mfa_selection_flow
account_path
else
authentication_methods_setup_path
diff --git a/app/javascript/packages/document-capture/components/location-collection-item.spec.tsx b/app/javascript/packages/document-capture/components/location-collection-item.spec.tsx
index 013d181f665..16d54ff419b 100644
--- a/app/javascript/packages/document-capture/components/location-collection-item.spec.tsx
+++ b/app/javascript/packages/document-capture/components/location-collection-item.spec.tsx
@@ -147,4 +147,44 @@ describe('LocationCollectionItem', () => {
expect(sunHours).not.to.exist();
});
});
+
+ context('when handleSelect callback is not provided', () => {
+ it('renders the component without the button', () => {
+ const onClick = sinon.stub();
+ const { container } = render(
+ ,
+ );
+
+ expect(container.textContent).to.contain('in_person_proofing.body.location.location_button');
+ });
+ });
+
+ context('when handleSelect callback is provided', () => {
+ it('renders the component with the button', () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.textContent).to.not.contain(
+ 'in_person_proofing.body.location.location_button',
+ );
+ });
+ });
});
diff --git a/app/javascript/packages/document-capture/components/location-collection-item.tsx b/app/javascript/packages/document-capture/components/location-collection-item.tsx
index 3cfc1ac2fd6..5d27d870685 100644
--- a/app/javascript/packages/document-capture/components/location-collection-item.tsx
+++ b/app/javascript/packages/document-capture/components/location-collection-item.tsx
@@ -4,7 +4,7 @@ import { useI18n } from '@18f/identity-react-i18n';
interface LocationCollectionItemProps {
distance?: string;
formattedCityStateZip: string;
- handleSelect: (event: React.MouseEvent, selection: number) => void;
+ handleSelect?: (event: React.MouseEvent, selection: number) => void;
name?: string;
saturdayHours: string;
selectId: number;
@@ -60,24 +60,28 @@ function LocationCollectionItem({
{`${t('in_person_proofing.body.location.retail_hours_sun')} ${sundayHours}`}
)}
- handleSelect(event, selectId)}
- type="submit"
- >
- {t('in_person_proofing.body.location.location_button')}
-
+ {handleSelect && (
+ handleSelect(event, selectId)}
+ type="submit"
+ >
+ {t('in_person_proofing.body.location.location_button')}
+
+ )}
- {
- handleSelect(event, selectId);
- }}
- type="submit"
- >
- {t('in_person_proofing.body.location.location_button')}
-
+ {handleSelect && (
+ {
+ handleSelect(event, selectId);
+ }}
+ type="submit"
+ >
+ {t('in_person_proofing.body.location.location_button')}
+
+ )}
diff --git a/app/javascript/packages/phone-input/package.json b/app/javascript/packages/phone-input/package.json
index c8f0d8a0018..e0d1e27b6eb 100644
--- a/app/javascript/packages/phone-input/package.json
+++ b/app/javascript/packages/phone-input/package.json
@@ -4,6 +4,6 @@
"version": "1.0.0",
"dependencies": {
"intl-tel-input": "^17.0.19",
- "libphonenumber-js": "^1.10.39"
+ "libphonenumber-js": "^1.10.41"
}
}
diff --git a/app/jobs/in_person/send_proofing_notification_job.rb b/app/jobs/in_person/send_proofing_notification_job.rb
index edc07781164..c3d84a404d5 100644
--- a/app/jobs/in_person/send_proofing_notification_job.rb
+++ b/app/jobs/in_person/send_proofing_notification_job.rb
@@ -2,6 +2,8 @@
module InPerson
class SendProofingNotificationJob < ApplicationJob
+ include LocaleHelper
+
# @param [Number] enrollment_id primary key of the enrollment
def perform(enrollment_id)
return unless IdentityConfig.store.in_person_proofing_enabled &&
@@ -42,7 +44,7 @@ def perform(enrollment_id)
rescue StandardError => err
analytics(user: enrollment&.user || AnonymousUser.new).
idv_in_person_send_proofing_notification_job_exception(
- enrollment_code: enrollment&.code,
+ enrollment_code: enrollment&.enrollment_code,
enrollment_id: enrollment_id,
exception_class: err.class.to_s,
exception_message: err.message,
@@ -72,12 +74,14 @@ def handle_telephony_response(enrollment:, phone:, telephony_response:)
end
def notification_message(enrollment:)
- proof_date = enrollment.proofed_at ? I18n.l(enrollment.proofed_at, format: :sms_date) : 'NA'
- I18n.t(
- 'telephony.confirmation_ipp_enrollment_result.sms',
- app_name: APP_NAME,
- proof_date: proof_date,
- )
+ with_user_locale(enrollment.user) do
+ proof_date = I18n.l(enrollment.proofed_at, format: :sms_date)
+ I18n.t(
+ 'telephony.confirmation_ipp_enrollment_result.sms',
+ app_name: APP_NAME,
+ proof_date: proof_date,
+ )
+ end
end
def analytics(user:)
diff --git a/app/models/in_person_enrollment.rb b/app/models/in_person_enrollment.rb
index dc9c2400dea..169b2f16779 100644
--- a/app/models/in_person_enrollment.rb
+++ b/app/models/in_person_enrollment.rb
@@ -34,44 +34,64 @@ class InPersonEnrollment < ApplicationRecord
before_create(:set_unique_id, unless: :unique_id)
before_create(:set_capture_secondary_id)
- def self.is_pending_and_established_between(early_benchmark, late_benchmark)
- where(status: :pending).
- and(
- where(enrollment_established_at: late_benchmark...(early_benchmark.end_of_day)),
- ).
- order(enrollment_established_at: :asc)
- end
+ class << self
+ def needs_early_email_reminder(early_benchmark, late_benchmark)
+ is_pending_and_established_between(
+ early_benchmark,
+ late_benchmark,
+ ).where(early_reminder_sent: false)
+ end
- def self.needs_early_email_reminder(early_benchmark, late_benchmark)
- self.is_pending_and_established_between(
- early_benchmark,
- late_benchmark,
- ).where(early_reminder_sent: false)
- end
+ def needs_late_email_reminder(early_benchmark, late_benchmark)
+ is_pending_and_established_between(
+ early_benchmark,
+ late_benchmark,
+ ).where(late_reminder_sent: false)
+ end
- def self.needs_late_email_reminder(early_benchmark, late_benchmark)
- self.is_pending_and_established_between(
- early_benchmark,
- late_benchmark,
- ).where(late_reminder_sent: false)
- end
+ # Find enrollments that need a status check via the USPS API
+ def needs_usps_status_check(check_interval)
+ where(status: :pending).
+ and(
+ where(last_batch_claimed_at: check_interval).
+ or(where(last_batch_claimed_at: nil)),
+ )
+ end
- # Find enrollments that need a status check via the USPS API
- def self.needs_usps_status_check(check_interval)
- where(status: :pending).
- and(
- where(last_batch_claimed_at: check_interval).
- or(where(last_batch_claimed_at: nil)),
- )
- end
+ def needs_usps_status_check_batch(batch_at)
+ where(status: :pending).
+ and(
+ where(last_batch_claimed_at: batch_at),
+ ).
+ order(status_check_attempted_at: :asc)
+ end
+
+ # Find enrollments that are ready for a status check via the USPS API
+ def needs_status_check_on_ready_enrollments(check_interval)
+ needs_usps_status_check(check_interval).where(ready_for_status_check: true)
+ end
- def self.needs_usps_status_check_batch(batch_at)
- where(status: :pending).
- and(
- where(last_batch_claimed_at: batch_at),
- ).
- order(status_check_attempted_at: :asc)
+ # Find waiting enrollments that need a status check via the USPS API
+ def needs_status_check_on_waiting_enrollments(check_interval)
+ needs_usps_status_check(check_interval).where(ready_for_status_check: false)
+ end
+
+ # Generates a random 18-digit string, the hex returns a string of length n*2
+ def generate_unique_id
+ SecureRandom.hex(9)
+ end
+
+ private
+
+ def is_pending_and_established_between(early_benchmark, late_benchmark)
+ where(status: :pending).
+ and(
+ where(enrollment_established_at: late_benchmark...(early_benchmark.end_of_day)),
+ ).
+ order(enrollment_established_at: :asc)
+ end
end
+ # end class methods
# Does this enrollment need a status check via the USPS API?
def needs_usps_status_check?(check_interval)
@@ -81,21 +101,11 @@ def needs_usps_status_check?(check_interval)
)
end
- # Find enrollments that are ready for a status check via the USPS API
- def self.needs_status_check_on_ready_enrollments(check_interval)
- needs_usps_status_check(check_interval).where(ready_for_status_check: true)
- end
-
# Does this ready enrollment need a status check via the USPS API?
def needs_status_check_on_ready_enrollment?(check_interval)
needs_usps_status_check?(check_interval) && ready_for_status_check?
end
- # Find waiting enrollments that need a status check via the USPS API
- def self.needs_status_check_on_waiting_enrollments(check_interval)
- needs_usps_status_check(check_interval).where(ready_for_status_check: false)
- end
-
# Does this waiting enrollment need a status check via the USPS API?
def needs_status_check_on_waiting_enrollment?(check_interval)
needs_usps_status_check?(check_interval) && !ready_for_status_check?
@@ -121,16 +131,6 @@ def minutes_since_last_status_update
(Time.zone.now - status_updated_at).seconds.in_minutes.round(2)
end
- # (deprecated) Returns the value to use for the USPS enrollment ID
- def usps_unique_id
- user.uuid.delete('-').slice(0, 18)
- end
-
- # Generates a random 18-digit string, the hex returns a string of length n*2
- def self.generate_unique_id
- SecureRandom.hex(9)
- end
-
def due_date
start_date = enrollment_established_at.presence || created_at
start_date + IdentityConfig.store.in_person_enrollment_validity_in_days.days
@@ -141,18 +141,24 @@ def days_to_due_date
(today...due_date).count
end
- def on_notification_sent_at_updated
- if self.notification_sent_at && self.notification_phone_configuration
- self.notification_phone_configuration.destroy
- end
+ def eligible_for_notification?
+ notification_phone_configuration.present? && (passed? || failed?)
end
- def eligible_for_notification?
- self.notification_phone_configuration.present? && (self.passed? || self.failed?)
+ # (deprecated) Returns the value to use for the USPS enrollment ID
+ def usps_unique_id
+ user.uuid.delete('-').slice(0, 18)
end
private
+ def on_notification_sent_at_updated
+ change_will_be_saved = notification_sent_at_change_to_be_saved&.last.present?
+ if change_will_be_saved && notification_phone_configuration.present?
+ notification_phone_configuration.destroy
+ end
+ end
+
def on_status_updated
if enrollment_will_be_cancelled_or_expired? && notification_phone_configuration.present?
notification_phone_configuration.destroy!
@@ -165,7 +171,7 @@ def enrollment_will_be_cancelled_or_expired?
end
def set_unique_id
- self.unique_id = self.class.generate_unique_id
+ self.unique_id = InPersonEnrollment.generate_unique_id
end
def profile_belongs_to_user
diff --git a/app/presenters/navigation_presenter.rb b/app/presenters/navigation_presenter.rb
index e9e7409098b..0613f22e587 100644
--- a/app/presenters/navigation_presenter.rb
+++ b/app/presenters/navigation_presenter.rb
@@ -25,7 +25,7 @@ def navigation_items
NavItem.new(
I18n.t('account.navigation.two_factor_authentication'),
account_two_factor_authentication_path, [
- NavItem.new(I18n.t('account.navigation.add_phone_number'), add_phone_path),
+ NavItem.new(I18n.t('account.navigation.add_phone_number'), phone_setup_path),
NavItem.new(
I18n.t('account.navigation.add_authentication_apps'),
authenticator_setup_url,
diff --git a/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb b/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb
index 1fc5f0ba08d..8bf2f003c51 100644
--- a/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb
+++ b/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb
@@ -73,7 +73,7 @@ def redirect_location_step
def troubleshoot_change_phone_or_method_option
if unconfirmed_phone
- BlockLinkComponent.new(url: add_phone_path).with_content(
+ BlockLinkComponent.new(url: phone_setup_path).with_content(
t('two_factor_authentication.phone_verification.troubleshooting.change_number'),
)
else
diff --git a/app/presenters/two_factor_options_presenter.rb b/app/presenters/two_factor_options_presenter.rb
index d733cf45a5c..37ce24c5408 100644
--- a/app/presenters/two_factor_options_presenter.rb
+++ b/app/presenters/two_factor_options_presenter.rb
@@ -1,18 +1,24 @@
class TwoFactorOptionsPresenter
include ActionView::Helpers::TranslationHelper
- attr_reader :user
-
- def initialize(user_agent:,
- user: nil,
- phishing_resistant_required: false,
- piv_cac_required: false,
- show_skip_additional_mfa_link: true)
+ attr_reader :user, :after_mfa_setup_path
+
+ delegate :two_factor_enabled?, to: :mfa_policy
+
+ def initialize(
+ user_agent:,
+ user: nil,
+ phishing_resistant_required: false,
+ piv_cac_required: false,
+ show_skip_additional_mfa_link: true,
+ after_mfa_setup_path: nil
+ )
@user_agent = user_agent
@user = user
@phishing_resistant_required = phishing_resistant_required
@piv_cac_required = piv_cac_required
@show_skip_additional_mfa_link = show_skip_additional_mfa_link
+ @after_mfa_setup_path = after_mfa_setup_path
end
def options
@@ -54,6 +60,10 @@ def show_skip_additional_mfa_link?
@show_skip_additional_mfa_link
end
+ def skip_path
+ after_mfa_setup_path if two_factor_enabled? && show_skip_additional_mfa_link?
+ end
+
private
def piv_cac_option
diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb
index 1ee40706f45..1cebaeccaeb 100644
--- a/app/services/analytics_events.rb
+++ b/app/services/analytics_events.rb
@@ -918,6 +918,8 @@ def idv_doc_auth_welcome_visited(**extra)
# @param [Boolean] gpo_verification_pending Profile is awaiting gpo verificaiton
# @param [Boolean] in_person_verification_pending Profile is awaiting in person verificaiton
# @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components
+ # @see Reporting::IdentityVerificationReport#query This event is used by the identity verification
+ # report. Changes here should be reflected there.
# Tracks the last step of IDV, indicates the user successfully proofed
def idv_final(
success:,
@@ -1051,6 +1053,8 @@ def idv_gpo_reminder_email_sent(user_id:, **extra)
# @param [Integer] attempts Number of attempts to enter a correct code
# @param [Boolean] pending_in_person_enrollment
# @param [Boolean] fraud_check_failed
+ # @see Reporting::IdentityVerificationReport#query This event is used by the identity verification
+ # report. Changes here should be reflected there.
# GPO verification submitted
def idv_gpo_verification_submitted(
success:,
@@ -3883,9 +3887,12 @@ def user_registration_2fa_setup(
end
# Tracks when user visits MFA selection page
- def user_registration_2fa_setup_visit
+ # @param [Integer] enabled_mfa_methods_count Number of MFAs associated with user at time of visit
+ def user_registration_2fa_setup_visit(enabled_mfa_methods_count:, **extra)
track_event(
'User Registration: 2FA Setup visited',
+ enabled_mfa_methods_count:,
+ **extra,
)
end
diff --git a/app/services/encryption/encryptors/pii_encryptor.rb b/app/services/encryption/encryptors/pii_encryptor.rb
index 704a8cc618c..af5ec65e9f2 100644
--- a/app/services/encryption/encryptors/pii_encryptor.rb
+++ b/app/services/encryption/encryptors/pii_encryptor.rb
@@ -70,10 +70,9 @@ def encrypt(plaintext, user_uuid: nil)
end
def decrypt(ciphertext_pair, user_uuid: nil)
- ciphertext_string = ciphertext_pair.single_region_ciphertext
-
+ ciphertext_string = ciphertext_pair.multi_or_single_region_ciphertext
ciphertext = Ciphertext.parse_from_string(ciphertext_string)
- aes_encrypted_ciphertext = single_region_kms_client.decrypt(
+ aes_encrypted_ciphertext = multi_region_kms_client.decrypt(
ciphertext.encrypted_data, kms_encryption_context(user_uuid: user_uuid)
)
aes_encryption_key = scrypt_password_digest(salt: ciphertext.salt, cost: ciphertext.cost)
diff --git a/app/services/encryption/password_verifier.rb b/app/services/encryption/password_verifier.rb
index b5133bfb400..edcc30a2b3c 100644
--- a/app/services/encryption/password_verifier.rb
+++ b/app/services/encryption/password_verifier.rb
@@ -69,7 +69,7 @@ def create_digest_pair(password:, user_uuid:)
end
def verify(password:, digest_pair:, user_uuid:)
- digest = digest_pair.single_region_ciphertext
+ digest = digest_pair.multi_or_single_region_ciphertext
password_digest = PasswordDigest.parse_from_string(digest)
return verify_uak_digest(password, digest) if stale_digest?(digest)
@@ -101,7 +101,7 @@ def verify_password_against_digest(password:, password_digest:, user_uuid:)
end
def decrypt_digest_with_kms(encrypted_password, user_uuid)
- single_region_kms_client.decrypt(
+ multi_region_kms_client.decrypt(
encrypted_password, kms_encryption_context(user_uuid: user_uuid)
)
end
diff --git a/app/services/encryption/regional_ciphertext_pair.rb b/app/services/encryption/regional_ciphertext_pair.rb
index 18bec0cde37..73646f14e79 100644
--- a/app/services/encryption/regional_ciphertext_pair.rb
+++ b/app/services/encryption/regional_ciphertext_pair.rb
@@ -4,4 +4,12 @@
def to_ary
[single_region_ciphertext, multi_region_ciphertext]
end
+
+ def multi_or_single_region_ciphertext
+ if IdentityConfig.store.aws_kms_multi_region_read_enabled
+ multi_region_ciphertext.presence || single_region_ciphertext
+ else
+ single_region_ciphertext
+ end
+ end
end
diff --git a/app/services/idv/steps/threat_metrix_step_helper.rb b/app/services/idv/steps/threat_metrix_step_helper.rb
index 24ec29174c9..6817b5cb5d5 100644
--- a/app/services/idv/steps/threat_metrix_step_helper.rb
+++ b/app/services/idv/steps/threat_metrix_step_helper.rb
@@ -12,12 +12,8 @@ def threatmetrix_view_variables
end
def generate_threatmetrix_session_id
- if !updating_ssn?
- idv_session.threatmetrix_session_id = SecureRandom.uuid
- # for 50/50 state, to be removed in next deploy
- flow_session[:threatmetrix_session_id] = idv_session.threatmetrix_session_id
- end
- idv_session.threatmetrix_session_id || flow_session[:threatmetrix_session_id]
+ idv_session.threatmetrix_session_id = SecureRandom.uuid if !updating_ssn?
+ idv_session.threatmetrix_session_id
end
# @return [Array]
diff --git a/app/services/service_provider_updater.rb b/app/services/service_provider_updater.rb
index ab878f9b16d..ccece2020ee 100644
--- a/app/services/service_provider_updater.rb
+++ b/app/services/service_provider_updater.rb
@@ -3,7 +3,6 @@ class ServiceProviderUpdater
SP_PROTECTED_ATTRIBUTES = %i[
created_at
id
- native
updated_at
].to_set.freeze
@@ -34,13 +33,12 @@ def update_cache(service_provider)
if service_provider['active'] == true
create_or_update_service_provider(issuer, service_provider)
else
- ServiceProvider.where(issuer: issuer, native: false).destroy_all
+ ServiceProvider.where(issuer: issuer).destroy_all
end
end
def create_or_update_service_provider(issuer, service_provider)
sp = ServiceProvider.find_by(issuer: issuer)
- return if sp&.native?
sync_model(sp, cleaned_service_provider(service_provider))
end
diff --git a/app/views/accounts/_phone.html.erb b/app/views/accounts/_phone.html.erb
index a9eaf9bb540..0e267f47f2b 100644
--- a/app/views/accounts/_phone.html.erb
+++ b/app/views/accounts/_phone.html.erb
@@ -25,7 +25,7 @@
<% if current_user.phone_configurations.count < IdentityConfig.store.max_phone_numbers_per_account %>
<%= render ButtonComponent.new(
- action: ->(**tag_options, &block) { link_to(add_phone_path, **tag_options, &block) },
+ action: ->(**tag_options, &block) { link_to(phone_setup_path, **tag_options, &block) },
outline: true,
icon: :add,
class: 'margin-top-2',
diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb
index 18946fc61b8..196360f07e3 100644
--- a/app/views/devise/sessions/new.html.erb
+++ b/app/views/devise/sessions/new.html.erb
@@ -17,6 +17,12 @@
<%= render 'shared/sp_alert', section: 'sign_in' %>
+<% if @issuer_forced_reauthentication %>
+
+ <%= t('account.login.forced_reauthentication_notice_html', sp_name: decorated_session.sp_name) %>
+
+<% end %>
+
<%= simple_form_for(
resource,
as: resource_name,
diff --git a/app/views/idv/link_sent/show.html.erb b/app/views/idv/link_sent/show.html.erb
index 3463e51f452..8fb01c7d954 100644
--- a/app/views/idv/link_sent/show.html.erb
+++ b/app/views/idv/link_sent/show.html.erb
@@ -9,10 +9,6 @@
<% title t('titles.doc_auth.link_sent') %>
-
-<% if @meta_refresh && !FeatureManagement.doc_capture_polling_enabled? %>
- <%= content_for(:meta_refresh) { @meta_refresh.to_s } %>
-<% end %>
<% if flow_session[:error_message] %>
<%= render AlertComponent.new(
type: :error,
diff --git a/app/views/idv/welcome/_welcome_new.html.erb b/app/views/idv/welcome/_welcome_new.html.erb
index 25896b3cb43..af39a487264 100644
--- a/app/views/idv/welcome/_welcome_new.html.erb
+++ b/app/views/idv/welcome/_welcome_new.html.erb
@@ -16,7 +16,7 @@
category: 'verify-your-identity',
article: 'how-to-verify-your-identity',
flow: :idv,
- step: :getting_started,
+ step: :welcome_new,
location: 'intro_paragraph',
),
),
@@ -59,7 +59,7 @@
<% end %>
- <%= render 'shared/cancel', link: idv_cancel_path(step: 'getting_started') %>
+ <%= render 'shared/cancel', link: idv_cancel_path(step: 'welcome_new') %>
<% end %>
<%= javascript_packs_tag_once('document-capture-welcome') %>
diff --git a/app/views/mfa_confirmation/show.html.erb b/app/views/mfa_confirmation/show.html.erb
index 5466f763230..d5c403da0ea 100644
--- a/app/views/mfa_confirmation/show.html.erb
+++ b/app/views/mfa_confirmation/show.html.erb
@@ -10,7 +10,7 @@
<%= link_to(
- second_mfa_setup_path,
+ authentication_methods_setup_path,
class: 'usa-button usa-button--wide usa-button--big margin-bottom-3',
) { @content.button } %>
@@ -23,4 +23,4 @@
) do %>
<%= t('mfa.skip') %>
<% end %>
-<% end %>
\ No newline at end of file
+<% end %>
diff --git a/app/views/shared/_cancel_or_back_to_options.html.erb b/app/views/shared/_cancel_or_back_to_options.html.erb
index 69d0afac603..ef39ba162e7 100644
--- a/app/views/shared/_cancel_or_back_to_options.html.erb
+++ b/app/views/shared/_cancel_or_back_to_options.html.erb
@@ -1,7 +1,5 @@
<%= render PageFooterComponent.new do %>
- <% if MfaPolicy.new(current_user).two_factor_enabled? && in_multi_mfa_selection_flow? %>
- <%= link_to t('two_factor_authentication.choose_another_option'), second_mfa_setup_path %>
- <% elsif MfaPolicy.new(current_user).two_factor_enabled? %>
+ <% if MfaPolicy.new(current_user).two_factor_enabled? && !in_multi_mfa_selection_flow? %>
<%= link_to t('links.cancel'), account_path %>
<% else %>
<%= link_to t('two_factor_authentication.choose_another_option'), authentication_methods_setup_path %>
diff --git a/app/views/sign_up/completions/show.html.erb b/app/views/sign_up/completions/show.html.erb
index 1fb2b14660f..a4cfdf5ba3f 100644
--- a/app/views/sign_up/completions/show.html.erb
+++ b/app/views/sign_up/completions/show.html.erb
@@ -18,7 +18,7 @@
<%= render(AlertComponent.new(type: :warning, class: 'margin-bottom-4')) do %>
<%= link_to(
t('mfa.second_method_warning.link'),
- second_mfa_setup_path,
+ authentication_methods_setup_path,
) %>
<%= t('mfa.second_method_warning.text') %>
<% end %>
@@ -30,4 +30,4 @@
<%= render PageFooterComponent.new do %>
<%= link_to t('links.cancel'), return_to_sp_cancel_path %>
-<% end %>
\ No newline at end of file
+<% end %>
diff --git a/app/views/users/backup_code_setup/confirm_backup_codes.html.erb b/app/views/users/backup_code_setup/confirm_backup_codes.html.erb
index 22133481d8b..d2f59eb88fa 100644
--- a/app/views/users/backup_code_setup/confirm_backup_codes.html.erb
+++ b/app/views/users/backup_code_setup/confirm_backup_codes.html.erb
@@ -27,11 +27,11 @@
big: true,
full_width: true,
outline: true,
- ).with_content(t('two_factor_authentication.backup_codes.new_backup_codes_html')) %>
+ ).with_content(t('two_factor_authentication.backup_codes.new_backup_codes_html')) %>
<%= render PageFooterComponent.new do %>
- <%= link_to t('two_factor_authentication.backup_codes.add_another_authentication_option'), second_mfa_setup_path %>
-<% end %>
+ <%= link_to t('two_factor_authentication.backup_codes.add_another_authentication_option'), authentication_methods_setup_path %>
+<% end %>
diff --git a/app/views/users/phone_setup/index.html.erb b/app/views/users/phone_setup/index.html.erb
index 397e40a7f73..613148ac898 100644
--- a/app/views/users/phone_setup/index.html.erb
+++ b/app/views/users/phone_setup/index.html.erb
@@ -1,8 +1,8 @@
-<% title t('titles.phone_setup') %>
+<%= title t('titles.add_info.phone') %>
<%= render(VendorOutageAlertComponent.new(vendors: [:sms, :voice])) %>
-<%= render PageHeadingComponent.new.with_content(t('titles.phone_setup')) %>
+<%= render PageHeadingComponent.new.with_content(t('headings.add_info.phone')) %>
<%= t('two_factor_authentication.phone_info') %>
diff --git a/app/views/users/phones/add.html.erb b/app/views/users/phones/add.html.erb
index 3560194b531..b91627fabe5 100644
--- a/app/views/users/phones/add.html.erb
+++ b/app/views/users/phones/add.html.erb
@@ -32,7 +32,7 @@
<%= render CaptchaSubmitButtonComponent.new(
form: f,
action: PhoneRecaptchaValidator::RECAPTCHA_ACTION,
- ).with_content(t('forms.buttons.continue')) %>
+ ).with_content(t('forms.buttons.send_one_time_code')) %>
<% end %>
diff --git a/app/views/users/two_factor_authentication_setup/index.html.erb b/app/views/users/two_factor_authentication_setup/index.html.erb
index e8a52619d9e..1506a1d37b3 100644
--- a/app/views/users/two_factor_authentication_setup/index.html.erb
+++ b/app/views/users/two_factor_authentication_setup/index.html.erb
@@ -14,6 +14,20 @@
<%= @presenter.intro %>
+<% if @presenter.two_factor_enabled? %>
+
+ <%= t('headings.account.two_factor') %>
+
+
+
+ <% @presenter.options.each do |option| %>
+ <% if option.mfa_configuration_count > 0 %>
+ <%= render partial: 'partials/multi_factor_authentication/selected_mfa_option', locals: { option: option } %>
+ <% end %>
+ <% end %>
+
+<% end %>
+
<%= simple_form_for @two_factor_options_form,
html: { autocomplete: 'off' },
method: :patch,
@@ -35,4 +49,12 @@
<%= f.submit t('forms.buttons.continue'), class: 'margin-bottom-1' %>
<% end %>
-<%= render 'shared/cancel', link: logout_path, link_method: :delete %>
+<% if @presenter.skip_path || !@presenter.two_factor_enabled? %>
+ <%= render PageFooterComponent.new do %>
+ <% if @presenter.skip_path %>
+ <%= link_to t('mfa.skip'), @presenter.skip_path %>
+ <% elsif !@presenter.two_factor_enabled? %>
+ <%= link_to t('links.cancel_account_creation'), sign_up_cancel_path %>
+ <% end %>
+ <% end %>
+<% end %>
diff --git a/bin/oncall/email-deliveries b/bin/oncall/email-deliveries
index 20ef164b29b..0272438f9c4 100755
--- a/bin/oncall/email-deliveries
+++ b/bin/oncall/email-deliveries
@@ -58,7 +58,7 @@ class EmailDeliveries
results = query_data(uuids)
table = Terminal::Table.new
- table << %w[user_id timestamp message_id events]
+ table << %w[user_id timestamp message_id email_action events]
table << :separator
results.each do |result|
@@ -66,6 +66,7 @@ class EmailDeliveries
result.user_id,
result.timestamp,
result.message_id,
+ result.email_action,
result.events.join(', '),
]
end
@@ -77,6 +78,7 @@ class EmailDeliveries
:user_id,
:timestamp,
:message_id,
+ :email_action,
:events,
keyword_init: true,
)
@@ -90,6 +92,7 @@ class EmailDeliveries
@timestamp
, properties.user_id AS user_id
, properties.event_properties.ses_message_id AS ses_message_id
+ , properties.event_properties.action AS email_action
| filter name = 'Email Sent'
| filter properties.user_id IN #{quote(uuids)}
| limit 10000
@@ -122,6 +125,7 @@ class EmailDeliveries
map do |message_id, events|
Result.new(
user_id: events_by_message_id[message_id]['user_id'],
+ email_action: events_by_message_id[message_id]['email_action'],
timestamp: events_by_message_id[message_id]['@timestamp'],
message_id: message_id,
events: events.sort_by { |e| e['@timestamp'] }.map { |e| e['event_type'] },
diff --git a/config/application.yml.default b/config/application.yml.default
index 53c502edb8c..71aeeb71819 100644
--- a/config/application.yml.default
+++ b/config/application.yml.default
@@ -59,6 +59,7 @@ aws_kms_key_id: alias/login-dot-gov-test-keymaker
aws_kms_client_contextless_pool_size: 5
aws_kms_client_multi_pool_size: 5
aws_kms_multi_region_key_id: alias/login-dot-gov-keymaker-multi-region
+aws_kms_multi_region_read_enabled: false
aws_logo_bucket: ''
aws_region: 'us-west-2'
backup_code_cost: '2000$8$1$'
@@ -81,7 +82,6 @@ disable_logout_get_request: true
disallow_all_web_crawlers: true
disposable_email_services: '[]'
doc_auth_attempt_window_in_minutes: 360
-doc_auth_extend_timeout_by_minutes: 40
doc_capture_polling_enabled: true
doc_auth_client_glare_threshold: 50
doc_auth_client_sharpness_threshold: 50
diff --git a/config/initializers/job_configurations.rb b/config/initializers/job_configurations.rb
index 1845e0dc250..8cf01a96956 100644
--- a/config/initializers/job_configurations.rb
+++ b/config/initializers/job_configurations.rb
@@ -182,12 +182,6 @@
cron: cron_24h,
args: -> { [Time.zone.today] },
},
- # Send reminder letters for old, outstanding GPO verification codes
- send_gpo_code_reminders: {
- class: 'GpoReminderJob',
- cron: cron_24h,
- args: -> { [14.days.ago] },
- },
}.compact
end
# rubocop:enable Metrics/BlockLength
diff --git a/config/locales/account/en.yml b/config/locales/account/en.yml
index 13d0dd36add..2ce691e82e4 100644
--- a/config/locales/account/en.yml
+++ b/config/locales/account/en.yml
@@ -71,6 +71,8 @@ en:
delete_account: Delete
regenerate_personal_key: Reset
login:
+ forced_reauthentication_notice_html: %{sp_name} needs you to
+ enter your email and password again.
piv_cac: Sign in with your government employee ID
tab_navigation: Account creation tabs
navigation:
diff --git a/config/locales/account/es.yml b/config/locales/account/es.yml
index 86762885259..c6e124fcde8 100644
--- a/config/locales/account/es.yml
+++ b/config/locales/account/es.yml
@@ -72,6 +72,8 @@ es:
delete_account: Eliminar
regenerate_personal_key: Restablecer
login:
+ forced_reauthentication_notice_html: %{sp_name} requiere que
+ vuelvas a ingresar tu correo electrónico y contraseña.
piv_cac: Inicie sesión con su identificación de empleado del gobierno
tab_navigation: Pestañas de creación de cuenta
navigation:
diff --git a/config/locales/account/fr.yml b/config/locales/account/fr.yml
index 51ae5a45cc5..c5e969c9181 100644
--- a/config/locales/account/fr.yml
+++ b/config/locales/account/fr.yml
@@ -77,6 +77,9 @@ fr:
delete_account: Effacer
regenerate_personal_key: Réinitialiser
login:
+ forced_reauthentication_notice_html: %{sp_name} nécessite que
+ vous saisissiez à nouveau votre adresse électronique et votre mot de
+ passe.
piv_cac: Connectez-vous avec votre ID d’employé du gouvernement
tab_navigation: Onglets de création de compte
navigation:
diff --git a/config/locales/titles/en.yml b/config/locales/titles/en.yml
index f5448999dd4..4be6ec2f24c 100644
--- a/config/locales/titles/en.yml
+++ b/config/locales/titles/en.yml
@@ -52,7 +52,6 @@ en:
change: Change the password for your account
forgot: Reset password
personal_key: Just in case
- phone_setup: Get your one-time code
piv_cac_login:
add: Add your PIV or CAC
new: Use your PIV/CAC to sign in to your account
diff --git a/config/locales/titles/es.yml b/config/locales/titles/es.yml
index cdc88f7fd44..5349232ffcd 100644
--- a/config/locales/titles/es.yml
+++ b/config/locales/titles/es.yml
@@ -52,7 +52,6 @@ es:
change: Cambie la contraseña de su cuenta
forgot: Restablecer la contraseña
personal_key: Por si acaso
- phone_setup: Obtenga su código único
piv_cac_login:
add: Agregue su PIV o CAC
new: Use su PIV / CAC para iniciar sesión en su cuenta
diff --git a/config/locales/titles/fr.yml b/config/locales/titles/fr.yml
index 6297821eaa1..ac64385f278 100644
--- a/config/locales/titles/fr.yml
+++ b/config/locales/titles/fr.yml
@@ -52,7 +52,6 @@ fr:
change: Changez le mot de passe de votre compte
forgot: Réinitialisez le mot de passe
personal_key: Juste au cas
- phone_setup: Obtenez votre code à usage unique
piv_cac_login:
add: Ajoutez votre PIV ou CAC
new: Utilisez votre PIV / CAC pour vous connecter à votre compte
diff --git a/lib/cleanup/destroyable_records.rb b/lib/cleanup/destroyable_records.rb
index fb417a8109b..31228211269 100644
--- a/lib/cleanup/destroyable_records.rb
+++ b/lib/cleanup/destroyable_records.rb
@@ -29,7 +29,8 @@ def print_data
stdout.puts '********'
stdout.puts "This provider has #{in_person_enrollments.size} in person enrollments " \
- "that will be destroyed"
+ "that will be destroyed - Please handle these removals manually. " \
+ "For more details check https://cm-jira.usa.gov/browse/LG-10679"
stdout.puts "\n"
stdout.puts '*******'
diff --git a/lib/identity_config.rb b/lib/identity_config.rb
index 78f1b56f4d6..9397b94b108 100644
--- a/lib/identity_config.rb
+++ b/lib/identity_config.rb
@@ -146,6 +146,7 @@ def self.build_store(config_map)
config.add(:aws_kms_client_multi_pool_size, type: :integer)
config.add(:aws_kms_key_id, type: :string)
config.add(:aws_kms_multi_region_key_id, type: :string)
+ config.add(:aws_kms_multi_region_read_enabled, type: :boolean)
config.add(:aws_logo_bucket, type: :string)
config.add(:aws_region, type: :string)
config.add(:backup_code_cost, type: :string)
@@ -186,7 +187,6 @@ def self.build_store(config_map)
config.add(:doc_auth_error_dpi_threshold, type: :integer)
config.add(:doc_auth_error_glare_threshold, type: :integer)
config.add(:doc_auth_error_sharpness_threshold, type: :integer)
- config.add(:doc_auth_extend_timeout_by_minutes, type: :integer)
config.add(:doc_auth_max_attempts, type: :integer)
config.add(:doc_auth_max_capture_attempts_before_native_camera, type: :integer)
config.add(:doc_auth_max_submission_attempts_before_native_camera, type: :integer)
diff --git a/lib/reporting/identity_verification_report.rb b/lib/reporting/identity_verification_report.rb
index b78526922a6..2953a0a5fd7 100644
--- a/lib/reporting/identity_verification_report.rb
+++ b/lib/reporting/identity_verification_report.rb
@@ -59,7 +59,7 @@ def to_csv
CSV.generate do |csv|
csv << ['Report Timeframe', "#{time_range.begin} to #{time_range.end}"]
csv << ['Report Generated', Date.today.to_s] # rubocop:disable Rails/Date
- csv << ['Issuer', issuer]
+ csv << ['Issuer', issuer] if issuer.present?
csv << []
csv << ['Metric', '# of Users']
csv << ['Started IdV Verification', idv_doc_auth_image_vendor_submitted]
@@ -140,7 +140,7 @@ def fetch_results
def query
params = {
- issuer: quote(issuer),
+ issuer: issuer && quote(issuer),
event_names: quote(Events.all_events),
usps_enrollment_status_updated: quote(Events::USPS_ENROLLMENT_STATUS_UPDATED),
gpo_verification_submitted: quote(Events::GPO_VERIFICATION_SUBMITTED),
@@ -151,13 +151,13 @@ def query
fields
name
, properties.user_id AS user_id
- | filter properties.service_provider = %{issuer}
+ #{issuer.present? ? '| filter properties.service_provider = %{issuer}' : ''}
| filter name in %{event_names}
| filter (name = %{usps_enrollment_status_updated} and properties.event_properties.passed = 1)
or (name != %{usps_enrollment_status_updated})
- | filter (name = %{gpo_verification_submitted} and properties.event_properties.success = 1)
+ | filter (name = %{gpo_verification_submitted} and properties.event_properties.success = 1 and !properties.event_properties.pending_in_person_enrollment and !properties.event_properties.fraud_check_failed)
or (name != %{gpo_verification_submitted})
- | filter (name = %{idv_final_resolution} and isblank(properties.event_properties.deactivation_reason))
+ | filter (name = %{idv_final_resolution} and !properties.event_properties.fraud_review_pending and !properties.event_properties.gpo_verification_pending and !properties.event_properties.in_person_verification_pending)
or (name != %{idv_final_resolution})
| limit 10000
QUERY
@@ -177,7 +177,7 @@ def cloudwatch_client
# rubocop:disable Rails/Output
if __FILE__ == $PROGRAM_NAME
- options = Reporting::CommandLineOptions.new.parse!(ARGV)
+ options = Reporting::CommandLineOptions.new.parse!(ARGV, require_issuer: false)
puts Reporting::IdentityVerificationReport.new(**options).to_csv
end
diff --git a/lib/tasks/backfill_fraud_pending_reason.rake b/lib/tasks/backfill_fraud_pending_reason.rake
deleted file mode 100644
index 7668f62824b..00000000000
--- a/lib/tasks/backfill_fraud_pending_reason.rake
+++ /dev/null
@@ -1,78 +0,0 @@
-namespace :profiles do
- desc 'If a profile is in review or rejected, store the reason it was marked for fraud'
-
- ##
- # Usage:
- #
- # Print pending updates
- # bundle exec rake profiles:backfill_fraud_pending_reason
- #
- # Commit updates
- # bundle exec rake profiles:backfill_fraud_pending_reason UPDATE_PROFILES=true
- #
- task backfill_fraud_pending_reason: :environment do |_task, _args|
- ActiveRecord::Base.connection.execute('SET statement_timeout = 60000')
-
- update_profiles = ENV['UPDATE_PROFILES'] == 'true'
-
- profiles = Profile.where(
- fraud_pending_reason: nil,
- ).where(
- 'fraud_review_pending_at IS NOT NULL OR fraud_rejection_at IS NOT NULL',
- )
-
- profiles.each do |profile|
- proofing_component_status = profile.proofing_components&.[]('threatmetrix_review_status')
- fraud_pending_reason = case proofing_component_status
- when 'review'
- 'threatmetrix_review'
- when 'reject'
- 'threatmetrix_reject'
- else
- 'threatmetrix_review'
- end
-
- warn "#{profile.id},#{fraud_pending_reason},#{proofing_component_status}"
- profile.update!(fraud_pending_reason: fraud_pending_reason) if update_profiles
- end
- end
-
- ##
- # Usage:
- #
- # Rollback the above:
- #
- # export BACKFILL_OUTPUT=''
- # bundle exec rake profiles:rollback_backfill_fraud_pending_reason
- #
- task rollback_backfill_fraud_pending_reason: :environment do |_task, _args|
- ActiveRecord::Base.connection.execute('SET statement_timeout = 60000')
-
- profile_ids = ENV['BACKFILL_OUTPUT'].split("\n").map do |profile_row|
- profile_row.split(',').first
- end
-
- warn "Updating #{profile_ids.count} records"
- Profile.where(id: profile_ids).update!(fraud_pending_reason: nil)
- end
-
- ##
- # Usage:
- # bundle exec rake profiles:validate_backfill_fraud_pending_reason
- #
- task validate_backfill_fraud_pending_reason: :environment do |_task, _args|
- ActiveRecord::Base.connection.execute('SET statement_timeout = 60000')
-
- profiles = Profile.where(
- fraud_pending_reason: nil,
- ).where(
- 'fraud_review_pending_at IS NOT NULL OR fraud_rejection_at IS NOT NULL',
- )
-
- if profiles.empty?
- warn 'fraud_pending_reason backfill was successful'
- else
- warn "fraud_pending_reason backfill left #{profile.count} rows"
- end
- end
-end
diff --git a/lib/tasks/backfill_fraud_review_pending_at.rake b/lib/tasks/backfill_fraud_review_pending_at.rake
deleted file mode 100644
index 9b250fbd83c..00000000000
--- a/lib/tasks/backfill_fraud_review_pending_at.rake
+++ /dev/null
@@ -1,75 +0,0 @@
-namespace :profiles do
- desc 'If a profile is in GPO and fraud pending state, move it out of fraud pending state'
-
- ##
- # Usage:
- #
- # Print pending updates
- # bundle exec rake profiles:backfill_fraud_review_pending_at
- #
- # Commit updates
- # bundle exec rake profiles:backfill_fraud_review_pending_at UPDATE_PROFILES=true
- #
- task backfill_fraud_review_pending_at: :environment do |_task, _args|
- ActiveRecord::Base.connection.execute('SET statement_timeout = 60000')
-
- update_profiles = ENV['UPDATE_PROFILES'] == 'true'
-
- profiles = Profile.where(
- 'fraud_review_pending_at IS NOT NULL OR fraud_rejection_at IS NOT NULL',
- ).where.not(
- gpo_verification_pending_at: nil,
- )
-
- profiles.each do |profile|
- if profile.fraud_pending_reason.blank?
- warn "Profile ##{profile.id} does not have a fraud pending reason!"
- break
- end
-
- warn "#{profile.id},#{profile.fraud_review_pending_at},#{profile.fraud_rejection_at}"
- profile.update!(fraud_review_pending_at: nil, fraud_rejection_at: nil) if update_profiles
- end
- end
-
- ##
- # Usage:
- #
- # Rollback the above:
- #
- # export BACKFILL_OUTPUT=''
- # bundle exec rake profiles:rollback_backfill_fraud_review_pending_at
- #
- task rollback_backfill_fraud_review_pending_at: :environment do |_task, _args|
- ActiveRecord::Base.connection.execute('SET statement_timeout = 60000')
-
- profile_data = ENV['BACKFILL_OUTPUT'].split("\n").map do |profile_row|
- profile_row.split(',')
- end
-
- warn "Updating #{profile_data.count} records"
- profile_data.each do |profile_datum|
- profile_id, fraud_review_pending_at, fraud_rejection_at = profile_datum
- Profile.where(id: profile_id).update!(
- fraud_review_pending_at: fraud_review_pending_at,
- fraud_rejection_at: fraud_rejection_at,
- )
- end
- end
-
- ##
- # Usage:
- # bundle exec rake profiles:validate_backfill_fraud_review_pending_at
- #
- task validate_backfill_fraud_review_pending_at: :environment do |_task, _args|
- ActiveRecord::Base.connection.execute('SET statement_timeout = 60000')
-
- profiles = Profile.where(
- 'fraud_review_pending_at IS NOT NULL OR fraud_rejection_at IS NOT NULL',
- ).where.not(
- gpo_verification_pending_at: nil,
- )
-
- warn "fraud_pending_reason backfill left #{profiles.count} rows"
- end
-end
diff --git a/lib/tasks/multi_region_kms.rake b/lib/tasks/multi_region_kms.rake
new file mode 100644
index 00000000000..879667128d8
--- /dev/null
+++ b/lib/tasks/multi_region_kms.rake
@@ -0,0 +1,92 @@
+namespace :multi_region_kms do
+ desc 'Confirm that the multi-region KMS inner-layers are the same for both ciphertexts'
+ task check_inner_layer: :environment do |_task, _args|
+ ActiveRecord::Base.connection.execute('SET statement_timeout = 60000')
+
+ sample_password_users =
+ User.where.not(encrypted_password_digest_multi_region: nil).limit(10000).all
+ sample_personal_key_users =
+ User.where.not(encrypted_recovery_code_digest_multi_region: nil).limit(10000).all
+ sample_profiles = Profile.where.not(encrypted_pii_multi_region: nil).limit(10000).all
+
+ kms_client = Encryption::KmsClient.new
+
+ mismatched_records = []
+
+ sample_password_users.each do |user|
+ kms_context = {
+ 'context' => 'password-digest',
+ 'user_uuid' => user.uuid,
+ }
+
+ mr_inner_layer = kms_client.decrypt(
+ JSON.parse(user.encrypted_password_digest_multi_region)['encrypted_password'], kms_context
+ )
+ sr_inner_layer = kms_client.decrypt(
+ JSON.parse(user.encrypted_password_digest)['encrypted_password'], kms_context
+ )
+
+ if mr_inner_layer != sr_inner_layer
+ warn "Mismatch identified: User##{user.id}"
+ mismatched_records.push(user)
+ end
+ end
+
+ sample_personal_key_users.each do |user|
+ kms_context = {
+ 'context' => 'password-digest',
+ 'user_uuid' => user.uuid,
+ }
+
+ mr_inner_layer = kms_client.decrypt(
+ JSON.parse(user.encrypted_recovery_code_digest_multi_region)['encrypted_password'],
+ kms_context,
+ )
+ sr_inner_layer = kms_client.decrypt(
+ JSON.parse(user.encrypted_recovery_code_digest)['encrypted_password'], kms_context
+ )
+
+ if mr_inner_layer != sr_inner_layer
+ warn "Mismatch identified: User##{user.id}"
+ mismatched_records.push(user)
+ end
+ end
+
+ sample_profiles.each do |profile|
+ kms_context = {
+ 'context' => 'pii-encryption',
+ 'user_uuid' => profile.user.uuid,
+ }
+
+ mr_pii_inner_layer = kms_client.decrypt(
+ Base64.decode64(JSON.parse(profile.encrypted_pii_multi_region)['encrypted_data']),
+ kms_context,
+ )
+ sr_pii_inner_layer = kms_client.decrypt(
+ Base64.decode64(JSON.parse(profile.encrypted_pii)['encrypted_data']),
+ kms_context,
+ )
+ mr_pii_recovery_inner_layer = kms_client.decrypt(
+ Base64.decode64(JSON.parse(profile.encrypted_pii_recovery_multi_region)['encrypted_data']),
+ kms_context,
+ )
+ sr_pii_recovery_inner_layer = kms_client.decrypt(
+ Base64.decode64(JSON.parse(profile.encrypted_pii_recovery)['encrypted_data']),
+ kms_context,
+ )
+
+ mistmatch_detected = mr_pii_inner_layer != sr_pii_inner_layer ||
+ mr_pii_recovery_inner_layer != sr_pii_recovery_inner_layer
+
+ if mistmatch_detected
+ warn "Mismatch identified: Profile##{profile.id}"
+ mismatched_records.push(profile)
+ end
+ end
+
+ warn "Sampled #{sample_password_users.size} passwords"
+ warn "Sampled #{sample_personal_key_users.size} personal keys"
+ warn "Sampled #{sample_profiles.size} encrypted PII records"
+ warn "#{mismatched_records.size} mismatched records detected"
+ end
+end
diff --git a/spec/bin/oncall/email-deliveries_spec.rb b/spec/bin/oncall/email-deliveries_spec.rb
index 2b176c1b91c..3bee52b0643 100644
--- a/spec/bin/oncall/email-deliveries_spec.rb
+++ b/spec/bin/oncall/email-deliveries_spec.rb
@@ -58,8 +58,8 @@
# rubocop:disable Layout/LineLength
let(:events_log) do
[
- { '@timestamp' => '2023-01-01 00:00:01', 'user_id' => 'abc123', 'ses_message_id' => 'message-1' },
- { '@timestamp' => '2023-01-01 00:00:02', 'user_id' => 'def456', 'ses_message_id' => 'message-2' },
+ { '@timestamp' => '2023-01-01 00:00:01', 'user_id' => 'abc123', 'email_action' => 'forgot_password', 'ses_message_id' => 'message-1' },
+ { '@timestamp' => '2023-01-01 00:00:02', 'user_id' => 'def456', 'email_action' => 'forgot_password', 'ses_message_id' => 'message-2' },
]
end
@@ -80,9 +80,9 @@
expect(table).to eq(
[
- ['user_id', 'timestamp', 'message_id', 'events'],
- ['abc123', '2023-01-01 00:00:01', 'message-1', 'Send, Delivery'],
- ['def456', '2023-01-01 00:00:02', 'message-2', 'Send, Bounce'],
+ ['user_id', 'timestamp', 'message_id', 'email_action', 'events'],
+ ['abc123', '2023-01-01 00:00:01', 'message-1', 'forgot_password', 'Send, Delivery'],
+ ['def456', '2023-01-01 00:00:02', 'message-2', 'forgot_password', 'Send, Bounce'],
],
)
end
diff --git a/spec/components/page_footer_component_spec.rb b/spec/components/page_footer_component_spec.rb
index 69e3a7ee0bb..43738a57cfc 100644
--- a/spec/components/page_footer_component_spec.rb
+++ b/spec/components/page_footer_component_spec.rb
@@ -6,6 +6,7 @@
rendered = render_inline PageFooterComponent.new.with_content(content)
expect(rendered).to have_content(content)
+ expect(rendered).to have_css('.page-footer')
end
context 'tag options' do
@@ -20,7 +21,7 @@
it 'appends custom class' do
rendered = render_inline PageFooterComponent.new(class: 'custom-class')
- expect(rendered).to have_css('.custom-class')
+ expect(rendered).to have_css('.page-footer.custom-class')
end
end
end
diff --git a/spec/controllers/concerns/forced_reauthentication_concern_spec.rb b/spec/controllers/concerns/forced_reauthentication_concern_spec.rb
new file mode 100644
index 00000000000..f2a5cf4dd30
--- /dev/null
+++ b/spec/controllers/concerns/forced_reauthentication_concern_spec.rb
@@ -0,0 +1,50 @@
+require 'rails_helper'
+
+RSpec.describe ForcedReauthenticationConcern do
+ let(:test_class) do
+ Class.new do
+ include ForcedReauthenticationConcern
+
+ attr_reader :session
+
+ def initialize(session = {})
+ @session = session
+ end
+ end
+ end
+ let(:instance) { test_class.new }
+
+ describe '#issuer_forced_reauthentication?' do
+ it 'returns true if issuer has forced reauthentication' do
+ instance.set_issuer_forced_reauthentication(
+ issuer: 'test_issuer',
+ is_forced_reauthentication: true,
+ )
+ expect(instance.issuer_forced_reauthentication?(issuer: 'test_issuer')).to eq true
+ end
+
+ it 'returns false if issuer has not forced reauthentication' do
+ expect(instance.issuer_forced_reauthentication?(issuer: 'test_issuer')).to eq false
+ end
+
+ it 'returns false if forced reauthentication is set to false for an issuer' do
+ instance.set_issuer_forced_reauthentication(
+ issuer: 'test_issuer',
+ is_forced_reauthentication: false,
+ )
+ expect(instance.issuer_forced_reauthentication?(issuer: 'test_issuer')).to eq false
+ end
+
+ it 'returns false if issuer sets forced reauthentication to true and then false' do
+ instance.set_issuer_forced_reauthentication(
+ issuer: 'test_issuer',
+ is_forced_reauthentication: true,
+ )
+ instance.set_issuer_forced_reauthentication(
+ issuer: 'test_issuer',
+ is_forced_reauthentication: false,
+ )
+ expect(instance.issuer_forced_reauthentication?(issuer: 'test_issuer')).to eq false
+ end
+ end
+end
diff --git a/spec/controllers/concerns/idv/ab_test_analytics_concern_spec.rb b/spec/controllers/concerns/idv/ab_test_analytics_concern_spec.rb
index 5181f0a8470..7a8c8b9cd34 100644
--- a/spec/controllers/concerns/idv/ab_test_analytics_concern_spec.rb
+++ b/spec/controllers/concerns/idv/ab_test_analytics_concern_spec.rb
@@ -8,6 +8,9 @@ class StepController < ApplicationController
end
let(:user) { create(:user) }
+ let(:idv_session) do
+ Idv::Session.new(user_session: subject.user_session, current_user: user, service_provider: nil)
+ end
describe '#ab_test_analytics_buckets' do
controller Idv::StepController do
@@ -24,12 +27,29 @@ class StepController < ApplicationController
and_return(getting_started_args)
end
- it 'includes acuant_sdk_ab_test_analytics_args' do
- expect(controller.ab_test_analytics_buckets).to include(acuant_sdk_args)
+ context 'idv_session is available' do
+ before do
+ sign_in(user)
+ expect(subject).to receive(:idv_session).and_return(idv_session)
+ end
+ it 'includes acuant_sdk_ab_test_analytics_args' do
+ expect(controller.ab_test_analytics_buckets).to include(acuant_sdk_args)
+ end
+
+ it 'includes getting_started_ab_test_analytics_bucket' do
+ expect(controller.ab_test_analytics_buckets).to include(getting_started_args)
+ end
+
+ it 'includes skip_hybrid_handoff' do
+ idv_session.skip_hybrid_handoff = :shh_value
+ expect(controller.ab_test_analytics_buckets).to include({ skip_hybrid_handoff: :shh_value })
+ end
end
- it 'includes getting_started_ab_test_analytics_bucket' do
- expect(controller.ab_test_analytics_buckets).to include(getting_started_args)
+ context 'idv_session is not available' do
+ it 'still works' do
+ expect(controller.ab_test_analytics_buckets).to include(acuant_sdk_args)
+ end
end
end
end
diff --git a/spec/controllers/frontend_log_controller_spec.rb b/spec/controllers/frontend_log_controller_spec.rb
index 1e84d535bc9..8eda1f642f5 100644
--- a/spec/controllers/frontend_log_controller_spec.rb
+++ b/spec/controllers/frontend_log_controller_spec.rb
@@ -195,5 +195,12 @@
expect(request.session_options[:skip]).to eql(true)
end
end
+
+ context 'with all events' do
+ it 'sorts keys alphabetically' do
+ expect(described_class::EVENT_MAP.keys).
+ to eq(described_class::EVENT_MAP.keys.sort_by(&:downcase))
+ end
+ end
end
end
diff --git a/spec/controllers/idv/agreement_controller_spec.rb b/spec/controllers/idv/agreement_controller_spec.rb
index dc252a1d32b..788ea74716a 100644
--- a/spec/controllers/idv/agreement_controller_spec.rb
+++ b/spec/controllers/idv/agreement_controller_spec.rb
@@ -39,6 +39,7 @@
{
step: 'agreement',
analytics_id: 'Doc Auth',
+ skip_hybrid_handoff: nil,
irs_reproofing: false,
}.merge(ab_test_args)
end
@@ -93,6 +94,7 @@
errors: {},
step: 'agreement',
analytics_id: 'Doc Auth',
+ skip_hybrid_handoff: nil,
irs_reproofing: false,
}.merge(ab_test_args)
end
@@ -134,7 +136,9 @@
put :update, params: params
end.to change {
subject.idv_session.flow_path
- }.from(nil).to('standard')
+ }.from(nil).to('standard').and change {
+ subject.idv_session.skip_hybrid_handoff
+ }.from(nil).to(true)
end
it 'redirects to hybrid handoff' do
diff --git a/spec/controllers/idv/document_capture_controller_spec.rb b/spec/controllers/idv/document_capture_controller_spec.rb
index 248b7dc1184..1c1bde51257 100644
--- a/spec/controllers/idv/document_capture_controller_spec.rb
+++ b/spec/controllers/idv/document_capture_controller_spec.rb
@@ -56,6 +56,8 @@
{
analytics_id: 'Doc Auth',
flow_path: 'standard',
+ redo_document_capture: nil,
+ skip_hybrid_handoff: nil,
irs_reproofing: false,
step: 'document_capture',
}.merge(ab_test_args)
@@ -147,6 +149,8 @@
errors: {},
analytics_id: 'Doc Auth',
flow_path: 'standard',
+ redo_document_capture: nil,
+ skip_hybrid_handoff: nil,
irs_reproofing: false,
step: 'document_capture',
}.merge(ab_test_args)
diff --git a/spec/controllers/idv/getting_started_controller_spec.rb b/spec/controllers/idv/getting_started_controller_spec.rb
index 17cbb438c31..d47e4204838 100644
--- a/spec/controllers/idv/getting_started_controller_spec.rb
+++ b/spec/controllers/idv/getting_started_controller_spec.rb
@@ -38,6 +38,7 @@
{
step: 'getting_started',
analytics_id: 'Doc Auth',
+ skip_hybrid_handoff: nil,
irs_reproofing: false,
}.merge(ab_test_args)
end
@@ -100,6 +101,7 @@
errors: {},
step: 'getting_started',
analytics_id: 'Doc Auth',
+ skip_hybrid_handoff: nil,
irs_reproofing: false,
}.merge(ab_test_args)
end
@@ -161,7 +163,9 @@
put :update, params: params
end.to change {
subject.idv_session.flow_path
- }.from(nil).to('standard')
+ }.from(nil).to('standard').and change {
+ subject.idv_session.skip_hybrid_handoff
+ }.from(nil).to(true)
end
it 'redirects to hybrid handoff' do
diff --git a/spec/controllers/idv/hybrid_handoff_controller_spec.rb b/spec/controllers/idv/hybrid_handoff_controller_spec.rb
index 361d1642996..59f63c2fd25 100644
--- a/spec/controllers/idv/hybrid_handoff_controller_spec.rb
+++ b/spec/controllers/idv/hybrid_handoff_controller_spec.rb
@@ -54,6 +54,8 @@
{
step: 'hybrid_handoff',
analytics_id: 'Doc Auth',
+ redo_document_capture: nil,
+ skip_hybrid_handoff: nil,
irs_reproofing: false,
}.merge(ab_test_args)
end
@@ -200,6 +202,8 @@
flow_path: 'hybrid',
step: 'hybrid_handoff',
analytics_id: 'Doc Auth',
+ redo_document_capture: nil,
+ skip_hybrid_handoff: nil,
irs_reproofing: false,
telephony_response: {
errors: {},
@@ -250,6 +254,8 @@
flow_path: 'standard',
step: 'hybrid_handoff',
analytics_id: 'Doc Auth',
+ redo_document_capture: nil,
+ skip_hybrid_handoff: nil,
irs_reproofing: false,
}.merge(ab_test_args)
end
diff --git a/spec/controllers/idv/in_person/ssn_controller_spec.rb b/spec/controllers/idv/in_person/ssn_controller_spec.rb
index df614b291e9..ae89bc25a9a 100644
--- a/spec/controllers/idv/in_person/ssn_controller_spec.rb
+++ b/spec/controllers/idv/in_person/ssn_controller_spec.rb
@@ -79,14 +79,8 @@
)
end
- it 'adds a session id to flow session' do
- get :show
- expect(flow_session[:threatmetrix_session_id]).to_not eq(nil)
- end
-
it 'adds a threatmetrix session id to idv_session' do
- get :show
- expect(subject.idv_session.threatmetrix_session_id).to_not eq(nil)
+ expect { get :show }.to change { subject.idv_session.threatmetrix_session_id }.from(nil)
end
context 'with an ssn in session' do
@@ -179,14 +173,6 @@
end
it 'does not change threatmetrix_session_id when updating ssn' do
- flow_session[:pii_from_user][:ssn] = ssn
- put :update, params: params
- session_id = flow_session[:threatmetrix_session_id]
- subject.threatmetrix_view_variables
- expect(flow_session[:threatmetrix_session_id]).to eq(session_id)
- end
-
- it 'does not change idv_session threatmetrix_session_id when updating ssn' do
flow_session[:pii_from_user][:ssn] = ssn
put :update, params: params
session_id = subject.idv_session.threatmetrix_session_id
diff --git a/spec/controllers/idv/in_person/verify_info_controller_spec.rb b/spec/controllers/idv/in_person/verify_info_controller_spec.rb
index db010800968..3cd9a7773dd 100644
--- a/spec/controllers/idv/in_person/verify_info_controller_spec.rb
+++ b/spec/controllers/idv/in_person/verify_info_controller_spec.rb
@@ -56,7 +56,6 @@
before do
stub_analytics
stub_attempts_tracker
- allow(@analytics).to receive(:track_event)
end
describe '#show' do
@@ -67,8 +66,6 @@
flow_path: 'standard',
irs_reproofing: false,
step: 'verify',
- same_address_as_id: true,
- pii_like_keypaths: [[:same_address_as_id], [:state_id, :state_id_jurisdiction]],
}.merge(ab_test_args)
end
@@ -81,7 +78,46 @@
it 'sends analytics_visited event' do
get :show
- expect(@analytics).to have_received(:track_event).with(analytics_name, analytics_args)
+ expect(@analytics).to have_logged_event(
+ 'IdV: doc auth verify visited',
+ hash_including(**analytics_args, same_address_as_id: true),
+ )
+ end
+
+ context 'when done' do
+ let(:review_status) { 'review' }
+ let(:async_state) { instance_double(ProofingSessionAsyncResult) }
+ let(:adjudicated_result) do
+ {
+ context: {
+ stages: {
+ threatmetrix: {
+ transaction_id: 1,
+ review_status: review_status,
+ response_body: {
+ tmx_summary_reason_code: ['Identity_Negative_History'],
+ },
+ },
+ },
+ },
+ errors: {},
+ exception: nil,
+ success: true,
+ threatmetrix_review_status: review_status,
+ }
+ end
+ it 'logs proofing results with analytics_id' do
+ allow(controller).to receive(:load_async_state).and_return(async_state)
+ allow(async_state).to receive(:done?).and_return(true)
+ allow(async_state).to receive(:result).and_return(adjudicated_result)
+
+ get :show
+
+ expect(@analytics).to have_logged_event(
+ 'IdV: doc auth verify proofing results',
+ hash_including(**analytics_args, success: true),
+ )
+ end
end
end
diff --git a/spec/controllers/idv/ssn_controller_spec.rb b/spec/controllers/idv/ssn_controller_spec.rb
index 61b109a0b9f..14cb43f5726 100644
--- a/spec/controllers/idv/ssn_controller_spec.rb
+++ b/spec/controllers/idv/ssn_controller_spec.rb
@@ -88,6 +88,10 @@
)
end
+ it 'adds a threatmetrix session id to idv_session' do
+ expect { get :show }.to change { subject.idv_session.threatmetrix_session_id }.from(nil)
+ end
+
context 'with an ssn in session' do
let(:referer) { idv_document_capture_url }
before do
@@ -216,27 +220,7 @@
end
end
- it 'adds a threatmetrix session id to flow session' do
- put :update, params: params
- subject.threatmetrix_view_variables
- expect(flow_session[:threatmetrix_session_id]).to_not eq(nil)
- end
-
- it 'does not change flow_session threatmetrix_session_id when updating ssn' do
- flow_session['pii_from_doc'][:ssn] = ssn
- put :update, params: params
- session_id = flow_session[:threatmetrix_session_id]
- subject.threatmetrix_view_variables
- expect(flow_session[:threatmetrix_session_id]).to eq(session_id)
- end
-
- it 'adds a threatmetrix session id to idv_session' do
- put :update, params: params
- subject.threatmetrix_view_variables
- expect(subject.idv_session.threatmetrix_session_id).to_not eq(nil)
- end
-
- it 'does not change idv_session threatmetrix_session_id when updating ssn' do
+ it 'does not change threatmetrix_session_id when updating ssn' do
flow_session['pii_from_doc'][:ssn] = ssn
put :update, params: params
session_id = subject.idv_session.threatmetrix_session_id
diff --git a/spec/controllers/idv/verify_info_controller_spec.rb b/spec/controllers/idv/verify_info_controller_spec.rb
index b830d7d04f2..a1f41d940d9 100644
--- a/spec/controllers/idv/verify_info_controller_spec.rb
+++ b/spec/controllers/idv/verify_info_controller_spec.rb
@@ -321,12 +321,12 @@
expect(response).to redirect_to idv_phone_url
end
- it 'logs an event' do
+ it 'logs an event with analytics_id set' do
put :show
expect(@analytics).to have_logged_event(
'IdV: doc auth verify proofing results',
- hash_including(success: true),
+ hash_including(**analytics_args, success: true, analytics_id: 'Doc Auth'),
)
end
diff --git a/spec/controllers/users/phone_setup_controller_spec.rb b/spec/controllers/users/phone_setup_controller_spec.rb
index f4706ef42b3..f65feba8996 100644
--- a/spec/controllers/users/phone_setup_controller_spec.rb
+++ b/spec/controllers/users/phone_setup_controller_spec.rb
@@ -17,11 +17,14 @@
end
context 'when signed in' do
- it 'renders the index view' do
+ let(:user) { build(:user, otp_delivery_preference: 'voice') }
+ before do
stub_analytics
- user = build(:user, otp_delivery_preference: 'voice')
stub_sign_in_before_2fa(user)
+ subject.user_session[:mfa_selections] = ['voice']
+ end
+ it 'renders the index view' do
expect(@analytics).to receive(:track_event).
with('User Registration: phone setup visited',
{ enabled_mfa_methods_count: 0 })
@@ -37,18 +40,6 @@
end
end
- context 'when fully registered and signed in' do
- it 'redirects to account page' do
- stub_analytics
- user = build(:user, :with_phone)
- stub_sign_in(user)
-
- get :index
-
- expect(response).to redirect_to(account_path)
- end
- end
-
context 'when fully registered and partially signed in' do
it 'redirects to 2FA page' do
stub_analytics
@@ -152,7 +143,7 @@
expect(response).to redirect_to(
otp_send_path(
otp_delivery_selection_form: { otp_delivery_preference: 'voice',
- otp_make_default_number: nil },
+ otp_make_default_number: false },
),
)
@@ -192,7 +183,7 @@
expect(response).to redirect_to(
otp_send_path(
otp_delivery_selection_form: { otp_delivery_preference: 'sms',
- otp_make_default_number: nil },
+ otp_make_default_number: false },
),
)
@@ -231,7 +222,7 @@
expect(response).to redirect_to(
otp_send_path(
otp_delivery_selection_form: { otp_delivery_preference: 'sms',
- otp_make_default_number: nil },
+ otp_make_default_number: false },
),
)
diff --git a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb
index 825c14cddd7..f1234ffd6b6 100644
--- a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb
+++ b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb
@@ -6,10 +6,12 @@
stub_sign_in_before_2fa
stub_analytics
- expect(@analytics).to receive(:track_event).
- with('User Registration: 2FA Setup visited')
-
get :index
+
+ expect(@analytics).to have_logged_event(
+ 'User Registration: 2FA Setup visited',
+ enabled_mfa_methods_count: 0,
+ )
end
context 'when signed out' do
@@ -21,13 +23,17 @@
end
context 'when fully authenticated and MFA enabled' do
- it 'loads the account page' do
- user = build(:user, :fully_registered)
+ it 'logs the visit event with mfa method count' do
+ user = build(:user, :with_phone)
stub_sign_in(user)
+ stub_analytics
get :index
- expect(response).to redirect_to(account_url)
+ expect(@analytics).to have_logged_event(
+ 'User Registration: 2FA Setup visited',
+ enabled_mfa_methods_count: 1,
+ )
end
end
diff --git a/spec/features/accessibility/user_pages_spec.rb b/spec/features/accessibility/user_pages_spec.rb
index d86cf271a55..5b33372668a 100644
--- a/spec/features/accessibility/user_pages_spec.rb
+++ b/spec/features/accessibility/user_pages_spec.rb
@@ -138,7 +138,7 @@
scenario 'add phone page' do
sign_in_and_2fa_user
- visit add_phone_path
+ visit phone_setup_path
expect_page_to_have_no_accessibility_violations(page)
end
diff --git a/spec/features/event_disavowal_spec.rb b/spec/features/event_disavowal_spec.rb
index e837a4c5c13..661bedc7130 100644
--- a/spec/features/event_disavowal_spec.rb
+++ b/spec/features/event_disavowal_spec.rb
@@ -43,13 +43,13 @@
scenario 'disavowing a phone being added' do
sign_in_and_2fa_user(user)
- visit add_phone_path
+ visit phone_setup_path
fill_in 'new_phone_form[phone]', with: '202-555-3434'
choose 'new_phone_form_otp_delivery_preference_sms'
check 'new_phone_form_otp_make_default_number'
- click_button t('forms.buttons.continue')
+ click_button t('forms.buttons.send_one_time_code')
submit_prefilled_otp_code(user, 'sms')
diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb
index 469187584a4..b7cf54dcdf1 100644
--- a/spec/features/idv/analytics_spec.rb
+++ b/spec/features/idv/analytics_spec.rb
@@ -38,28 +38,28 @@
let(:happy_path_events) do
{
'IdV: intro visited' => {},
- 'IdV: doc auth welcome visited' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default },
- 'IdV: doc auth welcome submitted' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default },
- 'IdV: doc auth agreement visited' => { step: 'agreement', analytics_id: 'Doc Auth', irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default },
+ 'IdV: doc auth welcome visited' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil },
+ 'IdV: doc auth welcome submitted' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil },
+ 'IdV: doc auth agreement visited' => { step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default },
'IdV: consent checkbox toggled' => { checked: true },
- 'IdV: doc auth agreement submitted' => { success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default },
- 'IdV: doc auth hybrid handoff visited' => { step: 'hybrid_handoff', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false },
- 'IdV: doc auth hybrid handoff submitted' => { success: true, errors: {}, destination: :document_capture, flow_path: 'standard', step: 'hybrid_handoff', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false },
- 'IdV: doc auth document_capture visited' => { flow_path: 'standard', step: 'document_capture', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false },
+ 'IdV: doc auth agreement submitted' => { success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default },
+ 'IdV: doc auth hybrid handoff visited' => { step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false },
+ 'IdV: doc auth hybrid handoff submitted' => { success: true, errors: {}, destination: :document_capture, flow_path: 'standard', step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false },
+ 'IdV: doc auth document_capture visited' => { flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false },
'Frontend: IdV: front image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO' },
'Frontend: IdV: back image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO' },
'IdV: doc auth image upload form submitted' => { success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String) },
'IdV: doc auth image upload vendor pii validation' => { success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String) },
- 'IdV: doc auth document_capture submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'document_capture', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false },
- 'IdV: doc auth ssn visited' => { flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false },
- 'IdV: doc auth ssn submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false },
- 'IdV: doc auth verify visited' => { flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false },
- 'IdV: doc auth verify submitted' => { flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false },
- 'IdV: doc auth verify proofing results' => { success: true, errors: {}, address_edited: false, address_line2_present: false, ssn_is_unique: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default,
+ 'IdV: doc auth document_capture submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false },
+ 'IdV: doc auth ssn visited' => { flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false },
+ 'IdV: doc auth ssn submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false },
+ 'IdV: doc auth verify visited' => { flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false },
+ 'IdV: doc auth verify submitted' => { flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false },
+ 'IdV: doc auth verify proofing results' => { success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, irs_reproofing: false, skip_hybrid_handoff: nil,
proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', double_address_verification: false, resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired' }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } },
- 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default,
+ 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil,
proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } },
- 'IdV: phone confirmation form' => { success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, otp_delivery_preference: 'sms',
+ 'IdV: phone confirmation form' => { success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, otp_delivery_preference: 'sms',
proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } },
'IdV: phone confirmation vendor' => { success: true, errors: {}, vendor: { exception: nil, vendor_name: 'AddressMock', transaction_id: 'address-mock-transaction-id-123', timed_out: false, reference: '' }, new_phone_added: false, area_code: '202', country_code: 'US', phone_fingerprint: anything,
proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } },
@@ -68,11 +68,11 @@
'IdV: phone confirmation otp visited' => { proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } },
'IdV: phone confirmation otp submitted' => { success: true, code_expired: false, code_matches: true, second_factor_attempts_count: 0, second_factor_locked_at: nil, errors: {},
proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } },
- 'IdV: review info visited' => { address_verification_method: 'phone', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default,
+ 'IdV: review info visited' => { address_verification_method: 'phone', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil,
proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } },
- 'IdV: review complete' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil,
+ 'IdV: review complete' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil,
proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } },
- 'IdV: final resolution' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil,
+ 'IdV: final resolution' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil,
proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } },
'IdV: personal key visited' => { address_verification_method: 'phone',
proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } },
@@ -86,35 +86,35 @@
let(:gpo_path_events) do
{
'IdV: intro visited' => {},
- 'IdV: doc auth welcome visited' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default },
- 'IdV: doc auth welcome submitted' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default },
- 'IdV: doc auth agreement visited' => { step: 'agreement', analytics_id: 'Doc Auth', irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default },
- 'IdV: doc auth agreement submitted' => { success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default },
- 'IdV: doc auth hybrid handoff visited' => { step: 'hybrid_handoff', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false },
- 'IdV: doc auth hybrid handoff submitted' => { success: true, errors: {}, destination: :document_capture, flow_path: 'standard', step: 'hybrid_handoff', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false },
- 'IdV: doc auth document_capture visited' => { flow_path: 'standard', step: 'document_capture', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false },
+ 'IdV: doc auth welcome visited' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil },
+ 'IdV: doc auth welcome submitted' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil },
+ 'IdV: doc auth agreement visited' => { step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default },
+ 'IdV: doc auth agreement submitted' => { success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default },
+ 'IdV: doc auth hybrid handoff visited' => { step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false },
+ 'IdV: doc auth hybrid handoff submitted' => { success: true, errors: {}, destination: :document_capture, flow_path: 'standard', redo_document_capture: nil, step: 'hybrid_handoff', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false },
+ 'IdV: doc auth document_capture visited' => { flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false },
'Frontend: IdV: front image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO' },
'Frontend: IdV: back image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO' },
'IdV: doc auth image upload form submitted' => { success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String) },
'IdV: doc auth image upload vendor pii validation' => { success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String) },
- 'IdV: doc auth document_capture submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'document_capture', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false },
- 'IdV: doc auth ssn visited' => { flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false },
- 'IdV: doc auth ssn submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false },
- 'IdV: doc auth verify visited' => { flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false },
- 'IdV: doc auth verify submitted' => { flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false },
- 'IdV: doc auth verify proofing results' => { success: true, errors: {}, address_edited: false, address_line2_present: false, ssn_is_unique: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default,
- proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', double_address_verification: false, should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired' }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } },
- 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default,
+ 'IdV: doc auth document_capture submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false },
+ 'IdV: doc auth ssn visited' => { flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false },
+ 'IdV: doc auth ssn submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false },
+ 'IdV: doc auth verify visited' => { flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false },
+ 'IdV: doc auth verify submitted' => { flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false },
+ 'IdV: doc auth verify proofing results' => { success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, irs_reproofing: false, skip_hybrid_handoff: nil,
+ proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', double_address_verification: false, resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired' }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } },
+ 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil,
proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } },
- 'IdV: USPS address letter requested' => { resend: false, phone_step_attempts: 0, first_letter_requested_at: nil, hours_since_first_letter: 0, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default,
+ 'IdV: USPS address letter requested' => { resend: false, phone_step_attempts: 0, first_letter_requested_at: nil, hours_since_first_letter: 0, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil,
proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } },
- 'IdV: review info visited' => { address_verification_method: 'gpo', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default,
+ 'IdV: review info visited' => { address_verification_method: 'gpo', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil,
proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } },
- 'IdV: USPS address letter enqueued' => { enqueued_at: Time.zone.now.utc, resend: false, phone_step_attempts: 0, first_letter_requested_at: Time.zone.now.utc, hours_since_first_letter: 0, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default,
+ 'IdV: USPS address letter enqueued' => { enqueued_at: Time.zone.now.utc, resend: false, phone_step_attempts: 0, first_letter_requested_at: Time.zone.now.utc, hours_since_first_letter: 0, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil,
proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } },
- 'IdV: review complete' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: true, in_person_verification_pending: false, deactivation_reason: nil,
+ 'IdV: review complete' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: true, in_person_verification_pending: false, deactivation_reason: nil,
proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } },
- 'IdV: final resolution' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: true, in_person_verification_pending: false, deactivation_reason: nil,
+ 'IdV: final resolution' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: true, in_person_verification_pending: false, deactivation_reason: nil,
proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } },
'IdV: come back later visited' => { proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } },
}
@@ -122,13 +122,13 @@
let(:in_person_path_events) do
{
- 'IdV: doc auth welcome visited' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default },
- 'IdV: doc auth welcome submitted' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default },
- 'IdV: doc auth agreement visited' => { step: 'agreement', analytics_id: 'Doc Auth', irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default },
- 'IdV: doc auth agreement submitted' => { success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default },
- 'IdV: doc auth hybrid handoff visited' => { step: 'hybrid_handoff', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false },
- 'IdV: doc auth hybrid handoff submitted' => { success: true, errors: {}, destination: :document_capture, flow_path: 'standard', step: 'hybrid_handoff', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false },
- 'IdV: doc auth document_capture visited' => { flow_path: 'standard', step: 'document_capture', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', irs_reproofing: false },
+ 'IdV: doc auth welcome visited' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil },
+ 'IdV: doc auth welcome submitted' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil },
+ 'IdV: doc auth agreement visited' => { step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default },
+ 'IdV: doc auth agreement submitted' => { success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default },
+ 'IdV: doc auth hybrid handoff visited' => { step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false },
+ 'IdV: doc auth hybrid handoff submitted' => { success: true, errors: {}, destination: :document_capture, flow_path: 'standard', redo_document_capture: nil, step: 'hybrid_handoff', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false },
+ 'IdV: doc auth document_capture visited' => { flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false },
'Frontend: IdV: front image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO' },
'Frontend: IdV: back image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO' },
'IdV: doc auth image upload form submitted' => { success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String) },
@@ -142,13 +142,13 @@
'IdV: in person proofing state_id submitted' => { success: true, flow_path: 'standard', step: 'state_id', step_count: 1, analytics_id: 'In Person Proofing', irs_reproofing: false, errors: {}, same_address_as_id: nil },
'IdV: in person proofing address visited' => { step: 'address', flow_path: 'standard', step_count: 1, analytics_id: 'In Person Proofing', irs_reproofing: false },
'IdV: in person proofing address submitted' => { success: true, step: 'address', flow_path: 'standard', step_count: 1, analytics_id: 'In Person Proofing', irs_reproofing: false, errors: {}, same_address_as_id: true },
- 'IdV: doc auth ssn visited' => { analytics_id: 'In Person Proofing', step: 'ssn', flow_path: 'standard', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, acuant_sdk_upgrade_ab_test_bucket: :default, same_address_as_id: true },
- 'IdV: doc auth ssn submitted' => { analytics_id: 'In Person Proofing', success: true, step: 'ssn', flow_path: 'standard', irs_reproofing: false, errors: {}, getting_started_ab_test_bucket: :welcome_default, acuant_sdk_upgrade_ab_test_bucket: :default, same_address_as_id: true },
- 'IdV: doc auth verify visited' => { analytics_id: 'In Person Proofing', step: 'verify', flow_path: 'standard', irs_reproofing: false, same_address_as_id: true, getting_started_ab_test_bucket: :welcome_default, acuant_sdk_upgrade_ab_test_bucket: :default },
- 'IdV: doc auth verify submitted' => { analytics_id: 'In Person Proofing', step: 'verify', flow_path: 'standard', irs_reproofing: false, same_address_as_id: true, getting_started_ab_test_bucket: :welcome_default, acuant_sdk_upgrade_ab_test_bucket: :default },
- 'IdV: doc auth verify proofing results' => { success: true, errors: {}, address_edited: false, address_line2_present: false, ssn_is_unique: true, getting_started_ab_test_bucket: :welcome_default, acuant_sdk_upgrade_ab_test_bucket: :default,
+ 'IdV: doc auth ssn visited' => { analytics_id: 'In Person Proofing', step: 'ssn', flow_path: 'standard', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, same_address_as_id: true },
+ 'IdV: doc auth ssn submitted' => { analytics_id: 'In Person Proofing', success: true, step: 'ssn', flow_path: 'standard', irs_reproofing: false, errors: {}, getting_started_ab_test_bucket: :welcome_default, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, same_address_as_id: true },
+ 'IdV: doc auth verify visited' => { analytics_id: 'In Person Proofing', step: 'verify', flow_path: 'standard', irs_reproofing: false, same_address_as_id: true, getting_started_ab_test_bucket: :welcome_default, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil },
+ 'IdV: doc auth verify submitted' => { analytics_id: 'In Person Proofing', step: 'verify', flow_path: 'standard', irs_reproofing: false, same_address_as_id: true, getting_started_ab_test_bucket: :welcome_default, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil },
+ 'IdV: doc auth verify proofing results' => { success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'In Person Proofing', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, irs_reproofing: false, skip_hybrid_handoff: nil,
proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', double_address_verification: false, resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired' }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } },
- 'IdV: phone confirmation form' => { success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, otp_delivery_preference: 'sms',
+ 'IdV: phone confirmation form' => { success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, otp_delivery_preference: 'sms',
proofing_components: { document_check: 'usps', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', source_check: 'aamva' } },
'IdV: phone confirmation vendor' => { success: true, errors: {}, vendor: { exception: nil, vendor_name: 'AddressMock', transaction_id: 'address-mock-transaction-id-123', timed_out: false, reference: '' }, new_phone_added: false, area_code: '202', country_code: 'US', phone_fingerprint: anything,
proofing_components: { address_check: 'lexis_nexis_address', document_check: 'usps', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', source_check: 'aamva' } },
@@ -157,11 +157,11 @@
'IdV: phone confirmation otp visited' => { proofing_components: { address_check: 'lexis_nexis_address', document_check: 'usps', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', source_check: 'aamva' } },
'IdV: phone confirmation otp submitted' => { success: true, code_expired: false, code_matches: true, second_factor_attempts_count: 0, second_factor_locked_at: nil, errors: {},
proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } },
- 'IdV: review info visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, address_verification_method: 'phone',
+ 'IdV: review info visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, address_verification_method: 'phone',
proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } },
- 'IdV: review complete' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: true, deactivation_reason: 'in_person_verification_pending',
+ 'IdV: review complete' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: true, deactivation_reason: 'in_person_verification_pending',
proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } },
- 'IdV: final resolution' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: true, deactivation_reason: 'in_person_verification_pending',
+ 'IdV: final resolution' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: true, deactivation_reason: 'in_person_verification_pending',
proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } },
'IdV: personal key visited' => { proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' }, address_verification_method: 'phone' },
'IdV: personal key acknowledgment toggled' => { checked: true,
diff --git a/spec/features/idv/doc_auth/verify_info_step_spec.rb b/spec/features/idv/doc_auth/verify_info_step_spec.rb
index 287bd42bb9f..4aac0cabf08 100644
--- a/spec/features/idv/doc_auth/verify_info_step_spec.rb
+++ b/spec/features/idv/doc_auth/verify_info_step_spec.rb
@@ -51,7 +51,11 @@
expect(fake_analytics).to have_logged_event(
'IdV: doc auth verify proofing results',
- hash_including(address_edited: true, address_line2_present: true),
+ hash_including(
+ address_edited: true,
+ address_line2_present: true,
+ analytics_id: 'Doc Auth',
+ ),
)
end
diff --git a/spec/features/idv/steps/in_person/verify_info_spec.rb b/spec/features/idv/steps/in_person/verify_info_spec.rb
index 9c2f31642fe..e0872eef1cc 100644
--- a/spec/features/idv/steps/in_person/verify_info_spec.rb
+++ b/spec/features/idv/steps/in_person/verify_info_spec.rb
@@ -5,14 +5,16 @@
include IdvStepHelper
include InPersonHelper
+ let(:user) { user_with_2fa }
+ let(:fake_analytics) { FakeAnalytics.new(user: user) }
+
before do
allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
+ allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics)
end
it 'provides back buttons for address, state ID, and SSN that discard changes',
allow_browser_log: true do
- user = user_with_2fa
-
sign_in_and_2fa_user(user)
begin_in_person_proofing(user)
complete_prepare_step(user)
@@ -72,8 +74,6 @@
it 'returns the user to the verify info page when updates are made',
allow_browser_log: true do
- user = user_with_2fa
-
sign_in_and_2fa_user(user)
begin_in_person_proofing(user)
complete_prepare_step(user)
@@ -135,7 +135,6 @@
it 'does not proceed to the next page if resolution fails',
allow_browser_log: true do
- user = user_with_2fa
sign_in_and_2fa_user
begin_in_person_proofing(user)
@@ -155,7 +154,6 @@
it 'proceeds to the next page if resolution passes',
allow_browser_log: true do
- user = user_with_2fa
sign_in_and_2fa_user
begin_in_person_proofing(user)
complete_prepare_step(user)
@@ -166,5 +164,9 @@
click_idv_continue
expect(page).to have_content(t('titles.idv.phone'))
+ expect(fake_analytics).to have_logged_event(
+ 'IdV: doc auth verify proofing results',
+ hash_including(analytics_id: 'In Person Proofing'),
+ )
end
end
diff --git a/spec/features/multi_factor_authentication/mfa_cta_spec.rb b/spec/features/multi_factor_authentication/mfa_cta_spec.rb
index 3dadce29904..b37dd57847f 100644
--- a/spec/features/multi_factor_authentication/mfa_cta_spec.rb
+++ b/spec/features/multi_factor_authentication/mfa_cta_spec.rb
@@ -68,7 +68,7 @@
expect(page).to have_current_path(confirm_backup_codes_path)
acknowledge_backup_code_confirmation
click_link(t('mfa.second_method_warning.link'))
- expect(page).to have_current_path(second_mfa_setup_path)
+ expect(page).to have_current_path(authentication_methods_setup_path)
end
end
end
diff --git a/spec/features/openid_connect/openid_connect_spec.rb b/spec/features/openid_connect/openid_connect_spec.rb
index f6f6048aec3..ffe97872183 100644
--- a/spec/features/openid_connect/openid_connect_spec.rb
+++ b/spec/features/openid_connect/openid_connect_spec.rb
@@ -167,6 +167,78 @@
expect(current_url).to start_with('http://localhost:7654/auth/result')
expect(page.get_rack_session.keys).to include('sp')
end
+
+ context 'when using prompt=login' do
+ it 'does not show reauthentication notice if user was not actively authenticated' do
+ service_provider = ServiceProvider.find_by(issuer: OidcAuthHelper::OIDC_IAL1_ISSUER)
+
+ visit_idp_from_ial1_oidc_sp(prompt: 'login')
+ expect(page).to_not have_content(
+ strip_tags(
+ t(
+ 'account.login.forced_reauthentication_notice_html',
+ sp_name: service_provider.friendly_name,
+ ),
+ ),
+ )
+ end
+
+ it 'does show reauthentication notice if user was actively authenticated' do
+ service_provider = ServiceProvider.find_by(issuer: OidcAuthHelper::OIDC_IAL1_ISSUER)
+ user = user_with_2fa
+ sign_in_user(user)
+
+ visit_idp_from_ial1_oidc_sp(prompt: 'login')
+
+ expect(page).to have_content(
+ strip_tags(
+ t(
+ 'account.login.forced_reauthentication_notice_html',
+ sp_name: service_provider.friendly_name,
+ ),
+ ),
+ )
+
+ visit_idp_from_ial1_oidc_sp(prompt: 'login')
+
+ expect(page).to have_content(
+ strip_tags(
+ t(
+ 'account.login.forced_reauthentication_notice_html',
+ sp_name: service_provider.friendly_name,
+ ),
+ ),
+ )
+ end
+
+ it 'does not show reauth notice if most recent request in session was not prompt=login' do
+ service_provider = ServiceProvider.find_by(issuer: OidcAuthHelper::OIDC_IAL1_ISSUER)
+ user = user_with_2fa
+ sign_in_user(user)
+
+ visit_idp_from_ial1_oidc_sp(prompt: 'login')
+
+ expect(page).to have_content(
+ strip_tags(
+ t(
+ 'account.login.forced_reauthentication_notice_html',
+ sp_name: service_provider.friendly_name,
+ ),
+ ),
+ )
+
+ visit_idp_from_ial1_oidc_sp(prompt: 'select_account')
+
+ expect(page).to_not have_content(
+ strip_tags(
+ t(
+ 'account.login.forced_reauthentication_notice_html',
+ sp_name: service_provider.friendly_name,
+ ),
+ ),
+ )
+ end
+ end
end
context 'when accepting id_token_hint in logout' do
diff --git a/spec/features/phone/add_phone_spec.rb b/spec/features/phone/add_phone_spec.rb
index a6315d6fb30..ea3895417e0 100644
--- a/spec/features/phone/add_phone_spec.rb
+++ b/spec/features/phone/add_phone_spec.rb
@@ -11,7 +11,7 @@
click_on t('account.navigation.add_phone_number')
end
fill_in :new_phone_form_phone, with: phone
- click_continue
+ click_send_one_time_code
fill_in_code_with_last_phone_otp
click_submit_default
@@ -30,7 +30,7 @@
click_on t('account.navigation.add_phone_number')
end
fill_in :new_phone_form_phone, with: phone
- click_continue
+ click_send_one_time_code
fill_in_code_with_last_phone_otp
click_submit_default
@@ -51,7 +51,7 @@
hidden_select = page.find('[name="new_phone_form[international_code]"]', visible: :hidden)
# Required field should prompt as required on submit
- click_continue
+ click_send_one_time_code
focused_input = page.find(':focus')
expect(focused_input).to match_css('.phone-input__number.usa-input--error')
expect(hidden_select.value).to eq('US')
@@ -66,7 +66,7 @@
# Invalid number should prompt as invalid on submit
fill_in :new_phone_form_phone, with: 'abcd1234'
- click_continue
+ click_send_one_time_code
focused_input = page.find(':focus')
expect(focused_input).to match_css('.phone-input__number.usa-input--error')
expect(hidden_select.value).to eq('US')
@@ -102,7 +102,7 @@
expect(page).to_not have_content(t('two_factor_authentication.otp_delivery_preference.title'))
expect(hidden_select.value).to eq('LK')
fill_in :new_phone_form_phone, with: '+94 071 234 5678'
- click_continue
+ click_send_one_time_code
expect(page.find(':focus')).to match_css('.phone-input__number')
# Switching to supported country should re-show delivery options, but prompt as invalid number
@@ -113,7 +113,7 @@
expect(page).to have_content(t('two_factor_authentication.otp_delivery_preference.title'))
expect(page).to have_css('.usa-error-message', text: '', visible: false)
expect(hidden_select.value).to eq('US')
- click_continue
+ click_send_one_time_code
expect(page.find(':focus')).to match_css('.phone-input__number')
expect(page).to have_content(t('errors.messages.invalid_phone_number.us'))
@@ -121,7 +121,7 @@
input = fill_in :new_phone_form_phone, with: '+81543543643'
expect(input.value).to eq('+81 543543643')
expect(hidden_select.value).to eq('JP')
- click_continue
+ click_send_one_time_code
expect(page).to have_content(t('components.one_time_code_input.label'))
end
@@ -157,7 +157,7 @@
phone = phone_configuration.phone.sub(/^\+1\s*/, '').gsub(/\D/, '')
fill_in :new_phone_form_phone, with: phone
- click_continue
+ click_send_one_time_code
expect(page).to have_content(I18n.t('errors.messages.phone_duplicate'))
@@ -179,7 +179,7 @@
click_on t('account.navigation.add_phone_number')
end
fill_in :new_phone_form_phone, with: telephony_gem_voip_number
- click_continue
+ click_send_one_time_code
expect(page).to have_content(t('errors.messages.voip_check_error'))
end
@@ -218,7 +218,7 @@
# Failing international should display spam protection screen
fill_in t('two_factor_authentication.phone_label'), with: '3065550100'
fill_in t('components.captcha_submit_button.mock_score_label'), with: '0.5'
- click_continue
+ click_send_one_time_code
expect(page).to have_content(t('titles.spam_protection'), wait: 5)
click_continue
expect(page).to have_content(t('two_factor_authentication.header_text'))
@@ -243,7 +243,7 @@
# Passing international should display OTP confirmation
fill_in t('two_factor_authentication.phone_label'), with: '3065550100'
fill_in t('components.captcha_submit_button.mock_score_label'), with: '0.7'
- click_continue
+ click_send_one_time_code
expect(page).to have_content(t('two_factor_authentication.header_text'), wait: 25)
visit account_path
within('.sidenav') { click_on t('account.navigation.add_phone_number') }
@@ -251,7 +251,7 @@
# Failing domestic should display OTP confirmation
fill_in t('two_factor_authentication.phone_label'), with: '5135550100'
fill_in t('components.captcha_submit_button.mock_score_label'), with: '0.5'
- click_continue
+ click_send_one_time_code
expect(page).to have_content(t('two_factor_authentication.header_text'), wait: 5)
visit account_path
within('.sidenav') { click_on t('account.navigation.add_phone_number') }
@@ -259,7 +259,7 @@
# Passing domestic should display OTP confirmation
fill_in t('two_factor_authentication.phone_label'), with: '5135550100'
fill_in t('components.captcha_submit_button.mock_score_label'), with: '0.7'
- click_continue
+ click_send_one_time_code
expect(page).to have_content(t('two_factor_authentication.header_text'), wait: 5)
end
@@ -272,7 +272,7 @@
click_on t('account.navigation.add_phone_number')
end
fill_in :new_phone_form_phone, with: phone
- click_continue
+ click_send_one_time_code
click_link t('links.cancel')
expect(page).to have_current_path(account_path)
@@ -290,9 +290,9 @@
end
fill_in :new_phone_form_phone, with: phone
- click_continue
+ click_send_one_time_code
click_link t('two_factor_authentication.phone_verification.troubleshooting.change_number')
- expect(page).to have_current_path(add_phone_path)
+ expect(page).to have_current_path(phone_setup_path)
end
end
diff --git a/spec/features/phone/confirmation_spec.rb b/spec/features/phone/confirmation_spec.rb
index cf3396e32e0..04478724e63 100644
--- a/spec/features/phone/confirmation_spec.rb
+++ b/spec/features/phone/confirmation_spec.rb
@@ -69,7 +69,7 @@ def visit_otp_confirmation(delivery_method)
end
fill_in :new_phone_form_phone, with: phone
select_phone_delivery_option(delivery_method)
- click_continue
+ click_send_one_time_code
end
def expect_successful_otp_confirmation(delivery_method)
diff --git a/spec/features/phone/default_phone_selection_spec.rb b/spec/features/phone/default_phone_selection_spec.rb
index 7239d270390..40340bda191 100644
--- a/spec/features/phone/default_phone_selection_spec.rb
+++ b/spec/features/phone/default_phone_selection_spec.rb
@@ -33,7 +33,7 @@
enter_phone_number('202-555-3434')
check 'new_phone_form_otp_make_default_number'
- click_button t('forms.buttons.continue')
+ click_button t('forms.buttons.send_one_time_code')
expect(page).to have_content t(
'instructions.mfa.sms.number_message_html',
@@ -61,7 +61,7 @@
new_phone = '202-555-3111'
sign_in_visit_add_phone_path(user, phone_config2)
fill_in :new_phone_form_phone, with: new_phone
- click_continue
+ click_send_one_time_code
fill_in_code_with_last_phone_otp
click_submit_default
@@ -104,7 +104,7 @@
enter_phone_number('202-555-3434')
choose 'new_phone_form_otp_delivery_preference_voice'
check 'new_phone_form_otp_make_default_number'
- click_button t('forms.buttons.continue')
+ click_button t('forms.buttons.send_one_time_code')
expect(page).to have_content t(
'instructions.mfa.voice.number_message_html',
@@ -147,7 +147,7 @@ def sign_in_visit_manage_phone_path(user, phone_config2)
def sign_in_visit_add_phone_path(user, phone_config2)
sign_in_and_2fa_user(user)
- visit add_phone_path(id: phone_config2.id)
+ visit phone_setup_path(id: phone_config2.id)
expect(page).to have_content t('two_factor_authentication.otp_make_default_number.label')
end
end
diff --git a/spec/features/phone/rate_limitting_spec.rb b/spec/features/phone/rate_limitting_spec.rb
index 35b83abc602..a89b085ee98 100644
--- a/spec/features/phone/rate_limitting_spec.rb
+++ b/spec/features/phone/rate_limitting_spec.rb
@@ -30,7 +30,7 @@ def visit_otp_confirmation(delivery_method)
end
fill_in :new_phone_form_phone, with: phone
select_phone_delivery_option(delivery_method)
- click_continue
+ click_send_one_time_code
end
end
end
diff --git a/spec/features/remember_device/webauthn_spec.rb b/spec/features/remember_device/webauthn_spec.rb
index 7dd1aef60df..3f286587970 100644
--- a/spec/features/remember_device/webauthn_spec.rb
+++ b/spec/features/remember_device/webauthn_spec.rb
@@ -123,14 +123,14 @@ def remember_device_and_sign_out_user
click_link t('mfa.add')
- expect(page).to have_current_path(second_mfa_setup_path)
+ expect(page).to have_current_path(authentication_methods_setup_path)
click_2fa_option('phone')
click_continue
expect(page).
- to have_content t('titles.phone_setup')
+ to have_content t('headings.add_info.phone')
expect(current_path).to eq phone_setup_path
diff --git a/spec/features/saml/saml_spec.rb b/spec/features/saml/saml_spec.rb
index 03809ff85b8..984c6c60a59 100644
--- a/spec/features/saml/saml_spec.rb
+++ b/spec/features/saml/saml_spec.rb
@@ -257,6 +257,7 @@
expect(sp_return_logs.count).to eq(1)
expect(sp_return_logs.first.ial).to eq(2)
end
+
context 'when ForceAuthn = true in SAMLRequest' do
let(:saml_request_overrides) do
{
@@ -276,15 +277,24 @@
scenario 'enforces reauthentication if already signed in' do
# start with an active user session
+ service_provider = ServiceProvider.find_by(issuer: sp1_issuer)
sign_in_live_with_2fa(user)
# visit from SP with force_authn: true
visit_saml_authn_request_url(overrides: saml_request_overrides)
expect(page).to have_content(
- 'is using Login.gov to allow you to sign in to your account safely and securely.',
+ t('headings.create_account_with_sp.sp_text', app_name: APP_NAME),
)
expect(page).to have_button('Sign in')
-
+ # visit from SP with force_authn: true
+ expect(page).to have_content(
+ strip_tags(
+ t(
+ 'account.login.forced_reauthentication_notice_html',
+ sp_name: service_provider.friendly_name,
+ ),
+ ),
+ )
# sign in again
fill_in_credentials_and_submit(user.email, user.password)
fill_in_code_with_last_phone_otp
@@ -309,12 +319,22 @@
end
scenario 'enforces reauthentication if already signed in from the same SP' do
+ service_provider = ServiceProvider.find_by(issuer: sp1_issuer)
# first visit from Test SP
visit_saml_authn_request_url(overrides: saml_request_overrides)
expect(page).to have_content(
'Test SP is using Login.gov to allow you to sign in' \
' to your account safely and securely.',
)
+ # does not show reauth notice if user was not logged in
+ expect(page).to_not have_content(
+ strip_tags(
+ t(
+ 'account.login.forced_reauthentication_notice_html',
+ sp_name: service_provider.friendly_name,
+ ),
+ ),
+ )
expect(page).to have_button('Sign in')
# Log in with Test SP as the SP session
fill_in_credentials_and_submit(user.email, user.password)
@@ -336,6 +356,14 @@
'Test SP is using Login.gov to allow you to sign in' \
' to your account safely and securely.',
)
+ expect(page).to have_content(
+ strip_tags(
+ t(
+ 'account.login.forced_reauthentication_notice_html',
+ sp_name: service_provider.friendly_name,
+ ),
+ ),
+ )
expect(page).to have_button('Sign in')
# log in for second time
@@ -367,15 +395,24 @@
end
scenario 'enforces reauthentication when ForceAuthn = true in SAMLRequest' do
+ service_provider = ServiceProvider.find_by(issuer: SamlAuthHelper::SP_ISSUER)
# start with an active user session
sign_in_live_with_2fa(user)
# visit from SP with force_authn: true
visit_saml_authn_request_url(overrides: { force_authn: true })
expect(page).to have_content(
- 'is using Login.gov to allow you to sign in to your account safely and securely.',
+ t('headings.create_account_with_sp.sp_text', app_name: APP_NAME),
)
expect(page).to have_button('Sign in')
+ expect(page).to have_content(
+ strip_tags(
+ t(
+ 'account.login.forced_reauthentication_notice_html',
+ sp_name: service_provider.friendly_name,
+ ),
+ ),
+ )
# sign in again
fill_in_credentials_and_submit(user.email, user.password)
@@ -391,6 +428,42 @@
xmldoc.status_code.attribute('Value').value,
).to eq 'urn:oasis:names:tc:SAML:2.0:status:Success'
end
+
+ scenario 'does not show reauth notice if most recent request in session was not ForceAuthn' do
+ service_provider = ServiceProvider.find_by(issuer: SamlAuthHelper::SP_ISSUER)
+ # start with an active user session
+ sign_in_live_with_2fa(user)
+
+ # visit from SP with force_authn: true
+ visit_saml_authn_request_url(overrides: { force_authn: true })
+ expect(page).to have_content(
+ t('headings.create_account_with_sp.sp_text', app_name: APP_NAME),
+ )
+ expect(page).to have_button('Sign in')
+ expect(page).to have_content(
+ strip_tags(
+ t(
+ 'account.login.forced_reauthentication_notice_html',
+ sp_name: service_provider.friendly_name,
+ ),
+ ),
+ )
+
+ visit_saml_authn_request_url
+
+ expect(page).to have_content(
+ t('headings.create_account_with_sp.sp_text', app_name: APP_NAME),
+ )
+ expect(page).to have_button('Sign in')
+ expect(page).to_not have_content(
+ strip_tags(
+ t(
+ 'account.login.forced_reauthentication_notice_html',
+ sp_name: service_provider.friendly_name,
+ ),
+ ),
+ )
+ end
end
end
diff --git a/spec/features/two_factor_authentication/change_factor_spec.rb b/spec/features/two_factor_authentication/change_factor_spec.rb
index d45ce065333..be135fae9e9 100644
--- a/spec/features/two_factor_authentication/change_factor_spec.rb
+++ b/spec/features/two_factor_authentication/change_factor_spec.rb
@@ -84,17 +84,17 @@
visit webauthn_setup_path
expect(current_path).to eq login_two_factor_options_path
- visit add_phone_path
+ visit phone_setup_path
expect(current_path).to eq login_two_factor_options_path
find("label[for='two_factor_options_form_selection_sms']").click
click_on t('forms.buttons.continue')
fill_in_code_with_last_phone_otp
click_submit_default
- expect(current_path).to eq add_phone_path
+ expect(current_path).to eq phone_setup_path
- visit add_phone_path
- expect(current_path).to eq add_phone_path
+ visit phone_setup_path
+ expect(current_path).to eq phone_setup_path
end
end
diff --git a/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb b/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb
index 39e566c8d4e..1abaa311ddd 100644
--- a/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb
+++ b/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb
@@ -15,7 +15,7 @@
click_continue
expect(page).
- to have_content t('titles.phone_setup')
+ to have_content t('headings.add_info.phone')
expect(current_path).to eq phone_setup_path
@@ -48,7 +48,7 @@
click_continue
expect(page).
- to have_content t('titles.phone_setup')
+ to have_content t('headings.add_info.phone')
expect(current_path).to eq phone_setup_path
@@ -62,7 +62,7 @@
click_link t('two_factor_authentication.choose_another_option')
- expect(page).to have_current_path(second_mfa_setup_path)
+ expect(page).to have_current_path(authentication_methods_setup_path)
select_2fa_option('auth_app')
fill_in t('forms.totp_setup.totp_step_1'), with: 'App'
@@ -132,7 +132,7 @@
click_continue
expect(page).
- to have_content t('titles.phone_setup')
+ to have_content t('headings.add_info.phone')
click_continue
@@ -168,7 +168,7 @@
expect(page).to have_current_path(auth_method_confirmation_path)
click_link t('mfa.add')
- expect(page).to have_current_path(second_mfa_setup_path)
+ expect(page).to have_current_path(authentication_methods_setup_path)
click_link t('mfa.skip')
expect(page).to have_current_path(account_path)
@@ -189,7 +189,7 @@
expect(page).to have_current_path(auth_method_confirmation_path)
click_link t('mfa.add')
- expect(page).to have_current_path(second_mfa_setup_path)
+ expect(page).to have_current_path(authentication_methods_setup_path)
click_continue
expect(page).to have_current_path(account_path)
@@ -218,10 +218,10 @@
expect(page).to_not have_button(t('mfa.skip'))
click_link t('mfa.add')
- expect(page).to have_current_path(second_mfa_setup_path)
+ expect(page).to have_current_path(authentication_methods_setup_path)
click_continue
- expect(page).to have_current_path(second_mfa_setup_path)
+ expect(page).to have_current_path(authentication_methods_setup_path)
expect(page).to have_content(
t('errors.two_factor_auth_setup.must_select_additional_option'),
)
diff --git a/spec/features/two_factor_authentication/sign_in_spec.rb b/spec/features/two_factor_authentication/sign_in_spec.rb
index 75141f4bea8..560f65eb5aa 100644
--- a/spec/features/two_factor_authentication/sign_in_spec.rb
+++ b/spec/features/two_factor_authentication/sign_in_spec.rb
@@ -14,7 +14,7 @@
click_continue
expect(page).
- to have_content t('titles.phone_setup')
+ to have_content t('headings.add_info.phone')
send_one_time_code_without_entering_phone_number
diff --git a/spec/features/users/sign_up_spec.rb b/spec/features/users/sign_up_spec.rb
index dec3ac13da7..7cc68677adf 100644
--- a/spec/features/users/sign_up_spec.rb
+++ b/spec/features/users/sign_up_spec.rb
@@ -197,8 +197,8 @@ def clipboard_text
set_up_2fa_with_backup_codes
skip_second_mfa_prompt
- visit add_phone_path
- expect(page).to have_current_path add_phone_path
+ visit phone_setup_path
+ expect(page).to have_current_path phone_setup_path
end
end
@@ -419,8 +419,8 @@ def clipboard_text
acknowledge_backup_code_confirmation
expect(page).to have_current_path account_path
- visit add_phone_path
- expect(page).to have_current_path add_phone_path
+ visit phone_setup_path
+ expect(page).to have_current_path phone_setup_path
end
describe 'visiting the homepage by clicking the logo image' do
diff --git a/spec/forms/webauthn_visit_form_spec.rb b/spec/forms/webauthn_visit_form_spec.rb
index bc394a3e0ae..45b4771358d 100644
--- a/spec/forms/webauthn_visit_form_spec.rb
+++ b/spec/forms/webauthn_visit_form_spec.rb
@@ -168,7 +168,7 @@
context 'with two_factor_enabled and in_mfa_selection_flow' do
let(:user) { create(:user, :with_phone) }
- it { is_expected.to eq(second_mfa_setup_path) }
+ it { is_expected.to eq(authentication_methods_setup_path) }
end
context 'with two_factor_enabled' do
diff --git a/spec/jobs/in_person/send_proofing_notification_job_spec.rb b/spec/jobs/in_person/send_proofing_notification_job_spec.rb
index 0c111830409..011cd2092b2 100644
--- a/spec/jobs/in_person/send_proofing_notification_job_spec.rb
+++ b/spec/jobs/in_person/send_proofing_notification_job_spec.rb
@@ -7,18 +7,23 @@
let(:passed_enrollment_without_notification) { create(:in_person_enrollment, :passed) }
let(:passed_enrollment) do
- enrollment = create(:in_person_enrollment, :passed, :with_notification_phone_configuration)
- enrollment.proofed_at = Time.zone.now - 3.days
- enrollment
+ create(
+ :in_person_enrollment,
+ :passed,
+ :with_notification_phone_configuration,
+ proofed_at: Time.zone.now - 3.days,
+ )
end
let(:failing_enrollment) do
- enrollment = create(:in_person_enrollment, :failed, :with_notification_phone_configuration)
- enrollment.proofed_at = Time.zone.now - 3.days
- enrollment
+ create(
+ :in_person_enrollment,
+ :failed,
+ :with_notification_phone_configuration,
+ proofed_at: Time.zone.now - 3.days,
+ )
end
let(:expired_enrollment) do
- enrollment = create(:in_person_enrollment, :expired, :with_notification_phone_configuration)
- enrollment
+ create(:in_person_enrollment, :expired, :with_notification_phone_configuration)
end
let(:sms_success_response) do
Telephony::Response.new(
@@ -40,6 +45,7 @@
error: Telephony::DailyLimitReachedError.new,
)
end
+
before do
ActiveJob::Base.queue_adapter = :test
allow(job).to receive(:analytics).and_return(analytics)
@@ -62,6 +68,7 @@
expect(analytics).not_to have_logged_event('SendProofingNotificationJob: job completed')
end
end
+
context 'job disabled' do
let(:in_person_proofing_enabled) { true }
let(:in_person_send_proofing_notifications_enabled) { false }
@@ -73,9 +80,11 @@
expect(analytics).not_to have_logged_event('SendProofingNotificationJob: job completed')
end
end
+
context 'ipp and job enabled' do
let(:in_person_proofing_enabled) { true }
let(:in_person_send_proofing_notifications_enabled) { true }
+
context 'enrollment does not exist' do
it 'returns without doing anything' do
bad_id = (InPersonEnrollment.all.pluck(:id).max || 0) + 1
@@ -84,6 +93,7 @@
expect(analytics).to have_logged_event('SendProofingNotificationJob: job skipped')
end
end
+
context 'enrollment has an unsupported status' do
it 'returns without doing anything' do
job.perform(expired_enrollment.id)
@@ -91,6 +101,7 @@
expect(analytics).to have_logged_event('SendProofingNotificationJob: job skipped')
end
end
+
context 'without notification phone notification' do
it 'returns without doing anything' do
job.perform(passed_enrollment_without_notification.id)
@@ -98,6 +109,7 @@
expect(analytics).to have_logged_event('SendProofingNotificationJob: job skipped')
end
end
+
context 'with notification phone configuration' do
it 'sends notification successfully when enrollment is successful and enrollment updated' do
allow(Telephony).to receive(:send_notification).and_return(sms_success_response)
@@ -116,6 +128,7 @@
expect(passed_enrollment.reload.notification_phone_configuration).to be_nil
end
end
+
it 'sends notification successfully when enrollment failed' do
allow(Telephony).to receive(:send_notification).and_return(sms_success_response)
@@ -131,7 +144,30 @@
expect(failing_enrollment.reload.notification_phone_configuration).to be_nil
end
end
+
+ it 'sends a message that respects the user email locale preference' do
+ allow(Telephony).to receive(:send_notification).and_return(sms_success_response)
+
+ passed_enrollment.user.update!(email_language: 'fr')
+ passed_enrollment.update!(proofed_at: Time.zone.now)
+ proofed_date = Time.zone.now.strftime('%m/%d/%Y')
+ phone_number = passed_enrollment.notification_phone_configuration.formatted_phone
+
+ expect(Telephony).
+ to(
+ receive(:send_notification).
+ with(
+ to: phone_number,
+ message: "Login.gov: Vous avez tenté de vérifier votre identité dans un bureau " \
+ "de poste le #{proofed_date}. Vérifiez votre e-mail pour votre résultat.",
+ country_code: Phonelib.parse(phone_number).country,
+ ),
+ )
+
+ job.perform(passed_enrollment.id)
+ end
end
+
context 'when failed to send notification' do
it 'logs sms send failure when number is opt out and enrollment not updated' do
allow(Telephony).to receive(:send_notification).and_return(sms_opt_out_response)
@@ -142,6 +178,7 @@
)
expect(passed_enrollment.reload.notification_sent_at).to be_nil
end
+
it 'logs sms send failure for delivery failure' do
allow(Telephony).to receive(:send_notification).and_return(sms_failure_response)
@@ -152,7 +189,8 @@
expect(passed_enrollment.reload.notification_sent_at).to be_nil
end
end
- context 'when an exception is raised' do
+
+ context 'when an exception is raised trying to find the enrollment' do
it 'logs the exception details' do
allow(InPersonEnrollment).
to receive(:find_by).
@@ -169,6 +207,28 @@
)
end
end
+
+ context 'when an exception is raised trying to send the notification' do
+ let(:exception_message) { 'SMS unsupported' }
+
+ it 'logs the exception details' do
+ allow(Telephony).
+ to(
+ receive(:send_notification).
+ and_raise(Telephony::SmsUnsupportedError.new(exception_message)),
+ )
+
+ job.perform(passed_enrollment.id)
+
+ expect(analytics).to have_logged_event(
+ 'SendProofingNotificationJob: exception raised',
+ enrollment_code: passed_enrollment.enrollment_code,
+ enrollment_id: passed_enrollment.id,
+ exception_class: 'Telephony::SmsUnsupportedError',
+ exception_message: exception_message,
+ )
+ 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 68c9769b83e..be70d91edea 100644
--- a/spec/lib/reporting/identity_verification_report_spec.rb
+++ b/spec/lib/reporting/identity_verification_report_spec.rb
@@ -77,6 +77,26 @@
end
end
+ describe '#query' do
+ context 'with an issuer' do
+ it 'includes an issuer filter' do
+ result = subject.query
+
+ expect(result).to include('| filter properties.service_provider = "my:example:issuer"')
+ end
+ end
+
+ context 'without an issuer' do
+ let(:issuer) { nil }
+
+ it 'does not include an issuer filter' do
+ result = subject.query
+
+ expect(result).to_not include('filter properties.service_provider')
+ end
+ end
+ end
+
describe '#cloudwatch_client' do
let(:opts) { {} }
let(:subject) { described_class.new(issuer:, time_range:, **opts) }
diff --git a/spec/models/in_person_enrollment_spec.rb b/spec/models/in_person_enrollment_spec.rb
index 372604202c0..35848e6d5ab 100644
--- a/spec/models/in_person_enrollment_spec.rb
+++ b/spec/models/in_person_enrollment_spec.rb
@@ -4,6 +4,8 @@
describe 'Associations' do
it { is_expected.to belong_to :user }
it { is_expected.to belong_to :profile }
+ it { is_expected.to belong_to :service_provider }
+ it { is_expected.to have_one(:notification_phone_configuration).dependent(:destroy) }
end
describe 'Status' do
@@ -66,7 +68,63 @@
end
end
- describe 'Triggers' do
+ describe 'Callbacks' do
+ describe 'when status is updated' do
+ it 'sets status_updated_at' do
+ enrollment = create(:in_person_enrollment, :establishing)
+ freeze_time do
+ current_time = Time.zone.now
+ expect(enrollment.status_updated_at).to be_nil
+ enrollment.update(status: InPersonEnrollment::STATUS_CANCELLED)
+ expect(enrollment.status_updated_at).to eq(current_time)
+ end
+ end
+
+ describe 'enrollment expires or is canceled' do
+ it 'deletes the notification phone number' do
+ statuses = [InPersonEnrollment::STATUS_CANCELLED, InPersonEnrollment::STATUS_EXPIRED]
+ statuses.each do |status|
+ enrollment = create(
+ :in_person_enrollment, :pending, :with_notification_phone_configuration
+ )
+ config_id = enrollment.notification_phone_configuration.id
+ expect(NotificationPhoneConfiguration.find_by({ id: config_id })).to_not be_nil
+
+ enrollment.update(status: status)
+ enrollment.reload
+
+ expect(enrollment.notification_phone_configuration).to be_nil
+ expect(NotificationPhoneConfiguration.find_by({ id: config_id })).to be_nil
+ end
+ end
+ end
+ end
+
+ describe 'when notification_sent_at is updated' do
+ context 'enrollment has a notification phone configuration' do
+ let!(:enrollment) do
+ create(:in_person_enrollment, :passed, :with_notification_phone_configuration)
+ end
+
+ it 'destroys the notification phone configuration' do
+ expect(enrollment.notification_phone_configuration).to_not be_nil
+
+ enrollment.update(notification_sent_at: Time.zone.now)
+
+ expect(enrollment.reload.notification_phone_configuration).to be_nil
+ end
+ end
+
+ context 'enrollment does not have a notification phone configuration' do
+ let!(:enrollment) { create(:in_person_enrollment, :passed) }
+
+ it 'does not raise an error' do
+ expect(enrollment.notification_phone_configuration).to be_nil
+ expect { enrollment.update!(notification_sent_at: Time.zone.now) }.to_not raise_error
+ end
+ end
+ end
+
it 'generates a unique ID if one is not provided' do
user = create(:user)
profile = create(:profile, gpo_verification_pending_at: 1.day.ago, user: user)
@@ -86,34 +144,62 @@
expect(enrollment.unique_id).to eq('1234')
end
+
+ describe 'setting capture_secondary_id_enabled on creation' do
+ let(:capture_enabled) { nil }
+
+ before do
+ allow(IdentityConfig.store).
+ to(
+ receive(:in_person_capture_secondary_id_enabled).
+ and_return(capture_enabled),
+ )
+ end
+
+ context 'feature flag is enabled' do
+ let(:capture_enabled) { true }
+ it 'sets capture_secondary_id_enabled to true on the enrollment' do
+ enrollment = create(:in_person_enrollment, :pending)
+ expect(enrollment.capture_secondary_id_enabled).to eq(true)
+ end
+ end
+
+ context 'feature flag is not enabled' do
+ let(:capture_enabled) { false }
+ it 'does not set capture_secondary_id_enabled to true on the enrollment' do
+ enrollment = create(:in_person_enrollment, :pending)
+ expect(enrollment.capture_secondary_id_enabled).to eq(false)
+ end
+ end
+ end
end
- describe 'email_reminders' do
+ describe 'enrollments that need email reminders' do
let(:early_benchmark) { Time.zone.now - 19.days }
let(:late_benchmark) { Time.zone.now - 26.days }
let(:final_benchmark) { Time.zone.now - 29.days }
- let!(:passed_enrollment) { create(:in_person_enrollment, :passed) }
- let!(:failing_enrollment) { create(:in_person_enrollment, :failed) }
- let!(:expired_enrollment) { create(:in_person_enrollment, :expired) }
- # send on days 11-5
- let!(:pending_enrollment_needing_early_reminder) do
+ # early reminder is sent on days 11-5
+ let!(:enrollments_needing_early_reminder) do
[
create(:in_person_enrollment, :pending, enrollment_established_at: Time.zone.now - 19.days),
create(:in_person_enrollment, :pending, enrollment_established_at: Time.zone.now - 25.days),
]
end
- # send on days 4 - 2
- let!(:pending_enrollment_needing_late_reminder) do
+ # late reminder is sent on days 4 - 2
+ let!(:enrollments_needing_late_reminder) do
[
create(:in_person_enrollment, :pending, enrollment_established_at: Time.zone.now - 26.days),
create(:in_person_enrollment, :pending, enrollment_established_at: Time.zone.now - 28.days),
]
end
- let!(:pending_enrollment) do
+ let!(:enrollments_needing_no_reminder) do
[
+ create(:in_person_enrollment, :passed),
+ create(:in_person_enrollment, :failed),
+ create(:in_person_enrollment, :expired),
create(:in_person_enrollment, :pending, enrollment_established_at: Time.zone.now),
create(:in_person_enrollment, :pending, created_at: Time.zone.now),
]
@@ -122,8 +208,7 @@
it 'returns pending enrollments that need early reminder' do
expect(InPersonEnrollment.count).to eq(9)
results = InPersonEnrollment.needs_early_email_reminder(early_benchmark, late_benchmark)
- expect(results.length).to eq pending_enrollment_needing_early_reminder.length
- expect(results.pluck(:id)).to match_array pending_enrollment_needing_early_reminder.pluck(:id)
+ expect(results.pluck(:id)).to match_array enrollments_needing_early_reminder.pluck(:id)
results.each do |result|
expect(result.pending?).to be_truthy
expect(result.early_reminder_sent?).to be_falsey
@@ -133,8 +218,7 @@
it 'returns pending enrollments that need late reminder' do
expect(InPersonEnrollment.count).to eq(9)
results = InPersonEnrollment.needs_late_email_reminder(late_benchmark, final_benchmark)
- expect(results.length).to eq(2)
- expect(results.pluck(:id)).to match_array pending_enrollment_needing_late_reminder.pluck(:id)
+ expect(results.pluck(:id)).to match_array enrollments_needing_late_reminder.pluck(:id)
results.each do |result|
expect(result.pending?).to be_truthy
expect(result.late_reminder_sent?).to be_falsey
@@ -142,40 +226,41 @@
end
end
- describe 'needs_usps_status_check' do
+ describe 'enrollments that need a status check' do
let(:check_interval) { ...1.hour.ago }
let!(:passed_enrollment) { create(:in_person_enrollment, :passed) }
- let!(:failing_enrollment) { create(:in_person_enrollment, :failed) }
+ let!(:failed_enrollment) { create(:in_person_enrollment, :failed) }
let!(:expired_enrollment) { create(:in_person_enrollment, :expired) }
let!(:checked_pending_enrollment) do
create(:in_person_enrollment, :pending, last_batch_claimed_at: Time.zone.now)
end
- let!(:needy_enrollments) do
- [
- create(:in_person_enrollment, :pending),
- create(:in_person_enrollment, :pending),
- create(:in_person_enrollment, :pending),
- create(:in_person_enrollment, :pending),
- ]
- end
+ let!(:needy_enrollments) { create_list(:in_person_enrollment, 4, :pending) }
- it 'returns only pending enrollments' do
+ it 'needs_usps_status_check returns only needy enrollments' do
expect(InPersonEnrollment.count).to eq(8)
results = InPersonEnrollment.needs_usps_status_check(check_interval)
- expect(results.length).to eq needy_enrollments.length
expect(results.pluck(:id)).to match_array needy_enrollments.pluck(:id)
- results.each do |result|
- expect(result.pending?).to be_truthy
+ results.each { |result| expect(result.pending?).to eq(true) }
+ end
+
+ it 'needs_usps_status_check_batch returns only matching enrollments' do
+ freeze_time do
+ batch_at = Time.zone.now
+ needy_enrollments.first(2).each do |enrollment|
+ enrollment.update(last_batch_claimed_at: batch_at)
+ end
+ results = InPersonEnrollment.needs_usps_status_check_batch(batch_at)
+ expect(results.pluck(:id)).to match_array needy_enrollments.first(2).pluck(:id)
end
end
it 'indicates whether an enrollment needs a status check' do
- expect(passed_enrollment.needs_usps_status_check?(check_interval)).to be_falsey
- expect(failing_enrollment.needs_usps_status_check?(check_interval)).to be_falsey
- expect(expired_enrollment.needs_usps_status_check?(check_interval)).to be_falsey
- expect(checked_pending_enrollment.needs_usps_status_check?(check_interval)).to be_falsey
+ expect(passed_enrollment.needs_usps_status_check?(check_interval)).to eq(false)
+ expect(failed_enrollment.needs_usps_status_check?(check_interval)).to eq(false)
+ expect(expired_enrollment.needs_usps_status_check?(check_interval)).to eq(false)
+ expect(checked_pending_enrollment.needs_usps_status_check?(check_interval)).to eq(false)
needy_enrollments.each do |enrollment|
- expect(enrollment.needs_usps_status_check?(check_interval)).to be_truthy
+ expect(enrollment.needs_usps_status_check?(check_interval)).to eq(true)
end
end
end
@@ -185,7 +270,7 @@
let!(:passed_enrollment) do
create(:in_person_enrollment, :passed, ready_for_status_check: true)
end
- let!(:failing_enrollment) do
+ let!(:failed_enrollment) do
create(:in_person_enrollment, :failed, ready_for_status_check: true)
end
let!(:expired_enrollment) do
@@ -193,198 +278,171 @@
end
let!(:checked_pending_enrollment) do
create(
- :in_person_enrollment, :pending, last_batch_claimed_at: Time.zone.now,
- ready_for_status_check: true
+ :in_person_enrollment,
+ :pending,
+ last_batch_claimed_at: Time.zone.now,
+ ready_for_status_check: true,
)
end
let!(:ready_enrollments) do
- [
- create(:in_person_enrollment, :pending, ready_for_status_check: true),
- create(:in_person_enrollment, :pending, ready_for_status_check: true),
- create(:in_person_enrollment, :pending, ready_for_status_check: true),
- create(:in_person_enrollment, :pending, ready_for_status_check: true),
- ]
+ create_list(:in_person_enrollment, 4, :pending, ready_for_status_check: true)
end
let!(:needy_enrollments) do
- [
- create(:in_person_enrollment, :pending, ready_for_status_check: false),
- create(:in_person_enrollment, :pending, ready_for_status_check: false),
- create(:in_person_enrollment, :pending, ready_for_status_check: false),
- create(:in_person_enrollment, :pending, ready_for_status_check: false),
- ]
+ create_list(:in_person_enrollment, 4, :pending, ready_for_status_check: false)
end
- it 'returns only pending enrollments that are ready for status check' do
+ it 'needs_status_check_on_ready_enrollments returns only ready pending enrollments' do
expect(InPersonEnrollment.count).to eq(12)
ready_results = InPersonEnrollment.needs_status_check_on_ready_enrollments(check_interval)
- expect(ready_results.length).to eq ready_enrollments.length
expect(ready_results.pluck(:id)).to match_array ready_enrollments.pluck(:id)
expect(ready_results.pluck(:id)).not_to match_array needy_enrollments.pluck(:id)
- ready_results.each do |result|
- expect(result.pending?).to be_truthy
- end
+ ready_results.each { |result| expect(result.pending?).to eq(true) }
end
- it 'indicates whether a ready enrollment needs a status check' do
- expect(passed_enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to(
- be(false),
- )
- expect(failing_enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to(
- be(false),
- )
- expect(expired_enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to(
- be(false),
- )
- expect(checked_pending_enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to(
- be(false),
- )
- needy_enrollments.each do |enrollment|
- expect(enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to(
- be(false),
- )
+ it 'needs_status_check_on_ready_enrollment? tells whether an enrollment needs a status check' do
+ other_enrollments = [
+ passed_enrollment,
+ failed_enrollment,
+ expired_enrollment,
+ checked_pending_enrollment,
+ ]
+
+ (other_enrollments + needy_enrollments).each do |enrollment|
+ expect(enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to eq(false)
end
+
ready_enrollments.each do |enrollment|
- expect(enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to(
- be(true),
- )
+ expect(enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to eq(true)
end
end
- it 'returns only pending enrollments that are not ready for status check' do
+ it 'needs_status_check_on_waiting_enrollments returns only not ready pending enrollments' do
expect(InPersonEnrollment.count).to eq(12)
waiting_results = InPersonEnrollment.needs_status_check_on_waiting_enrollments(check_interval)
- expect(waiting_results.length).to eq needy_enrollments.length
expect(waiting_results.pluck(:id)).to match_array needy_enrollments.pluck(:id)
expect(waiting_results.pluck(:id)).not_to match_array ready_enrollments.pluck(:id)
- waiting_results.each do |result|
- expect(result.pending?).to be_truthy
- end
+ waiting_results.each { |result| expect(result.pending?).to eq(true) }
end
it 'indicates whether a waiting enrollment needs a status check' do
- expect(passed_enrollment.needs_status_check_on_waiting_enrollment?(check_interval)).to(
- be(false),
- )
- expect(
- failing_enrollment.needs_status_check_on_waiting_enrollment?(check_interval),
- ).to(
- be(false),
- )
- expect(
- expired_enrollment.needs_status_check_on_waiting_enrollment?(check_interval),
- ).to(
- be(false),
- )
- expect(
- checked_pending_enrollment.needs_status_check_on_waiting_enrollment?(check_interval),
- ).to(
- be(false),
- )
- needy_enrollments.each do |enrollment|
- expect(enrollment.needs_status_check_on_waiting_enrollment?(check_interval)).to be(true)
+ other_enrollments = [
+ passed_enrollment,
+ failed_enrollment,
+ expired_enrollment,
+ checked_pending_enrollment,
+ ]
+
+ (other_enrollments + ready_enrollments).each do |enrollment|
+ expect(enrollment.needs_status_check_on_waiting_enrollment?(check_interval)).to eq(false)
end
- ready_enrollments.each do |enrollment|
- expect(enrollment.needs_status_check_on_waiting_enrollment?(check_interval)).to be(false)
+
+ needy_enrollments.each do |enrollment|
+ expect(enrollment.needs_status_check_on_waiting_enrollment?(check_interval)).to eq(true)
end
end
end
describe 'minutes_since_established' do
- let(:enrollment) do
- create(
- :in_person_enrollment, :passed, enrollment_established_at: Time.zone.now - 2.hours
- )
- end
-
it 'returns number of minutes since enrollment was established' do
freeze_time do
+ enrollment = create(
+ :in_person_enrollment,
+ :passed,
+ enrollment_established_at: Time.zone.now - 2.hours,
+ )
expect(enrollment.minutes_since_established).to eq 120
end
end
it 'returns nil if enrollment has not been established' do
- enrollment.status = InPersonEnrollment::STATUS_ESTABLISHING
- enrollment.enrollment_established_at = nil
+ enrollment = create(:in_person_enrollment, :establishing, enrollment_established_at: nil)
- expect(enrollment.minutes_since_established).to eq(nil)
+ expect(enrollment.minutes_since_established).to be_nil
end
end
describe 'minutes_since_last_status_check' do
- let(:enrollment) do
- create(
- :in_person_enrollment, :passed, status_check_attempted_at: Time.zone.now - 2.hours
- )
- end
-
it 'returns number of minutes since last status check' do
- expect(enrollment.minutes_since_last_status_check).to be_within(0.01).of(120)
+ freeze_time do
+ enrollment = create(
+ :in_person_enrollment,
+ status_check_attempted_at: Time.zone.now - 2.hours,
+ )
+ expect(enrollment.minutes_since_last_status_check).to eq 120
+ end
end
it 'returns nil if enrollment has not been status-checked' do
- enrollment.status_check_attempted_at = nil
+ enrollment = create(:in_person_enrollment, status_check_attempted_at: nil)
- expect(enrollment.minutes_since_last_status_check).to eq(nil)
+ expect(enrollment.minutes_since_last_status_check).to be_nil
end
end
describe 'minutes_since_last_status_check_completed' do
- let(:enrollment) do
- create(
- :in_person_enrollment, :passed, status_check_completed_at: Time.zone.now - 2.hours
- )
- end
-
it 'returns number of minutes since last status check was completed' do
- expect(enrollment.minutes_since_last_status_check_completed).to be_within(0.01).of(120)
+ freeze_time do
+ enrollment = create(
+ :in_person_enrollment,
+ status_check_completed_at: Time.zone.now - 2.hours,
+ )
+ expect(enrollment.minutes_since_last_status_check_completed).to eq 120
+ end
end
it 'returns nil if enrollment has not completed a status check' do
- enrollment.status_check_completed_at = nil
+ enrollment = create(:in_person_enrollment, status_check_completed_at: nil)
- expect(enrollment.minutes_since_last_status_check_completed).to eq(nil)
+ expect(enrollment.minutes_since_last_status_check_completed).to be_nil
end
end
- describe 'minutes_since_status_updated' do
- let(:enrollment) do
- enrollment = create(:in_person_enrollment, :passed)
- enrollment.status_updated_at = (Time.zone.now - 2.hours)
- enrollment
- end
-
+ describe 'minutes_since_last_status_update' do
it 'returns number of minutes since the status was updated' do
- expect(enrollment.minutes_since_last_status_update).to be_within(0.01).of(120)
+ freeze_time do
+ enrollment = create(:in_person_enrollment, status_updated_at: Time.zone.now - 2.hours)
+ expect(enrollment.minutes_since_last_status_update).to eq 120
+ end
end
it 'returns nil if enrollment status has not been updated' do
- enrollment.status_updated_at = nil
+ enrollment = create(:in_person_enrollment, status_updated_at: nil)
- expect(enrollment.minutes_since_last_status_update).to eq(nil)
+ expect(enrollment.minutes_since_last_status_update).to be_nil
end
end
- describe 'when notification_sent_at is updated' do
- context 'enrollment has a notification phone configuration' do
- let!(:enrollment) do
- create(:in_person_enrollment, :passed, :with_notification_phone_configuration)
- end
-
- it 'destroys the notification phone configuration' do
- expect(enrollment.notification_phone_configuration).to_not be_nil
+ describe 'due_date and days_to_due_date' do
+ let(:validity_in_days) { 10 }
+ let(:days_ago_established_at) { 7 }
- enrollment.update(notification_sent_at: Time.zone.now)
+ before do
+ allow(IdentityConfig.store).
+ to(
+ receive(:in_person_enrollment_validity_in_days).
+ and_return(validity_in_days),
+ )
+ end
- expect(enrollment.reload.notification_phone_configuration).to be_nil
+ it 'due_date returns the enrollment expiration date based on when it was established' do
+ freeze_time do
+ enrollment = create(
+ :in_person_enrollment,
+ enrollment_established_at: days_ago_established_at.days.ago,
+ )
+ expect(enrollment.due_date).to(
+ eq((validity_in_days - days_ago_established_at).days.from_now),
+ )
end
end
- context 'enrollment does not have a notification phone configuration' do
- let!(:enrollment) { create(:in_person_enrollment, :passed) }
-
- it 'does not raise an error' do
- expect(enrollment.notification_phone_configuration).to be_nil
- expect { enrollment.update!(notification_sent_at: Time.zone.now) }.to_not raise_error
+ it 'days_to_due_date returns the number of days left until the due date' do
+ freeze_time do
+ enrollment = create(
+ :in_person_enrollment,
+ enrollment_established_at: days_ago_established_at.days.ago,
+ )
+ expect(enrollment.days_to_due_date).to eq(validity_in_days - days_ago_established_at)
end
end
end
@@ -409,7 +467,7 @@
create(:in_person_enrollment, :failed)
end
- it 'returns true when status of passed/failed/expired and notification configuration' do
+ it 'returns true when status of passed/failed/expired and with notification configuration' do
expect(passed_enrollment.eligible_for_notification?).to eq(true)
expect(failed_enrollment.eligible_for_notification?).to eq(true)
end
@@ -421,47 +479,4 @@
expect(failed_enrollment_without_notification.eligible_for_notification?).to eq(false)
end
end
-
- describe 'user cancelling their enrollment' do
- context 'user has a notification phone number stored' do
- it 'deletes the notification phone number' do
- enrollment = create(:in_person_enrollment, :passed, :with_notification_phone_configuration)
- config_id = enrollment.notification_phone_configuration.id
- expect(NotificationPhoneConfiguration.find_by({ id: config_id })).to_not eq(nil)
-
- enrollment.cancelled!
- enrollment.reload
-
- expect(enrollment.notification_phone_configuration).to eq(nil)
- expect(NotificationPhoneConfiguration.find_by({ id: config_id })).to eq(nil)
- end
- end
-
- context 'user has a "nil" for their notification phone number' do
- it 'does nothing' do
- enrollment = create(:in_person_enrollment, :pending, notification_phone_configuration: nil)
-
- enrollment.cancelled!
- enrollment.reload
-
- expect(enrollment.notification_phone_configuration).to eq(nil)
- end
- end
- end
-
- describe 'enrollment expires' do
- it 'deletes the notification phone number' do
- enrollment = create(
- :in_person_enrollment, :establishing, :with_notification_phone_configuration
- )
- config_id = enrollment.notification_phone_configuration.id
- expect(NotificationPhoneConfiguration.find_by({ id: config_id })).to_not eq(nil)
-
- enrollment.expired!
- enrollment.reload
-
- expect(enrollment.notification_phone_configuration).to eq(nil)
- expect(NotificationPhoneConfiguration.find_by({ id: config_id })).to eq(nil)
- end
- end
end
diff --git a/spec/models/profile_spec.rb b/spec/models/profile_spec.rb
index 9132aecee56..6115623b71f 100644
--- a/spec/models/profile_spec.rb
+++ b/spec/models/profile_spec.rb
@@ -230,6 +230,31 @@
expect { profile.decrypt_pii(user.password) }.to raise_error(Encryption::EncryptionError)
end
+
+ context 'with aws_kms_multi_region_read_enabled enabled' do
+ before do
+ allow(IdentityConfig.store).to receive(:aws_kms_multi_region_read_enabled).and_return(true)
+ end
+
+ it 'decrypts the PII for users with a multi region ciphertext' do
+ profile.encrypt_pii(pii, user.password)
+
+ expect(profile.encrypted_pii_multi_region).to_not be_nil
+
+ decrypted_pii = profile.decrypt_pii(user.password)
+
+ expect(decrypted_pii).to eq pii
+ end
+
+ it 'decrypts the PII for users with only a single region ciphertext' do
+ profile.encrypt_pii(pii, user.password)
+ profile.update!(encrypted_pii_multi_region: nil)
+
+ decrypted_pii = profile.decrypt_pii(user.password)
+
+ expect(decrypted_pii).to eq pii
+ end
+ end
end
describe '#recover_pii' do
@@ -243,6 +268,37 @@
expect(profile.recover_pii(normalized_personal_key)).to eq pii
end
+
+ context 'with aws_kms_multi_region_read_enabled enabled' do
+ before do
+ allow(IdentityConfig.store).to receive(:aws_kms_multi_region_read_enabled).and_return(true)
+ end
+
+ it 'decrypts the PII for users with a multi region ciphertext' do
+ profile.encrypt_pii(pii, user.password)
+ personal_key = profile.personal_key
+
+ normalized_personal_key = PersonalKeyGenerator.new(user).normalize(personal_key)
+
+ expect(profile.encrypted_pii_recovery_multi_region).to_not be_nil
+
+ decrypted_pii = profile.recover_pii(normalized_personal_key)
+
+ expect(decrypted_pii).to eq pii
+ end
+
+ it 'decrypts the PII for users with only a single region ciphertext' do
+ profile.encrypt_pii(pii, user.password)
+ profile.update!(encrypted_pii_recovery_multi_region: nil)
+ personal_key = profile.personal_key
+
+ normalized_personal_key = PersonalKeyGenerator.new(user).normalize(personal_key)
+
+ decrypted_pii = profile.recover_pii(normalized_personal_key)
+
+ expect(decrypted_pii).to eq pii
+ end
+ end
end
describe 'allows only one active Profile per user' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index f76e836ada6..2ac84c3e9e9 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -401,6 +401,38 @@
expect(user.valid_password?('test password')).to eq(true)
expect(user.valid_password?('wrong password')).to eq(false)
end
+
+ context 'aws_kms_multi_region_read_enabled is set to true' do
+ before do
+ allow(IdentityConfig.store).to receive(:aws_kms_multi_region_read_enabled).and_return(true)
+ end
+
+ it 'validates the password for a user with a multi-region digest' do
+ user = build(:user, password: 'test password')
+
+ expect(user.encrypted_password_digest_multi_region).to_not be_nil
+
+ expect(user.valid_password?('test password')).to eq(true)
+ expect(user.valid_password?('wrong password')).to eq(false)
+ end
+
+ it 'validates the password for a user with a only a single-region digest' do
+ user = build(:user, password: 'test password')
+ user.encrypted_password_digest_multi_region = nil
+
+ expect(user.valid_password?('test password')).to eq(true)
+ expect(user.valid_password?('wrong password')).to eq(false)
+ end
+
+ it 'validates the password for a user with a only a single-region UAK digest' do
+ user = build(:user)
+ user.encrypted_password_digest = Encryption::UakPasswordVerifier.digest('test password')
+ user.encrypted_password_digest_multi_region = nil
+
+ expect(user.valid_password?('test password')).to eq(true)
+ expect(user.valid_password?('wrong password')).to eq(false)
+ end
+ end
end
describe '#personal_key=' do
@@ -430,6 +462,39 @@
expect(user.valid_personal_key?('test personal key')).to eq(true)
expect(user.valid_personal_key?('wrong personal key')).to eq(false)
end
+
+ context 'aws_kms_multi_region_read_enabled is set to true' do
+ before do
+ allow(IdentityConfig.store).to receive(:aws_kms_multi_region_read_enabled).and_return(true)
+ end
+
+ it 'validates the personal key for a user with a multi-region digest' do
+ user = build(:user, personal_key: 'test personal key')
+
+ expect(user.encrypted_recovery_code_digest_multi_region).to_not be_nil
+
+ expect(user.valid_personal_key?('test personal key')).to eq(true)
+ expect(user.valid_personal_key?('wrong personal key')).to eq(false)
+ end
+
+ it 'validates the personal key for a user with a only a single-region digest' do
+ user = build(:user, personal_key: 'test personal key')
+ user.encrypted_recovery_code_digest_multi_region = nil
+
+ expect(user.valid_personal_key?('test personal key')).to eq(true)
+ expect(user.valid_personal_key?('wrong personal key')).to eq(false)
+ end
+
+ it 'validates the personal key for a user with a only a single-region UAK digest' do
+ user = build(:user)
+ user.encrypted_recovery_code_digest =
+ Encryption::UakPasswordVerifier.digest('test personal key')
+ user.encrypted_recovery_code_digest_multi_region = nil
+
+ expect(user.valid_personal_key?('test personal key')).to eq(true)
+ expect(user.valid_personal_key?('wrong personal key')).to eq(false)
+ end
+ end
end
describe '#authenticatable_salt' do
diff --git a/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb b/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb
index 76c76dc30c2..7487d03d421 100644
--- a/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb
+++ b/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb
@@ -75,7 +75,7 @@
it 'should show an option to change phone number' do
expect(presenter.troubleshooting_options).to include(
an_object_satisfying do |c|
- c.url == add_phone_path &&
+ c.url == phone_setup_path &&
c.content == t(
'two_factor_authentication.phone_verification.troubleshooting.change_number',
)
diff --git a/spec/presenters/two_factor_options_presenter_spec.rb b/spec/presenters/two_factor_options_presenter_spec.rb
index d7745b3d655..383fdeb3595 100644
--- a/spec/presenters/two_factor_options_presenter_spec.rb
+++ b/spec/presenters/two_factor_options_presenter_spec.rb
@@ -4,13 +4,16 @@
include Rails.application.routes.url_helpers
include RequestHelper
+ let(:user) { build(:user) }
let(:user_agent) do
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 \
(KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36'
end
+ let(:after_mfa_setup_path) { account_path }
+ let(:show_skip_additional_mfa_link) { true }
let(:presenter) do
- described_class.new(user_agent: user_agent)
+ described_class.new(user:, user_agent:, after_mfa_setup_path:, show_skip_additional_mfa_link:)
end
before do
@@ -18,6 +21,12 @@
and_return(false)
end
+ describe '#two_factor_enabled?' do
+ it 'delegates to mfa_policy' do
+ expect(presenter).to delegate_method(:two_factor_enabled?).to(:mfa_policy)
+ end
+ end
+
describe '#options' do
it 'supplies all the options for a user' do
expect(presenter.options.map(&:class)).to eq [
@@ -78,6 +87,24 @@
end
end
+ describe '#skip_path' do
+ subject(:skip_path) { presenter.skip_path }
+
+ it { expect(skip_path).to be_nil }
+
+ context 'with mfa configured' do
+ let(:user) { build(:user, :with_phone) }
+
+ it { expect(skip_path).to eq(after_mfa_setup_path) }
+
+ context 'with skip link hidden' do
+ let(:show_skip_additional_mfa_link) { false }
+
+ it { expect(skip_path).to be_nil }
+ end
+ end
+ end
+
describe '#show_skip_additional_mfa_link?' do
it 'returns true' do
expect(presenter.show_skip_additional_mfa_link?).to eq(true)
diff --git a/spec/services/encryption/encryptors/pii_encryptor_spec.rb b/spec/services/encryption/encryptors/pii_encryptor_spec.rb
index f5d657848aa..c2f442b1465 100644
--- a/spec/services/encryption/encryptors/pii_encryptor_spec.rb
+++ b/spec/services/encryption/encryptors/pii_encryptor_spec.rb
@@ -108,7 +108,7 @@
expect(scrypt_password).to receive(:digest).and_return(scrypt_digest)
expect(SCrypt::Password).to receive(:new).and_return(scrypt_password)
- kms_client = subject.send(:single_region_kms_client)
+ kms_client = subject.send(:multi_region_kms_client)
expect(kms_client).to receive(:decrypt).
with('kms_ciphertext', { 'context' => 'pii-encryption', 'user_uuid' => 'uuid-123-abc' }).
and_return('aes_ciphertext')
@@ -131,5 +131,60 @@
expect(result).to eq(plaintext)
end
+
+ context 'aws_kms_multi_region_read_enabled is set to true' do
+ before do
+ allow(IdentityConfig.store).to receive(:aws_kms_multi_region_read_enabled).and_return(true)
+ end
+
+ it 'uses the multi-region ciphertext if it is available' do
+ test_ciphertext_pair = Encryption::RegionalCiphertextPair.new(
+ single_region_ciphertext: subject.encrypt(
+ 'single-region-text', user_uuid: '123abc'
+ ).single_region_ciphertext,
+ multi_region_ciphertext: subject.encrypt(
+ 'multi-region-text', user_uuid: '123abc'
+ ).multi_region_ciphertext,
+ )
+
+ result = subject.decrypt(test_ciphertext_pair, user_uuid: '123abc')
+
+ expect(result).to eq('multi-region-text')
+ end
+
+ it 'uses the single region ciphertext if the multi-region ciphertext is nil' do
+ test_ciphertext_pair = Encryption::RegionalCiphertextPair.new(
+ single_region_ciphertext: subject.encrypt(
+ 'single-region-text', user_uuid: '123abc'
+ ).single_region_ciphertext,
+ multi_region_ciphertext: nil,
+ )
+
+ result = subject.decrypt(test_ciphertext_pair, user_uuid: '123abc')
+
+ expect(result).to eq('single-region-text')
+ end
+ end
+
+ context 'aws_kms_multi_region_read_enabled is set to false' do
+ before do
+ allow(IdentityConfig.store).to receive(:aws_kms_multi_region_read_enabled).and_return(false)
+ end
+
+ it 'uses the single-region ciphertext to decrypt' do
+ test_ciphertext_pair = Encryption::RegionalCiphertextPair.new(
+ single_region_ciphertext: subject.encrypt(
+ 'single-region-text', user_uuid: '123abc'
+ ).single_region_ciphertext,
+ multi_region_ciphertext: subject.encrypt(
+ 'multi-region-text', user_uuid: '123abc'
+ ).multi_region_ciphertext,
+ )
+
+ result = subject.decrypt(test_ciphertext_pair, user_uuid: '123abc')
+
+ expect(result).to eq('single-region-text')
+ end
+ end
end
end
diff --git a/spec/services/encryption/password_verifier_spec.rb b/spec/services/encryption/password_verifier_spec.rb
index 5ddd973b5ea..14715ef8fed 100644
--- a/spec/services/encryption/password_verifier_spec.rb
+++ b/spec/services/encryption/password_verifier_spec.rb
@@ -129,6 +129,94 @@
expect(bad_match_result).to eq(false)
end
+
+ context 'aws_kms_multi_region_read_enabled is set to true' do
+ before do
+ allow(IdentityConfig.store).to receive(:aws_kms_multi_region_read_enabled).and_return(true)
+ end
+
+ it 'uses the multi-region digest if it is available' do
+ test_digest_pair = Encryption::RegionalCiphertextPair.new(
+ single_region_ciphertext: subject.create_digest_pair(
+ password: 'single-region-password',
+ user_uuid: user_uuid,
+ ).single_region_ciphertext,
+ multi_region_ciphertext: subject.create_digest_pair(
+ password: 'multi-region-password',
+ user_uuid: user_uuid,
+ ).multi_region_ciphertext,
+ )
+
+ single_region_result = subject.verify(
+ password: 'single-region-password',
+ digest_pair: test_digest_pair,
+ user_uuid: user_uuid,
+ )
+ multi_region_result = subject.verify(
+ password: 'multi-region-password',
+ digest_pair: test_digest_pair,
+ user_uuid: user_uuid,
+ )
+
+ expect(single_region_result).to eq(false)
+ expect(multi_region_result).to eq(true)
+ end
+
+ it 'uses the single region digest if the multi-region digest is nil' do
+ test_digest_pair = subject.create_digest_pair(
+ password: password,
+ user_uuid: user_uuid,
+ )
+ test_digest_pair.multi_region_ciphertext = nil
+
+ correct_password_result = subject.verify(
+ password: password,
+ digest_pair: test_digest_pair,
+ user_uuid: user_uuid,
+ )
+ incorrect_password_result = subject.verify(
+ password: 'this is a fake password lol',
+ digest_pair: test_digest_pair,
+ user_uuid: user_uuid,
+ )
+
+ expect(correct_password_result).to eq(true)
+ expect(incorrect_password_result).to eq(false)
+ end
+ end
+
+ context 'aws_kms_multi_region_read_enabled is set to false' do
+ before do
+ allow(IdentityConfig.store).to receive(:aws_kms_multi_region_read_enabled).and_return(false)
+ end
+
+ it 'uses the single-region digest to verify' do
+ test_digest_pair = Encryption::RegionalCiphertextPair.new(
+ single_region_ciphertext: subject.create_digest_pair(
+ password: 'single-region-password',
+ user_uuid: user_uuid,
+ ).single_region_ciphertext,
+ multi_region_ciphertext: subject.create_digest_pair(
+ password: 'multi-region-password',
+ user_uuid: user_uuid,
+ ).multi_region_ciphertext,
+ )
+
+ single_region_result = subject.verify(
+ password: 'single-region-password',
+ digest_pair: test_digest_pair,
+ user_uuid: user_uuid,
+ )
+ multi_region_result = subject.verify(
+ password: 'multi-region-password',
+ digest_pair: test_digest_pair,
+ user_uuid: user_uuid,
+ )
+
+ expect(single_region_result).to eq(true)
+ expect(multi_region_result).to eq(false)
+ end
+ end
end
describe '#stale_digest?' do
diff --git a/spec/services/service_provider_updater_spec.rb b/spec/services/service_provider_updater_spec.rb
index 157d873f61f..5706e7b8f56 100644
--- a/spec/services/service_provider_updater_spec.rb
+++ b/spec/services/service_provider_updater_spec.rb
@@ -27,7 +27,6 @@
block_encryption: 'aes256-cbc',
certs: [saml_test_sp_cert],
active: true,
- native: false,
approved: true,
help_text: {
sign_in: { en: 'A new different sign-in help text' },
@@ -105,7 +104,6 @@
expect(sp.id).to_not eq 0
expect(sp.updated_at).to_not eq friendly_sp[:updated_at]
expect(sp.created_at).to_not eq friendly_sp[:created_at]
- expect(sp.native).to eq false
expect(sp.approved).to eq true
expect(sp.help_text['sign_in']).to eq friendly_sp[:help_text][:sign_in].
stringify_keys
@@ -129,7 +127,6 @@
expect(sp.id).to eq old_id
expect(sp.updated_at).to_not eq friendly_sp[:updated_at]
expect(sp.created_at).to_not eq friendly_sp[:created_at]
- expect(sp.native).to eq false
expect(sp.approved).to eq true
expect(sp.help_text['sign_in']).to eq friendly_sp[:help_text][:sign_in].
stringify_keys
@@ -184,7 +181,7 @@
end
end
- context 'a non-native service provider is invalid' do
+ context 'a service provider is invalid' do
let(:dashboard_service_providers) do
[
{
@@ -200,7 +197,6 @@
block_encryption: 'aes256-cbc',
certs: [saml_test_sp_cert],
active: true,
- native: false,
approved: true,
redirect_uris: [''],
},
@@ -294,25 +290,12 @@
let(:sp) { create(:service_provider, issuer: attributes[:issuer]) }
before { attributes[:active] = false }
- context 'it is not a native service provider' do
- it 'destroys the service_provider' do
- subject.run(attributes)
-
- destroyed_sp = ServiceProvider.find_by(issuer: attributes[:issuer])
-
- expect(destroyed_sp).to be nil
- end
- end
-
- context 'it is a native service provider' do
- before { sp.update!(native: true) }
- it 'is not destroyed' do
- subject.run(attributes)
+ it 'destroys the service_provider' do
+ subject.run(attributes)
- destroyed_sp = ServiceProvider.find_by(issuer: attributes[:issuer])
+ destroyed_sp = ServiceProvider.find_by(issuer: attributes[:issuer])
- expect(destroyed_sp).to eq sp
- end
+ expect(destroyed_sp).to be nil
end
end
end
diff --git a/spec/support/saml_auth_helper.rb b/spec/support/saml_auth_helper.rb
index 2fa63db8610..d89bc561cd4 100644
--- a/spec/support/saml_auth_helper.rb
+++ b/spec/support/saml_auth_helper.rb
@@ -3,6 +3,7 @@
## GET /api/saml/auth helper methods
module SamlAuthHelper
PATH_YEAR = '2023'
+ SP_ISSUER = 'http://localhost:3000'
def saml_settings(overrides: {})
settings = OneLogin::RubySaml::Settings.new
@@ -16,7 +17,7 @@ def saml_settings(overrides: {})
settings.name_identifier_format = Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT
# SP + IdP Settings
- settings.issuer = 'http://localhost:3000'
+ settings.issuer = SP_ISSUER
settings.security[:authn_requests_signed] = true
settings.security[:logout_requests_signed] = true
settings.security[:embed_sign] = true
diff --git a/spec/views/idv/welcome/show.html.erb_spec.rb b/spec/views/idv/welcome/show.html.erb_spec.rb
index 7f257b51322..550c43ffe75 100644
--- a/spec/views/idv/welcome/show.html.erb_spec.rb
+++ b/spec/views/idv/welcome/show.html.erb_spec.rb
@@ -83,6 +83,16 @@
expect(rendered).to have_content(@title)
expect(rendered).to have_content(t('doc_auth.getting_started.instructions.getting_started'))
+ expect(rendered).to have_link(
+ t('doc_auth.info.getting_started_learn_more'),
+ href: help_center_redirect_path(
+ category: 'verify-your-identity',
+ article: 'how-to-verify-your-identity',
+ flow: :idv,
+ step: :welcome_new,
+ location: 'intro_paragraph',
+ ),
+ )
expect(rendered).not_to have_link(
t('doc_auth.instructions.learn_more'),
href: policy_redirect_url(flow: :idv, step: :welcome, location: :footer),
diff --git a/spec/views/shared/_cancel_or_back_to_options.html.erb_spec.rb b/spec/views/shared/_cancel_or_back_to_options.html.erb_spec.rb
new file mode 100644
index 00000000000..446f3959483
--- /dev/null
+++ b/spec/views/shared/_cancel_or_back_to_options.html.erb_spec.rb
@@ -0,0 +1,39 @@
+require 'rails_helper'
+
+RSpec.describe 'shared/_cancel_or_back_to_options.html.erb' do
+ let(:user) { build(:user) }
+ let(:in_multi_mfa_selection_flow) { false }
+
+ subject(:rendered) { render }
+
+ before do
+ allow(view).to receive(:current_user).and_return(user)
+ allow(view).to receive(:in_multi_mfa_selection_flow?).and_return(in_multi_mfa_selection_flow)
+ end
+
+ it 'renders link to choose another authentication method' do
+ expect(rendered).to have_link(
+ t('two_factor_authentication.choose_another_option'),
+ href: authentication_methods_setup_path,
+ )
+ end
+
+ context 'with mfa configured' do
+ let(:user) { build(:user, :with_phone) }
+
+ it 'renders link to cancel and return to account' do
+ expect(rendered).to have_link(t('links.cancel'), href: account_path)
+ end
+
+ context 'when in multi mfa selection flow' do
+ let(:in_multi_mfa_selection_flow) { true }
+
+ it 'renders link to choose another authentication method' do
+ expect(rendered).to have_link(
+ t('two_factor_authentication.choose_another_option'),
+ href: authentication_methods_setup_path,
+ )
+ end
+ end
+ end
+end
diff --git a/spec/views/two_factor_authentication/otp_verification/show.html.erb_spec.rb b/spec/views/two_factor_authentication/otp_verification/show.html.erb_spec.rb
index e7303fe16a6..e276bd53324 100644
--- a/spec/views/two_factor_authentication/otp_verification/show.html.erb_spec.rb
+++ b/spec/views/two_factor_authentication/otp_verification/show.html.erb_spec.rb
@@ -229,7 +229,7 @@
render
- expect(rendered).to have_link(t('forms.two_factor.try_again'), href: add_phone_path)
+ expect(rendered).to have_link(t('forms.two_factor.try_again'), href: phone_setup_path)
end
end
@@ -244,7 +244,7 @@
)
render
- expect(rendered).to have_link(t('forms.two_factor.try_again'), href: add_phone_path)
+ expect(rendered).to have_link(t('forms.two_factor.try_again'), href: phone_setup_path)
end
end
@@ -312,7 +312,7 @@
expect(rendered).to have_link(
t('two_factor_authentication.phone_verification.troubleshooting.change_number'),
- href: add_phone_path,
+ href: phone_setup_path,
)
end
end
diff --git a/spec/views/users/two_factor_authentication_setup/index.html.erb_spec.rb b/spec/views/users/two_factor_authentication_setup/index.html.erb_spec.rb
index b7ecb9d1972..b6714f86533 100644
--- a/spec/views/users/two_factor_authentication_setup/index.html.erb_spec.rb
+++ b/spec/views/users/two_factor_authentication_setup/index.html.erb_spec.rb
@@ -3,14 +3,64 @@
RSpec.describe 'users/two_factor_authentication_setup/index.html.erb' do
include Devise::Test::ControllerHelpers
+ let(:user) { build(:user) }
+ let(:user_agent) { '' }
+ let(:show_skip_additional_mfa_link) { true }
+ let(:after_mfa_setup_path) { account_path }
subject(:rendered) { render }
before do
- user = build_stubbed(:user)
- @presenter = TwoFactorOptionsPresenter.new(user_agent: '', user: user)
+ @presenter = TwoFactorOptionsPresenter.new(
+ user_agent:,
+ user:,
+ show_skip_additional_mfa_link:,
+ after_mfa_setup_path:,
+ )
@two_factor_options_form = TwoFactorLoginOptionsForm.new(user)
end
+ it 'has link to cancel account creation' do
+ render
+
+ expect(rendered).to have_css('.page-footer')
+ expect(rendered).to have_link(t('links.cancel_account_creation'), href: sign_up_cancel_path)
+ end
+
+ it 'does not list currently configured mfa methods' do
+ render
+
+ expect(rendered).not_to have_content(t('headings.account.two_factor'))
+ end
+
+ context 'with configured mfa methods' do
+ let(:user) { build(:user, :with_phone) }
+
+ it 'lists currently configured mfa methods' do
+ render
+
+ expect(rendered).to have_content(t('headings.account.two_factor'))
+ end
+
+ it 'has link to skip additional mfa setup' do
+ render
+
+ expect(rendered).to have_css('.page-footer')
+ expect(rendered).to have_link(t('mfa.skip'), href: after_mfa_setup_path)
+ end
+
+ context 'with skip link hidden' do
+ let(:show_skip_additional_mfa_link) { false }
+
+ it 'does not have footer link' do
+ render
+
+ expect(rendered).not_to have_css('.page-footer')
+ expect(rendered).not_to have_link(t('links.cancel_account_creation'))
+ expect(rendered).not_to have_link(t('mfa.skip'))
+ end
+ end
+ end
+
context 'all phone vendor outage' do
before do
allow_any_instance_of(OutageStatus).to receive(:all_vendor_outage?).
diff --git a/yarn.lock b/yarn.lock
index ae4673c323d..fcd3b3e641b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4457,10 +4457,10 @@ levn@^0.4.1:
prelude-ls "^1.2.1"
type-check "~0.4.0"
-libphonenumber-js@^1.10.39:
- version "1.10.39"
- resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.39.tgz#12dd512621c9ebb13402a694ac81dc78511cd982"
- integrity sha512-iPMM/NbSNIrdwbr94rAOos6krB7snhfzEptmk/DJUtTPs+P9gOhZ1YXVPcRgjpp3jJByclfm/Igvz45spfJK7g==
+libphonenumber-js@^1.10.41:
+ version "1.10.41"
+ resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.41.tgz#14b6be5894bed3385808a6a088031b5b8a27c105"
+ integrity sha512-4rmmF4u4vD3eGNuuCGjCPwRwO+fIuu1WWcS7VwbPTiMFkJd8F02v8o5pY5tlYuMR+xOvJ88mtOHpkm0Tnu2LcQ==
lightningcss-darwin-arm64@1.16.1:
version "1.16.1"