diff --git a/app/assets/stylesheets/_uswds-core.scss b/app/assets/stylesheets/_uswds-core.scss index 656649b41b0..b236d00d03e 100644 --- a/app/assets/stylesheets/_uswds-core.scss +++ b/app/assets/stylesheets/_uswds-core.scss @@ -1,7 +1,3 @@ -// Prevent USWDS from rendering `@font-face` in every stylesheet. One stylesheet should override -// this to ensure that the `@font-face` is defined at least once. -$render-font-face: false !default; - @forward '@18f/identity-design-system/packages/uswds-core' with ( $theme-body-font-size: 'sm', $theme-font-path: '', @@ -12,21 +8,6 @@ $render-font-face: false !default; $theme-header-min-width: 'tablet', $theme-link-visited-color: 'primary', $theme-style-body-element: true, - $theme-typeface-tokens: - if( - $render-font-face, - (), - ( - 'roboto-mono': ( - cap-height: 380px, - src: false, - ), - 'public-sans': ( - cap-height: 362px, - src: false, - ), - ) - ), $output-these-utilities: ( 'add-list-reset', 'align-items', diff --git a/app/assets/stylesheets/_uswds-fonts.scss b/app/assets/stylesheets/_uswds-fonts.scss new file mode 100644 index 00000000000..7d762676bd7 --- /dev/null +++ b/app/assets/stylesheets/_uswds-fonts.scss @@ -0,0 +1,3 @@ +// This stylesheet stubs the default uswds-fonts package to prevent USWDS from rendering all fonts +// declarations in all stylesheets. They are rendered once by removing the contents of the common +// root import, and referencing the source files in the main application stylesheet. diff --git a/app/assets/stylesheets/_uswds-form-controls.scss b/app/assets/stylesheets/_uswds-form-controls.scss index bcb8825cf56..2f4c2842cbe 100644 --- a/app/assets/stylesheets/_uswds-form-controls.scss +++ b/app/assets/stylesheets/_uswds-form-controls.scss @@ -7,7 +7,6 @@ @forward 'usa-input'; @forward 'usa-label'; @forward 'usa-legend'; -@forward 'usa-memorable-date'; @forward 'usa-radio'; @forward 'usa-select'; @forward 'usa-success-message'; diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss index dd641787da0..8134b3f36c8 100644 --- a/app/assets/stylesheets/application.css.scss +++ b/app/assets/stylesheets/application.css.scss @@ -1,6 +1,5 @@ -@forward 'uswds-core' with ( - $render-font-face: true -); +@forward 'uswds-core'; +@forward 'uswds-fonts/src/styles'; @forward 'uswds'; @forward 'design-system-waiting-room'; @forward 'components'; diff --git a/app/assets/stylesheets/components/_index.scss b/app/assets/stylesheets/components/_index.scss index c51731e14bd..c21d2bcec7e 100644 --- a/app/assets/stylesheets/components/_index.scss +++ b/app/assets/stylesheets/components/_index.scss @@ -6,7 +6,6 @@ @forward 'btn'; @forward 'card'; @forward 'code'; -@forward 'click-observer'; @forward 'file-input'; @forward 'form-steps'; @forward 'footer'; @@ -16,15 +15,10 @@ @forward 'icon'; @forward 'language-picker'; @forward 'list'; -@forward 'modal'; @forward 'nav'; -@forward 'one-time-code-input'; @forward 'page-heading'; -@forward 'phone-input'; @forward 'password'; -@forward 'password-toggle'; @forward 'profile-section'; -@forward 'recaptcha-badge'; @forward 'personal-key'; @forward 'spinner-button'; @forward 'spinner-dots'; diff --git a/app/assets/stylesheets/email.css.scss b/app/assets/stylesheets/email.css.scss index b312c06ea99..20a619cab65 100644 --- a/app/assets/stylesheets/email.css.scss +++ b/app/assets/stylesheets/email.css.scss @@ -1,7 +1,6 @@ @use 'uswds-core' as *; @use 'variables/app' as *; @use 'variables/email' as *; -@import 'foundation-emails/scss/foundation-emails'; .gray { &:active, diff --git a/app/assets/stylesheets/variables/_email.scss b/app/assets/stylesheets/variables/_email.scss index e670dc8a5f7..134d4856e85 100644 --- a/app/assets/stylesheets/variables/_email.scss +++ b/app/assets/stylesheets/variables/_email.scss @@ -148,3 +148,96 @@ $thumbnail-shadow: 0 0 0 1px rgba($black, 0.2); $thumbnail-shadow-hover: 0 0 6px 1px rgba($primary-color, 0.5); $thumbnail-transition: box-shadow 200ms ease-out; $thumbnail-radius: $global-radius; + +@forward 'foundation-emails/scss/foundation-emails' with ( + $primary-color: $primary-color, + $secondary-color: $secondary-color, + $success-color: $success-color, + $warning-color: $warning-color, + $alert-color: $alert-color, + $light-gray: $light-gray, + $black: $black, + $white: $white, + $medium-gray: $medium-gray, + $dark-gray: $dark-gray, + $pre-color: $pre-color, + $global-width: $global-width, + $global-width-small: $global-width-small, + $global-gutter: $global-gutter, + $global-gutter-small: $global-gutter-small, + $body-background: $body-background, + $container-background: $container-background, + $global-padding: $global-padding, + $global-margin: $global-margin, + $global-radius: $global-radius, + $global-rounded: $global-rounded, + $global-breakpoint: $global-breakpoint, + $grid-column-count: $grid-column-count, + $column-padding-bottom: $column-padding-bottom, + $container-radius: $container-radius, + $block-grid-max: $block-grid-max, + $block-grid-gutter: $block-grid-gutter, + $global-font-color: $global-font-color, + $body-font-family: $body-font-family, + $global-font-weight: $global-font-weight, + $header-color: $header-color, + $global-line-height: $global-line-height, + $global-font-size: $global-font-size, + $body-line-height: $body-line-height, + $header-font-family: $header-font-family, + $header-font-weight: $header-font-weight, + $h1-font-size: $h1-font-size, + $h2-font-size: $h2-font-size, + $h3-font-size: $h3-font-size, + $h4-font-size: $h4-font-size, + $h5-font-size: $h5-font-size, + $h6-font-size: $h6-font-size, + $header-margin-bottom: $header-margin-bottom, + $paragraph-margin-bottom: $paragraph-margin-bottom, + $small-font-size: $small-font-size, + $small-font-color: $small-font-color, + $lead-font-size: $lead-font-size, + $lead-line-height: $lead-line-height, + $text-padding: $text-padding, + $subheader-lineheight: $subheader-lineheight, + $subheader-color: $subheader-color, + $subheader-font-weight: $subheader-font-weight, + $subheader-margin-top: $subheader-margin-top, + $subheader-margin-bottom: $subheader-margin-bottom, + $hr-width: $hr-width, + $hr-border: $hr-border, + $hr-margin: $hr-margin, + $anchor-text-decoration: $anchor-text-decoration, + $anchor-color: $anchor-color, + $anchor-color-visited: $anchor-color-visited, + $anchor-color-hover: $anchor-color-hover, + $anchor-color-active: $anchor-color-active, + $stat-font-size: $stat-font-size, + $button-padding: $button-padding, + $button-font-size: $button-font-size, + $button-color: $button-color, + $button-color-alt: $button-color-alt, + $button-font-weight: $button-font-weight, + $button-margin: $button-margin, + $button-background: $button-background, + $button-border: $button-border, + $button-radius: $button-radius, + $button-rounded: $button-rounded, + $callout-background-fade: $callout-background-fade, + $callout-padding: $callout-padding, + $callout-margin-bottom: $callout-margin-bottom, + $callout-border: $callout-border, + $callout-border-secondary: $callout-border-secondary, + $callout-border-success: $callout-border-success, + $callout-border-warning: $callout-border-warning, + $callout-border-alert: $callout-border-alert, + $menu-item-padding: $menu-item-padding, + $menu-item-gutter: $menu-item-gutter, + $menu-item-color: $menu-item-color, + $thumbnail-border: $thumbnail-border, + $thumbnail-margin-bottom: $thumbnail-margin-bottom, + $thumbnail-shadow: $thumbnail-shadow, + $thumbnail-shadow-hover: $thumbnail-shadow-hover, + $thumbnail-transition: $thumbnail-transition, + $thumbnail-radius: $thumbnail-radius +); diff --git a/app/assets/stylesheets/components/_recaptcha-badge.scss b/app/components/captcha_submit_button_component.scss similarity index 100% rename from app/assets/stylesheets/components/_recaptcha-badge.scss rename to app/components/captcha_submit_button_component.scss diff --git a/app/assets/stylesheets/components/_click-observer.scss b/app/components/click_observer_component.scss similarity index 100% rename from app/assets/stylesheets/components/_click-observer.scss rename to app/components/click_observer_component.scss diff --git a/app/components/memorable_date_component.scss b/app/components/memorable_date_component.scss new file mode 100644 index 00000000000..a8858908870 --- /dev/null +++ b/app/components/memorable_date_component.scss @@ -0,0 +1,3 @@ +@use 'uswds-core' as *; + +@forward 'usa-memorable-date'; diff --git a/app/assets/stylesheets/components/_modal.scss b/app/components/modal_component.scss similarity index 95% rename from app/assets/stylesheets/components/_modal.scss rename to app/components/modal_component.scss index 7611bc77fc0..c92cf63bcc9 100644 --- a/app/assets/stylesheets/components/_modal.scss +++ b/app/components/modal_component.scss @@ -1,5 +1,5 @@ @use 'uswds-core' as *; -@use '../variables/app' as *; +@use 'variables/app' as *; .usa-modal-overlay { // Temporary styles to avoid inheriting too much of the USWDS opinionated modal styling until diff --git a/app/assets/stylesheets/components/_one-time-code-input.scss b/app/components/one_time_code_input_component.scss similarity index 100% rename from app/assets/stylesheets/components/_one-time-code-input.scss rename to app/components/one_time_code_input_component.scss diff --git a/app/assets/stylesheets/components/_password-toggle.scss b/app/components/password_toggle_component.scss similarity index 100% rename from app/assets/stylesheets/components/_password-toggle.scss rename to app/components/password_toggle_component.scss diff --git a/app/assets/stylesheets/components/_phone-input.scss b/app/components/phone_input_component.scss similarity index 100% rename from app/assets/stylesheets/components/_phone-input.scss rename to app/components/phone_input_component.scss diff --git a/app/controllers/accounts/personal_keys_controller.rb b/app/controllers/accounts/personal_keys_controller.rb index 4014ab62a12..08ccfb97899 100644 --- a/app/controllers/accounts/personal_keys_controller.rb +++ b/app/controllers/accounts/personal_keys_controller.rb @@ -6,7 +6,7 @@ class PersonalKeysController < ApplicationController before_action :confirm_two_factor_authenticated before_action :prompt_for_password_if_pii_locked - before_action :confirm_recently_authenticated + before_action :confirm_recently_authenticated_2fa def new analytics.profile_personal_key_visit diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 986bb6bc8b4..aee94d56303 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -16,7 +16,6 @@ def show user: current_user, locked_for_session: pii_locked_for_session?(current_user), ) - @use_reauthentication_route = FeatureManagement.use_reauthentication_route? end # This action is used to re-authenticate when PII on the account page is locked on `show` action diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb index de946e5883e..37fd362d35f 100644 --- a/app/controllers/concerns/idv/verify_info_concern.rb +++ b/app/controllers/concerns/idv/verify_info_concern.rb @@ -13,7 +13,7 @@ def shared_update pii[:uuid_prefix] = ServiceProvider.find_by(issuer: sp_session[:issuer])&.app_id set_state_id_type - ssn_throttle.increment! + ssn_rate_limiter.increment! document_capture_session = DocumentCaptureSession.create( user_id: current_user.id, @@ -61,17 +61,17 @@ def aamva_disallowed_for_service_provider? banlist.include?(sp_session[:issuer]) end - def resolution_throttle - @resolution_throttle ||= Throttle.new( + def resolution_rate_limiter + @resolution_rate_limiter ||= RateLimiter.new( user: current_user, - throttle_type: :idv_resolution, + rate_limit_type: :idv_resolution, ) end - def ssn_throttle - @ssn_throttle ||= Throttle.new( + def ssn_rate_limiter + @ssn_rate_limiter ||= RateLimiter.new( target: Pii::Fingerprinter.fingerprint(pii[:ssn]), - throttle_type: :proof_ssn, + rate_limit_type: :proof_ssn, ) end @@ -85,14 +85,14 @@ def idv_failure(result) :mva_exception, ) - resolution_throttle.increment! if proofing_results_exception.blank? + resolution_rate_limiter.increment! if proofing_results_exception.blank? - if ssn_throttle.throttled? - idv_failure_log_throttled(:proof_ssn) + if ssn_rate_limiter.limited? + idv_failure_log_rate_limited(:proof_ssn) redirect_to idv_session_errors_ssn_failure_url - elsif resolution_throttle.throttled? - idv_failure_log_throttled(:idv_resolution) - redirect_to throttled_url + elsif resolution_rate_limiter.limited? + idv_failure_log_rate_limited(:idv_resolution) + redirect_to rate_limited_url elsif proofing_results_exception.present? && is_mva_exception idv_failure_log_warning redirect_to state_id_warning_url @@ -105,14 +105,14 @@ def idv_failure(result) end end - def idv_failure_log_throttled(throttle_type) - if throttle_type == :proof_ssn + def idv_failure_log_rate_limited(rate_limit_type) + if rate_limit_type == :proof_ssn irs_attempts_api_tracker.idv_verification_rate_limited(throttle_context: 'multi-session') analytics.throttler_rate_limit_triggered( throttle_type: :proof_ssn, step_name: STEP_NAME, ) - elsif throttle_type == :idv_resolution + elsif rate_limit_type == :idv_resolution irs_attempts_api_tracker.idv_verification_rate_limited(throttle_context: 'single-session') analytics.throttler_rate_limit_triggered( throttle_type: :idv_resolution, @@ -124,18 +124,18 @@ def idv_failure_log_throttled(throttle_type) def idv_failure_log_error analytics.idv_doc_auth_exception_visited( step_name: STEP_NAME, - remaining_attempts: resolution_throttle.remaining_count, + remaining_attempts: resolution_rate_limiter.remaining_count, ) end def idv_failure_log_warning analytics.idv_doc_auth_warning_visited( step_name: STEP_NAME, - remaining_attempts: resolution_throttle.remaining_count, + remaining_attempts: resolution_rate_limiter.remaining_count, ) end - def throttled_url + def rate_limited_url idv_session_errors_failure_url end @@ -203,7 +203,7 @@ def async_state_done(current_async_state) ) form_response = form_response.merge(check_ssn) if form_response.success? - summarize_result_and_throttle_failures(form_response) + summarize_result_and_rate_limit_failures(form_response) delete_async if form_response.success? @@ -231,10 +231,10 @@ def save_threatmetrix_status(form_response) idv_session.threatmetrix_review_status = review_status end - def summarize_result_and_throttle_failures(summary_result) + def summarize_result_and_rate_limit_failures(summary_result) if summary_result.success? add_proofing_components - ssn_throttle.reset! + ssn_rate_limiter.reset! else idv_failure(summary_result) end diff --git a/app/controllers/concerns/idv_step_concern.rb b/app/controllers/concerns/idv_step_concern.rb index 50d044a60fd..0fad0e3b67f 100644 --- a/app/controllers/concerns/idv_step_concern.rb +++ b/app/controllers/concerns/idv_step_concern.rb @@ -31,7 +31,7 @@ def pii_from_doc # copied from doc_auth_controller def flow_path - flow_session[:flow_path] + idv_session.flow_path || flow_session[:flow_path] end private diff --git a/app/controllers/concerns/rate_limit_concern.rb b/app/controllers/concerns/rate_limit_concern.rb index ee19cd1ef49..a789ee84f96 100644 --- a/app/controllers/concerns/rate_limit_concern.rb +++ b/app/controllers/concerns/rate_limit_concern.rb @@ -3,8 +3,8 @@ module RateLimitConcern def confirm_not_rate_limited rate_limited = false - %i[idv_resolution idv_doc_auth proof_address proof_ssn].each do |throttle_type| - if rate_limit_redirect!(throttle_type) + %i[idv_resolution idv_doc_auth proof_address proof_ssn].each do |rate_limit_type| + if rate_limit_redirect!(rate_limit_type) rate_limited = true break end @@ -12,21 +12,21 @@ def confirm_not_rate_limited rate_limited end - def rate_limit_redirect!(throttle_type) - if idv_attempter_rate_limited?(throttle_type) - track_rate_limited_event(throttle_type) - rate_limited_redirect(throttle_type) + def rate_limit_redirect!(rate_limit_type) + if idv_attempter_rate_limited?(rate_limit_type) + track_rate_limited_event(rate_limit_type) + rate_limited_redirect(rate_limit_type) return true end end - def track_rate_limited_event(throttle_type) - analytics_args = { throttle_type: throttle_type } + def track_rate_limited_event(rate_limit_type) + analytics_args = { throttle_type: rate_limit_type } throttle_context = 'single-session' - if throttle_type == :proof_address + if rate_limit_type == :proof_address analytics_args[:step_name] = :phone - elsif throttle_type == :proof_ssn + elsif rate_limit_type == :proof_ssn analytics_args[:step_name] = 'verify_info' throttle_context = 'multi-session' end @@ -35,8 +35,8 @@ def track_rate_limited_event(throttle_type) analytics.throttler_rate_limit_triggered(**analytics_args) end - def rate_limited_redirect(throttle_type) - case throttle_type + def rate_limited_redirect(rate_limit_type) + case rate_limit_type when :idv_resolution redirect_to idv_session_errors_failure_url when :idv_doc_auth @@ -48,18 +48,18 @@ def rate_limited_redirect(throttle_type) end end - def idv_attempter_rate_limited?(throttle_type) - if throttle_type == :proof_ssn + def idv_attempter_rate_limited?(rate_limit_type) + if rate_limit_type == :proof_ssn return unless pii_ssn - Throttle.new( + RateLimiter.new( target: Pii::Fingerprinter.fingerprint(pii_ssn), - throttle_type: :proof_ssn, - ).throttled? + rate_limit_type: :proof_ssn, + ).limited? else - Throttle.new( + RateLimiter.new( user: idv_session_user, - throttle_type: throttle_type, - ).throttled? + rate_limit_type: rate_limit_type, + ).limited? end end diff --git a/app/controllers/concerns/reauthentication_required_concern.rb b/app/controllers/concerns/reauthentication_required_concern.rb index 5bafdbcf1d8..2057bac6afa 100644 --- a/app/controllers/concerns/reauthentication_required_concern.rb +++ b/app/controllers/concerns/reauthentication_required_concern.rb @@ -1,18 +1,6 @@ module ReauthenticationRequiredConcern include MfaSetupConcern - def confirm_recently_authenticated - if IdentityConfig.store.reauthentication_for_second_factor_management_enabled - confirm_recently_authenticated_2fa - else - @reauthn = reauthn? - return unless user_signed_in? - return if recently_authenticated? - - prompt_for_current_password - end - end - def confirm_recently_authenticated_2fa @reauthn = reauthn? return unless user_fully_authenticated? @@ -38,14 +26,6 @@ def recently_authenticated? authn_at > Time.zone.now - IdentityConfig.store.reauthn_window end - def prompt_for_current_password - store_location(request.url) - user_session[:context] = 'reauthentication' - user_session[:factor_to_change] = factor_from_controller_name - user_session[:current_password_required] = true - redirect_to user_password_confirm_url - end - def prompt_for_second_factor store_location(request.url) user_session[:context] = 'reauthentication' @@ -53,16 +33,6 @@ def prompt_for_second_factor redirect_to login_two_factor_options_path(reauthn: true) end - def factor_from_controller_name - { - # see LG-5701, translate these - 'emails' => 'email', - 'passwords' => 'password', - 'phones' => 'phone', - 'personal_keys' => 'personal key', - }[controller_name] - end - def store_location(url) user_session[:stored_location] = url end diff --git a/app/controllers/concerns/remember_device_concern.rb b/app/controllers/concerns/remember_device_concern.rb index efc1b6bf5e3..3eee6b1cc47 100644 --- a/app/controllers/concerns/remember_device_concern.rb +++ b/app/controllers/concerns/remember_device_concern.rb @@ -14,11 +14,7 @@ def save_remember_device_preference end def check_remember_device_preference - if IdentityConfig.store.reauthentication_for_second_factor_management_enabled - return unless UserSessionContext.authentication_context?(context) - else - return unless UserSessionContext.authentication_or_reauthentication_context?(context) - end + return unless UserSessionContext.authentication_context?(context) return if remember_device_cookie.nil? return unless remember_device_cookie.valid_for_user?( user: current_user, diff --git a/app/controllers/idv/agreement_controller.rb b/app/controllers/idv/agreement_controller.rb index d4cc0d041c4..8001c42632e 100644 --- a/app/controllers/idv/agreement_controller.rb +++ b/app/controllers/idv/agreement_controller.rb @@ -48,7 +48,8 @@ def analytics_arguments def skip_to_capture flow_session[:skip_upload_step] = true - flow_session[:flow_path] = 'standard' + idv_session.flow_path = 'standard' + flow_session[:flow_path] = 'standard' # temp added for 50/50, remove in future deploy end def consent_form_params diff --git a/app/controllers/idv/capture_doc_status_controller.rb b/app/controllers/idv/capture_doc_status_controller.rb index 2503771a2a2..2d226b0111c 100644 --- a/app/controllers/idv/capture_doc_status_controller.rb +++ b/app/controllers/idv/capture_doc_status_controller.rb @@ -16,7 +16,7 @@ def status :unauthorized elsif document_capture_session.cancelled_at :gone - elsif throttled? + elsif rate_limiter.limited? :too_many_requests elsif confirmed_barcode_attention_result? || user_has_establishing_in_person_enrollment? :ok @@ -33,7 +33,7 @@ def status def redirect_url return unless flow_session && document_capture_session - if throttled? + if rate_limiter.limited? idv_session_errors_throttled_url elsif user_has_establishing_in_person_enrollment? idv_in_person_url @@ -59,12 +59,11 @@ def document_capture_session_uuid flow_session[:document_capture_session_uuid] end - def throttled? - throttle.throttled? - end - - def throttle - @throttle ||= Throttle.new(user: document_capture_session.user, throttle_type: :idv_doc_auth) + def rate_limiter + @rate_limiter ||= RateLimiter.new( + user: document_capture_session.user, + rate_limit_type: :idv_doc_auth, + ) end def user_has_establishing_in_person_enrollment? diff --git a/app/controllers/idv/document_capture_controller.rb b/app/controllers/idv/document_capture_controller.rb index f4762eded50..cacfa95cdec 100644 --- a/app/controllers/idv/document_capture_controller.rb +++ b/app/controllers/idv/document_capture_controller.rb @@ -51,7 +51,8 @@ def extra_view_variables private def confirm_hybrid_handoff_complete - return if flow_session[:flow_path].present? + return if idv_session.flow_path.present? + return if flow_session[:flow_path].present? # remove in future deploy redirect_to idv_hybrid_handoff_url end diff --git a/app/controllers/idv/gpo_verify_controller.rb b/app/controllers/idv/gpo_verify_controller.rb index 5fd27f8dd2d..db9beadc674 100644 --- a/app/controllers/idv/gpo_verify_controller.rb +++ b/app/controllers/idv/gpo_verify_controller.rb @@ -18,8 +18,8 @@ def index !gpo_mail.mail_spammed? && !gpo_mail.profile_too_old? - if throttle.throttled? - render_throttled + if rate_limiter.limited? + render_rate_limited elsif pii_locked? redirect_to capture_password_url else @@ -34,9 +34,9 @@ def pii def create @gpo_verify_form = build_gpo_verify_form - throttle.increment! - if throttle.throttled? - render_throttled + rate_limiter.increment! + if rate_limiter.limited? + render_rate_limited return end @@ -85,20 +85,20 @@ def prepare_for_personal_key idv_session.address_confirmed! end - def throttle - @throttle ||= Throttle.new( + def rate_limiter + @rate_limiter ||= RateLimiter.new( user: current_user, - throttle_type: :verify_gpo_key, + rate_limit_type: :verify_gpo_key, ) end - def render_throttled + def render_rate_limited irs_attempts_api_tracker.idv_gpo_verification_rate_limited analytics.throttler_rate_limit_triggered( throttle_type: :verify_gpo_key, ) - @expires_at = throttle.expires_at + @expires_at = rate_limiter.expires_at render :throttled end diff --git a/app/controllers/idv/hybrid_handoff_controller.rb b/app/controllers/idv/hybrid_handoff_controller.rb index 7c8f2ab3d5f..2539f737034 100644 --- a/app/controllers/idv/hybrid_handoff_controller.rb +++ b/app/controllers/idv/hybrid_handoff_controller.rb @@ -38,10 +38,11 @@ def hybrid_flow_chosen? end def handle_phone_submission - throttle.increment! - return throttled_failure if throttle.throttled? + rate_limiter.increment! + return rate_limited_failure if rate_limiter.limited? idv_session.phone_for_mobile_flow = params[:doc_auth][:phone] - flow_session[:flow_path] = 'hybrid' + idv_session.flow_path = 'hybrid' + flow_session[:flow_path] = 'hybrid' # temp addition for 50/50 remove in future deploy telephony_result = send_link telephony_form_response = build_telephony_form_response(telephony_result) @@ -60,7 +61,8 @@ def handle_phone_submission redirect_to idv_link_sent_url else redirect_to idv_hybrid_handoff_url - flow_session[:flow_path] = nil + idv_session.flow_path = nil + flow_session[:flow_path] = nil # temp added for 50/50, remove in future deploy end analytics.idv_doc_auth_upload_submitted( @@ -97,7 +99,7 @@ def build_telephony_form_response(telephony_result) extra: { telephony_response: telephony_result.to_h, destination: :link_sent, - flow_path: flow_session[:flow_path], + flow_path: idv_session.flow_path || flow_session[:flow_path], # remove in future deploy }, ) end @@ -113,7 +115,8 @@ def update_document_capture_session_requested_at(session_uuid) end def bypass_send_link_steps - flow_session[:flow_path] = 'standard' + idv_session.flow_path = 'standard' + flow_session[:flow_path] = 'standard' # temp added for 50/50, remove in future deploy redirect_to idv_document_capture_url analytics.idv_doc_auth_upload_submitted( @@ -144,10 +147,10 @@ def build_form ) end - def throttle - @throttle ||= Throttle.new( + def rate_limiter + @rate_limiter ||= RateLimiter.new( user: current_user, - throttle_type: :idv_send_link, + rate_limit_type: :idv_send_link, ) end @@ -167,12 +170,12 @@ def form_response(destination:) extra: { destination: destination, skip_upload_step: mobile_device?, - flow_path: flow_session[:flow_path], + flow_path: idv_session.flow_path, }, ) end - def throttled_failure + def rate_limited_failure analytics.throttler_rate_limit_triggered( throttle_type: :idv_send_link, ) @@ -180,7 +183,7 @@ def throttled_failure 'errors.doc_auth.send_link_throttle', timeout: distance_of_time_in_words( Time.zone.now, - [throttle.expires_at, Time.zone.now].compact.max, + [rate_limiter.expires_at, Time.zone.now].compact.max, except: :seconds, ), ) @@ -210,13 +213,15 @@ def confirm_agreement_step_complete def confirm_hybrid_handoff_needed setup_for_redo if params[:redo] + idv_session.flow_path = 'standard' if flow_session[:skip_upload_step] + # next line temp added for 50/50, remove in future deploy flow_session[:flow_path] = 'standard' if flow_session[:skip_upload_step] + # flow_session temp added for 50/50, remove in future deploy. + return if !idv_session.flow_path && !flow_session[:flow_path] - return if !flow_session[:flow_path] - - if flow_session[:flow_path] == 'standard' + if idv_session.flow_path == 'standard' || flow_session[:flow_path] == 'standard' redirect_to idv_document_capture_url - elsif flow_session[:flow_path] == 'hybrid' + elsif idv_session.flow_path == 'hybrid' || flow_session[:flow_path] == 'hybrid' redirect_to idv_link_sent_url end end @@ -224,9 +229,11 @@ def confirm_hybrid_handoff_needed def setup_for_redo flow_session[:redo_document_capture] = true if flow_session[:skip_upload_step] - flow_session[:flow_path] = 'standard' + idv_session.flow_path = 'standard' + flow_session[:flow_path] = 'standard' # temp added for 50/50, remove in future deploy else - flow_session[:flow_path] = nil + idv_session.flow_path = nil + flow_session[:flow_path] = nil # temp added for 50/50, remove in future deploy end end diff --git a/app/controllers/idv/link_sent_controller.rb b/app/controllers/idv/link_sent_controller.rb index 24862d4712a..9db8ca8f2eb 100644 --- a/app/controllers/idv/link_sent_controller.rb +++ b/app/controllers/idv/link_sent_controller.rb @@ -39,9 +39,10 @@ def extra_view_variables private def confirm_hybrid_handoff_complete + return if idv_session.flow_path == 'hybrid' return if flow_session[:flow_path] == 'hybrid' - if flow_session[:flow_path] == 'standard' + if idv_session.flow_path == 'standard' || flow_session[:flow_path] == 'standard' redirect_to idv_document_capture_url else redirect_to idv_hybrid_handoff_url @@ -69,12 +70,14 @@ def analytics_arguments def handle_document_verification_success(get_results_response) save_proofing_components(current_user) extract_pii_from_doc(current_user, get_results_response, store_in_session: true) - flow_session[:flow_path] = 'hybrid' + idv_session.flow_path = 'hybrid' + flow_session[:flow_path] = 'hybrid' # temp added for 50/50, remove in future deploy end def render_document_capture_cancelled redirect_to idv_hybrid_handoff_url - flow_session[:flow_path] = nil + idv_session.flow_path = nil + flow_session[:flow_path] = nil # temp added for 50/50, remove in future deploy failure(I18n.t('errors.doc_auth.document_capture_cancelled')) end diff --git a/app/controllers/idv/otp_verification_controller.rb b/app/controllers/idv/otp_verification_controller.rb index dc0a5138578..57f80b68968 100644 --- a/app/controllers/idv/otp_verification_controller.rb +++ b/app/controllers/idv/otp_verification_controller.rb @@ -32,6 +32,7 @@ def update if result.success? idv_session.user_phone_confirmation = true + save_in_person_notification_phone flash[:success] = t('idv.messages.review.phone_verified') redirect_to idv_review_url else @@ -70,6 +71,27 @@ def handle_otp_confirmation_failure end end + def save_in_person_notification_phone + return unless IdentityConfig.store.in_person_send_proofing_notifications_enabled + return unless in_person_enrollment? + # future tickets will support voice otp + return unless idv_session.user_phone_confirmation_session.delivery_method == :sms + + establishing_enrollment.notification_phone_configuration = + NotificationPhoneConfiguration.new( + phone: idv_session.user_phone_confirmation_session.phone, + ) + end + + def establishing_enrollment + current_user.establishing_in_person_enrollment + end + + def in_person_enrollment? + return false unless IdentityConfig.store.in_person_proofing_enabled + establishing_enrollment.present? + end + def phone_confirmation_otp_verification_form @phone_confirmation_otp_verification_form ||= PhoneConfirmationOtpVerificationForm.new( user: current_user, diff --git a/app/controllers/idv/phone_controller.rb b/app/controllers/idv/phone_controller.rb index bb93471307f..d1f5cd6ca05 100644 --- a/app/controllers/idv/phone_controller.rb +++ b/app/controllers/idv/phone_controller.rb @@ -19,7 +19,7 @@ def new async_state = step.async_state # It's possible that create redirected here after a success and left the - # throttle maxed out. Check for success before checking throttle. + # rate_limiter maxed out. Check for success before checking rate_limiter. return async_state_done(async_state) if async_state.done? render 'shared/wait' and return if async_state.in_progress? @@ -58,8 +58,8 @@ def create private - def throttle - @throttle ||= Throttle.new(user: current_user, throttle_type: :proof_address) + def rate_limiter + @rate_limiter ||= RateLimiter.new(user: current_user, rate_limit_type: :proof_address) end def redirect_to_next_step @@ -171,7 +171,7 @@ def async_state_done(async_state) ) if async_state.result[:success] - throttle.reset! + rate_limiter.reset! redirect_to_next_step and return end handle_proofing_failure diff --git a/app/controllers/idv/phone_errors_controller.rb b/app/controllers/idv/phone_errors_controller.rb index 58ae128cc96..fc5cc763c61 100644 --- a/app/controllers/idv/phone_errors_controller.rb +++ b/app/controllers/idv/phone_errors_controller.rb @@ -9,7 +9,7 @@ class PhoneErrorsController < ApplicationController before_action :ignore_form_step_wait_requests def warning - @remaining_attempts = throttle.remaining_count + @remaining_attempts = rate_limiter.remaining_count if idv_session.previous_phone_step_params @phone = idv_session.previous_phone_step_params[:phone] @@ -20,24 +20,24 @@ def warning end def timeout - @remaining_step_attempts = throttle.remaining_count + @remaining_step_attempts = rate_limiter.remaining_count track_event(type: :timeout) end def jobfail - @remaining_attempts = throttle.remaining_count + @remaining_attempts = rate_limiter.remaining_count track_event(type: :jobfail) end def failure - @expires_at = throttle.expires_at + @expires_at = rate_limiter.expires_at track_event(type: :failure) end private - def throttle - Throttle.new(user: idv_session.current_user, throttle_type: :proof_address) + def rate_limiter + RateLimiter.new(user: idv_session.current_user, rate_limit_type: :proof_address) end def confirm_idv_phone_step_needed diff --git a/app/controllers/idv/session_errors_controller.rb b/app/controllers/idv/session_errors_controller.rb index 1c21a82c1f2..69524de60a0 100644 --- a/app/controllers/idv/session_errors_controller.rb +++ b/app/controllers/idv/session_errors_controller.rb @@ -13,13 +13,13 @@ def exception end def warning - throttle = Throttle.new( + rate_limiter = RateLimiter.new( user: idv_session_user, - throttle_type: :idv_resolution, + rate_limit_type: :idv_resolution, ) - @remaining_attempts = throttle.remaining_count - log_event(based_on_throttle: throttle) + @remaining_attempts = rate_limiter.remaining_count + log_event(based_on_throttle: rate_limiter) end def state_id_warning @@ -27,34 +27,34 @@ def state_id_warning end def failure - throttle = Throttle.new( + rate_limiter = RateLimiter.new( user: idv_session_user, - throttle_type: :idv_resolution, + rate_limit_type: :idv_resolution, ) - @expires_at = throttle.expires_at + @expires_at = rate_limiter.expires_at @sp_name = decorated_session.sp_name - log_event(based_on_throttle: throttle) + log_event(based_on_throttle: rate_limiter) end def ssn_failure - throttle = nil + rate_limiter = nil if ssn_from_doc - throttle = Throttle.new( + rate_limiter = RateLimiter.new( target: Pii::Fingerprinter.fingerprint(ssn_from_doc), - throttle_type: :proof_ssn, + rate_limit_type: :proof_ssn, ) - @expires_at = throttle.expires_at + @expires_at = rate_limiter.expires_at end - log_event(based_on_throttle: throttle) + log_event(based_on_throttle: rate_limiter) render 'idv/session_errors/failure' end def throttled - throttle = Throttle.new(user: idv_session_user, throttle_type: :idv_doc_auth) - log_event(based_on_throttle: throttle) - @expires_at = throttle.expires_at + rate_limiter = RateLimiter.new(user: idv_session_user, rate_limit_type: :idv_doc_auth) + log_event(based_on_throttle: rate_limiter) + @expires_at = rate_limiter.expires_at end private diff --git a/app/controllers/idv/verify_info_controller.rb b/app/controllers/idv/verify_info_controller.rb index 32fffb54380..968737e7c90 100644 --- a/app/controllers/idv/verify_info_controller.rb +++ b/app/controllers/idv/verify_info_controller.rb @@ -28,7 +28,8 @@ def update # Don't allow the user to go back to document capture after verifying if flow_session['redo_document_capture'] flow_session.delete('redo_document_capture') - flow_session[:flow_path] ||= 'standard' + idv_session.flow_path ||= 'standard' + flow_session[:flow_path] ||= 'standard' # temp added for 50/50, remove in future deploy end redirect_to idv_verify_info_url diff --git a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb index acd791cc440..01d87c985d6 100644 --- a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb +++ b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb @@ -83,8 +83,7 @@ def confirm_webauthn_enabled def presenter_for_two_factor_authentication_method TwoFactorAuthCode::WebauthnAuthenticationPresenter.new( view: view_context, - data: { credential_ids: credential_ids, - user_opted_remember_device_cookie: user_opted_remember_device_cookie }, + data: { credentials:, user_opted_remember_device_cookie: }, service_provider: current_sp, remember_device_default: remember_device_default, platform_authenticator: params[:platform].to_s == 'true', @@ -96,8 +95,10 @@ def save_challenge_in_session user_session[:webauthn_challenge] = credential_creation_options.challenge.bytes.to_a end - def credential_ids - MfaContext.new(current_user).webauthn_configurations.map(&:credential_id).join(',') + def credentials + MfaContext.new(current_user).webauthn_configurations.map do |configuration| + { id: configuration.credential_id, transports: configuration.transports } + end.to_json end def analytics_properties diff --git a/app/controllers/users/backup_code_setup_controller.rb b/app/controllers/users/backup_code_setup_controller.rb index c75c18dcb19..542ef58c0d2 100644 --- a/app/controllers/users/backup_code_setup_controller.rb +++ b/app/controllers/users/backup_code_setup_controller.rb @@ -11,9 +11,7 @@ class BackupCodeSetupController < ApplicationController before_action :set_backup_code_setup_presenter before_action :apply_secure_headers_override before_action :authorize_backup_code_disable, only: [:delete] - before_action :confirm_recently_authenticated_2fa, except: [:reminder], if: -> do - IdentityConfig.store.reauthentication_for_second_factor_management_enabled - end + before_action :confirm_recently_authenticated_2fa, except: [:reminder] helper_method :in_multi_mfa_selection_flow? diff --git a/app/controllers/users/edit_phone_controller.rb b/app/controllers/users/edit_phone_controller.rb index 7b159e71251..bf4326dde62 100644 --- a/app/controllers/users/edit_phone_controller.rb +++ b/app/controllers/users/edit_phone_controller.rb @@ -6,7 +6,7 @@ class EditPhoneController < ApplicationController before_action :confirm_two_factor_authenticated before_action :confirm_user_can_edit_phone before_action :confirm_user_can_remove_phone, only: %i[destroy] - before_action :confirm_recently_authenticated + before_action :confirm_recently_authenticated_2fa def edit analytics.phone_change_viewed diff --git a/app/controllers/users/emails_controller.rb b/app/controllers/users/emails_controller.rb index 26fcf0d57a0..bb944540f01 100644 --- a/app/controllers/users/emails_controller.rb +++ b/app/controllers/users/emails_controller.rb @@ -6,7 +6,7 @@ class EmailsController < ApplicationController before_action :authorize_user_to_edit_email, except: %i[add show verify resend] before_action :check_max_emails_per_account, only: %i[show add] before_action :retain_confirmed_emails, only: %i[delete] - before_action :confirm_recently_authenticated + before_action :confirm_recently_authenticated_2fa def show @add_user_email_form = AddUserEmailForm.new diff --git a/app/controllers/users/passwords_controller.rb b/app/controllers/users/passwords_controller.rb index c69e7fcfcd7..f86299f3e41 100644 --- a/app/controllers/users/passwords_controller.rb +++ b/app/controllers/users/passwords_controller.rb @@ -4,7 +4,7 @@ class PasswordsController < ApplicationController before_action :confirm_two_factor_authenticated before_action :capture_password_if_pii_requested_but_locked - before_action :confirm_recently_authenticated + before_action :confirm_recently_authenticated_2fa def edit @update_user_password_form = UpdateUserPasswordForm.new(current_user) diff --git a/app/controllers/users/phones_controller.rb b/app/controllers/users/phones_controller.rb index 1383d4d8b77..d2813f92eec 100644 --- a/app/controllers/users/phones_controller.rb +++ b/app/controllers/users/phones_controller.rb @@ -9,7 +9,7 @@ class PhonesController < ApplicationController before_action :redirect_if_phone_vendor_outage before_action :check_max_phone_numbers_per_account, only: %i[add create] before_action :allow_csp_recaptcha_src, if: :recaptcha_enabled? - before_action :confirm_recently_authenticated + before_action :confirm_recently_authenticated_2fa def add user_session[:phone_id] = nil diff --git a/app/controllers/users/piv_cac_authentication_setup_controller.rb b/app/controllers/users/piv_cac_authentication_setup_controller.rb index e4ac9e625d4..5599e4b57cd 100644 --- a/app/controllers/users/piv_cac_authentication_setup_controller.rb +++ b/app/controllers/users/piv_cac_authentication_setup_controller.rb @@ -12,9 +12,7 @@ class PivCacAuthenticationSetupController < ApplicationController before_action :authorize_piv_cac_disable, only: :delete before_action :set_piv_cac_setup_csp_form_action_uris, only: :new before_action :cap_piv_cac_count, only: %i[new submit_new_piv_cac] - before_action :confirm_recently_authenticated_2fa, if: -> do - IdentityConfig.store.reauthentication_for_second_factor_management_enabled - end + before_action :confirm_recently_authenticated_2fa helper_method :in_multi_mfa_selection_flow? diff --git a/app/controllers/users/piv_cac_setup_controller.rb b/app/controllers/users/piv_cac_setup_controller.rb index 745359e1d9d..a644c7e6649 100644 --- a/app/controllers/users/piv_cac_setup_controller.rb +++ b/app/controllers/users/piv_cac_setup_controller.rb @@ -4,7 +4,7 @@ class PivCacSetupController < ApplicationController include ReauthenticationRequiredConcern before_action :confirm_two_factor_authenticated - before_action :confirm_recently_authenticated + before_action :confirm_recently_authenticated_2fa def delete; end diff --git a/app/controllers/users/piv_cac_setup_from_sign_in_controller.rb b/app/controllers/users/piv_cac_setup_from_sign_in_controller.rb index 4b5d943c6ef..25d91b27bed 100644 --- a/app/controllers/users/piv_cac_setup_from_sign_in_controller.rb +++ b/app/controllers/users/piv_cac_setup_from_sign_in_controller.rb @@ -6,9 +6,7 @@ class PivCacSetupFromSignInController < ApplicationController include ReauthenticationRequiredConcern before_action :confirm_two_factor_authenticated - before_action :confirm_recently_authenticated_2fa, if: -> do - IdentityConfig.store.reauthentication_for_second_factor_management_enabled - end + before_action :confirm_recently_authenticated_2fa before_action :apply_secure_headers_override, only: :success before_action :set_piv_cac_setup_csp_form_action_uris, only: :prompt diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index aec4b1dbada..0d7650e7fea 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -20,7 +20,6 @@ def new override_csp_for_google_analytics @ial = sp_session_ial - @browser_is_ie11 = browser_is_ie11? analytics.sign_in_page_visit( flash: flash[:alert], stored_location: session['user_return_to'], @@ -32,11 +31,11 @@ def create return process_locked_out_session if session_bad_password_count_max_exceeded? return process_locked_out_user if current_user && user_locked_out?(current_user) - throttle_password_failure = true + rate_limit_password_failure = true self.resource = warden.authenticate!(auth_options) handle_valid_authentication ensure - increment_session_bad_password_count if throttle_password_failure && !current_user + increment_session_bad_password_count if rate_limit_password_failure && !current_user track_authentication_attempt(auth_params[:email]) end @@ -116,10 +115,6 @@ def handle_valid_authentication redirect_to next_url_after_valid_authentication end - def browser_is_ie11? - BrowserCache.parse(request.user_agent).ie?(11) - end - def track_authentication_attempt(email) user = User.find_with_email(email) || AnonymousUser.new diff --git a/app/controllers/users/totp_setup_controller.rb b/app/controllers/users/totp_setup_controller.rb index 6ecc8a94545..f9325094da2 100644 --- a/app/controllers/users/totp_setup_controller.rb +++ b/app/controllers/users/totp_setup_controller.rb @@ -10,9 +10,7 @@ class TotpSetupController < ApplicationController before_action :set_totp_setup_presenter before_action :apply_secure_headers_override before_action :cap_auth_app_count, only: %i[new confirm] - before_action :confirm_recently_authenticated_2fa, if: -> do - IdentityConfig.store.reauthentication_for_second_factor_management_enabled - end + before_action :confirm_recently_authenticated_2fa helper_method :in_multi_mfa_selection_flow? diff --git a/app/controllers/users/two_factor_authentication_controller.rb b/app/controllers/users/two_factor_authentication_controller.rb index 765980cc1cd..59c112056e8 100644 --- a/app/controllers/users/two_factor_authentication_controller.rb +++ b/app/controllers/users/two_factor_authentication_controller.rb @@ -264,17 +264,17 @@ def exceeded_otp_send_limit? return otp_rate_limiter.lock_out_user if otp_rate_limiter.exceeded_otp_send_limit? end - def phone_confirmation_throttle - @phone_confirmation_throttle ||= Throttle.new( + def phone_confirmation_rate_limiter + @phone_confirmation_rate_limiter ||= RateLimiter.new( user: current_user, - throttle_type: :phone_confirmation, + rate_limit_type: :phone_confirmation, ) end def exceeded_phone_confirmation_limit? return false unless UserSessionContext.confirmation_context?(context) - phone_confirmation_throttle.increment! - phone_confirmation_throttle.throttled? + phone_confirmation_rate_limiter.increment! + phone_confirmation_rate_limiter.limited? end def send_user_otp(method) @@ -369,7 +369,7 @@ def handle_too_many_confirmation_sends 'errors.messages.phone_confirmation_throttled', timeout: distance_of_time_in_words( Time.zone.now, - [phone_confirmation_throttle.expires_at, Time.zone.now].compact.max, + [phone_confirmation_rate_limiter.expires_at, Time.zone.now].compact.max, except: :seconds, ), ) diff --git a/app/controllers/users/verify_personal_key_controller.rb b/app/controllers/users/verify_personal_key_controller.rb index 68d173af9e1..50f56f51e91 100644 --- a/app/controllers/users/verify_personal_key_controller.rb +++ b/app/controllers/users/verify_personal_key_controller.rb @@ -13,17 +13,17 @@ def new personal_key: '', ) - if throttle.throttled? - render_throttled + if rate_limiter.limited? + render_rate_limited else render :new end end def create - throttle.increment! - if throttle.throttled? - render_throttled + rate_limiter.increment! + if rate_limiter.limited? + render_rate_limited else result = personal_key_form.submit @@ -45,21 +45,21 @@ def create private - def throttle - @throttle ||= Throttle.new( + def rate_limiter + @rate_limiter ||= RateLimiter.new( user: current_user, - throttle_type: :verify_personal_key, + rate_limit_type: :verify_personal_key, ) end - def render_throttled + def render_rate_limited analytics.throttler_rate_limit_triggered( throttle_type: :verify_personal_key, ) irs_attempts_api_tracker.personal_key_reactivation_rate_limited - @expires_at = throttle.expires_at + @expires_at = rate_limiter.expires_at render :throttled end diff --git a/app/controllers/users/webauthn_setup_controller.rb b/app/controllers/users/webauthn_setup_controller.rb index e6ef10c4ac3..fb76235571b 100644 --- a/app/controllers/users/webauthn_setup_controller.rb +++ b/app/controllers/users/webauthn_setup_controller.rb @@ -9,9 +9,7 @@ class WebauthnSetupController < ApplicationController before_action :confirm_user_authenticated_for_2fa_setup before_action :apply_secure_headers_override before_action :set_webauthn_setup_presenter - before_action :confirm_recently_authenticated_2fa, if: -> do - IdentityConfig.store.reauthentication_for_second_factor_management_enabled - end + before_action :confirm_recently_authenticated_2fa helper_method :in_multi_mfa_selection_flow? @@ -196,7 +194,13 @@ def new_params end def confirm_params - params.permit(:attestation_object, :client_data_json, :name, :platform_authenticator) + params.permit( + :attestation_object, + :client_data_json, + :transports, + :name, + :platform_authenticator, + ) end def delete_params diff --git a/app/forms/idv/api_image_upload_form.rb b/app/forms/idv/api_image_upload_form.rb index 15683954d0e..968da03566a 100644 --- a/app/forms/idv/api_image_upload_form.rb +++ b/app/forms/idv/api_image_upload_form.rb @@ -8,7 +8,7 @@ class ApiImageUploadForm validates_presence_of :document_capture_session validate :validate_images - validate :throttle_if_rate_limited + validate :limit_if_rate_limited def initialize(params, service_provider:, analytics: nil, uuid_prefix: nil, irs_attempts_api_tracker: nil, store_encrypted_images: false) @@ -32,7 +32,7 @@ def submit if client_response.success? doc_pii_response = validate_pii_from_doc(client_response) - throttle.reset! + rate_limiter.reset! end end @@ -52,15 +52,15 @@ def submit attr_reader :params, :analytics, :service_provider, :form_response, :uuid_prefix, :irs_attempts_api_tracker - def increment_throttle! + def increment_rate_limiter! return unless document_capture_session - throttle.increment! + rate_limiter.increment! end def validate_form success = valid? - increment_throttle! - track_rate_limited if throttled? + increment_rate_limiter! + track_rate_limited if rate_limited? response = Idv::DocAuthFormResponse.new( success: success, @@ -130,11 +130,11 @@ def extra_attributes end def remaining_attempts - throttle.remaining_count if document_capture_session + rate_limiter.remaining_count if document_capture_session end def attempts - throttle.attempts if document_capture_session + rate_limiter.attempts if document_capture_session end def determine_response(form_response:, client_response:, doc_pii_response:) @@ -184,9 +184,9 @@ def validate_images end end - def throttle_if_rate_limited + def limit_if_rate_limited return unless document_capture_session - return unless throttled? + return unless rate_limited? errors.add(:limit, t('errors.doc_auth.throttled_heading'), type: :throttled) end @@ -306,15 +306,15 @@ def user_uuid document_capture_session&.user&.uuid end - def throttle - @throttle ||= Throttle.new( + def rate_limiter + @rate_limiter ||= RateLimiter.new( user: document_capture_session.user, - throttle_type: :idv_doc_auth, + rate_limit_type: :idv_doc_auth, ) end - def throttled? - throttle.throttled? if document_capture_session + def rate_limited? + rate_limiter.limited? if document_capture_session end def track_event(response) diff --git a/app/forms/register_user_email_form.rb b/app/forms/register_user_email_form.rb index a84a3f9a3a6..c5682d73e4e 100644 --- a/app/forms/register_user_email_form.rb +++ b/app/forms/register_user_email_form.rb @@ -15,7 +15,7 @@ def self.model_name end def initialize(analytics:, attempts_tracker:, password_reset_requested: false) - @throttled = false + @rate_limited = false @password_reset_requested = password_reset_requested @analytics = analytics @attempts_tracker = attempts_tracker @@ -110,16 +110,16 @@ def extra_analytics_attributes email_already_exists: email_taken?, user_id: user.uuid || existing_user.uuid, domain_name: email&.split('@')&.last, - throttled: @throttled, + throttled: @rate_limited, } end def send_sign_up_unconfirmed_email(request_id) - throttler = Throttle.new(user: existing_user, throttle_type: :reg_unconfirmed_email) - throttler.increment! - @throttled = throttler.throttled? + rate_limiter = RateLimiter.new(user: existing_user, rate_limit_type: :reg_unconfirmed_email) + rate_limiter.increment! + @rate_limited = rate_limiter.limited? - if @throttled + if @rate_limited @analytics.throttler_rate_limit_triggered( throttle_type: :reg_unconfirmed_email, ) @@ -132,11 +132,11 @@ def send_sign_up_unconfirmed_email(request_id) end def send_sign_up_confirmed_email - throttler = Throttle.new(user: existing_user, throttle_type: :reg_confirmed_email) - throttler.increment! - @throttled = throttler.throttled? + rate_limiter = RateLimiter.new(user: existing_user, rate_limit_type: :reg_confirmed_email) + rate_limiter.increment! + @rate_limited = rate_limiter.limited? - if @throttled + if @rate_limited @analytics.throttler_rate_limit_triggered( throttle_type: :reg_confirmed_email, ) diff --git a/app/forms/webauthn_setup_form.rb b/app/forms/webauthn_setup_form.rb index 2febf957a1f..a768962be15 100644 --- a/app/forms/webauthn_setup_form.rb +++ b/app/forms/webauthn_setup_form.rb @@ -43,7 +43,7 @@ def platform_authenticator? private - attr_reader :success + attr_reader :success, :transports, :invalid_transports attr_accessor :user, :challenge, :attestation_object, :client_data_json, :name, :platform_authenticator @@ -52,6 +52,9 @@ def consume_parameters(params) @client_data_json = params[:client_data_json] @name = params[:name] @platform_authenticator = (params[:platform_authenticator].to_s == 'true') + @transports, @invalid_transports = params[:transports]&.split(',')&.partition do |transport| + WebauthnConfiguration::VALID_TRANSPORTS.include?(transport) + end end def name_is_unique @@ -98,6 +101,7 @@ def create_webauthn_configuration credential_id: id, name: name, platform_authenticator: platform_authenticator, + transports: transports.presence, ) end @@ -116,6 +120,7 @@ def extra_analytics_attributes enabled_mfa_methods_count: mfa_user.enabled_mfa_methods_count, multi_factor_auth_method: auth_method, pii_like_keypaths: [[:mfa_method_counts, :phone]], - } + unknown_transports: invalid_transports.presence, + }.compact end end diff --git a/app/javascript/packages/webauthn/enroll-webauthn-device.spec.ts b/app/javascript/packages/webauthn/enroll-webauthn-device.spec.ts index 2d53f8d547f..8de2008c7ae 100644 --- a/app/javascript/packages/webauthn/enroll-webauthn-device.spec.ts +++ b/app/javascript/packages/webauthn/enroll-webauthn-device.spec.ts @@ -24,6 +24,7 @@ describe('enrollWebauthnDevice', () => { response: { attestationObject: Buffer.from('attest', 'utf-8'), clientDataJSON: Buffer.from('json', 'utf-8'), + getTransports: () => ['usb'], }, }), }, @@ -80,6 +81,7 @@ describe('enrollWebauthnDevice', () => { webauthnPublicKey: '123', attestationObject: btoa('attest'), clientDataJSON: btoa('json'), + transports: ['usb'], }); }); diff --git a/app/javascript/packages/webauthn/enroll-webauthn-device.ts b/app/javascript/packages/webauthn/enroll-webauthn-device.ts index 024f266b18c..4d4c19e197e 100644 --- a/app/javascript/packages/webauthn/enroll-webauthn-device.ts +++ b/app/javascript/packages/webauthn/enroll-webauthn-device.ts @@ -18,6 +18,8 @@ interface EnrollResult { attestationObject: string; clientDataJSON: string; + + transports: string[]; } async function enrollWebauthnDevice({ @@ -79,6 +81,7 @@ async function enrollWebauthnDevice({ webauthnPublicKey: credential.id, attestationObject: arrayBufferToBase64(response.attestationObject), clientDataJSON: arrayBufferToBase64(response.clientDataJSON), + transports: response.getTransports(), }; } diff --git a/app/javascript/packages/webauthn/index.ts b/app/javascript/packages/webauthn/index.ts index 203216c740c..85a1c01a9dc 100644 --- a/app/javascript/packages/webauthn/index.ts +++ b/app/javascript/packages/webauthn/index.ts @@ -3,3 +3,5 @@ export { default as enrollWebauthnDevice } from './enroll-webauthn-device'; export { default as extractCredentials } from './extract-credentials'; export { default as verifyWebauthnDevice } from './verify-webauthn-device'; export * from './converters'; + +export type { VerifyCredentialDescriptor } from './verify-webauthn-device'; diff --git a/app/javascript/packages/webauthn/verify-webauthn-device.spec.ts b/app/javascript/packages/webauthn/verify-webauthn-device.spec.ts index 341a57bdfec..a8cd451edf1 100644 --- a/app/javascript/packages/webauthn/verify-webauthn-device.spec.ts +++ b/app/javascript/packages/webauthn/verify-webauthn-device.spec.ts @@ -7,7 +7,10 @@ describe('verifyWebauthnDevice', () => { const defineProperty = useDefineProperty(); const userChallenge = '[1, 2, 3, 4, 5, 6, 7, 8]'; - const credentialIds = [btoa('credential123'), btoa('credential456')].join(','); + const credentials = [ + { id: btoa('credential123'), transports: ['usb'] as AuthenticatorTransport[] }, + { id: btoa('credential456'), transports: ['internal', 'hybrid'] as AuthenticatorTransport[] }, + ]; context('webauthn api resolves credential', () => { beforeEach(() => { @@ -35,10 +38,12 @@ describe('verifyWebauthnDevice', () => { { id: new TextEncoder().encode('credential123').buffer, type: 'public-key', + transports: ['usb'], }, { id: new TextEncoder().encode('credential456').buffer, type: 'public-key', + transports: ['internal', 'hybrid'], }, ], timeout: 800000, @@ -47,7 +52,7 @@ describe('verifyWebauthnDevice', () => { const result = await verifyWebauthnDevice({ userChallenge, - credentialIds, + credentials, }); expect(navigator.credentials.get).to.have.been.calledWith(expectedGetOptions); @@ -77,7 +82,7 @@ describe('verifyWebauthnDevice', () => { try { await verifyWebauthnDevice({ userChallenge, - credentialIds, + credentials, }); } catch (error) { expect(error).to.equal(error); diff --git a/app/javascript/packages/webauthn/verify-webauthn-device.ts b/app/javascript/packages/webauthn/verify-webauthn-device.ts index df18445814c..ee39c201621 100644 --- a/app/javascript/packages/webauthn/verify-webauthn-device.ts +++ b/app/javascript/packages/webauthn/verify-webauthn-device.ts @@ -1,10 +1,15 @@ -import { arrayBufferToBase64 } from './converters'; -import extractCredentials from './extract-credentials'; +import { base64ToArrayBuffer, arrayBufferToBase64 } from '@18f/identity-webauthn'; + +export interface VerifyCredentialDescriptor { + id: string; + + transports: null | AuthenticatorTransport[]; +} interface VerifyOptions { userChallenge: string; - credentialIds: string; + credentials: VerifyCredentialDescriptor[]; } interface VerifyResult { @@ -17,15 +22,23 @@ interface VerifyResult { signature: string; } +const mapVerifyCredential = ( + credential: VerifyCredentialDescriptor, +): PublicKeyCredentialDescriptor => ({ + type: 'public-key', + id: base64ToArrayBuffer(credential.id), + transports: credential.transports || undefined, +}); + async function verifyWebauthnDevice({ userChallenge, - credentialIds, + credentials, }: VerifyOptions): Promise { const credential = (await navigator.credentials.get({ publicKey: { challenge: new Uint8Array(JSON.parse(userChallenge)), rpId: window.location.hostname, - allowCredentials: extractCredentials(credentialIds.split(',').filter(Boolean)), + allowCredentials: credentials.map(mapVerifyCredential), timeout: 800000, }, })) as PublicKeyCredential; diff --git a/app/javascript/packs/webauthn-authenticate.ts b/app/javascript/packs/webauthn-authenticate.ts index 14c4784fe57..5675e9bc3af 100644 --- a/app/javascript/packs/webauthn-authenticate.ts +++ b/app/javascript/packs/webauthn-authenticate.ts @@ -1,4 +1,5 @@ import { isWebauthnSupported, verifyWebauthnDevice } from '@18f/identity-webauthn'; +import type { VerifyCredentialDescriptor } from '@18f/identity-webauthn'; function webauthn() { const webauthnInProgressContainer = document.getElementById('webauthn-auth-in-progress')!; @@ -15,6 +16,10 @@ function webauthn() { const spinner = document.getElementById('spinner')!; spinner.classList.remove('display-none'); + const credentials: VerifyCredentialDescriptor[] = JSON.parse( + (document.getElementById('credentials') as HTMLInputElement).value, + ); + if ( !isWebauthnSupported() || (webauthnPlatformRequested && !isPlatformAvailable && !multipleFactorsEnabled) @@ -25,7 +30,7 @@ function webauthn() { // if platform auth is not supported on device, we should take user to the error screen if theres no additional methods. verifyWebauthnDevice({ userChallenge: (document.getElementById('user_challenge') as HTMLInputElement).value, - credentialIds: (document.getElementById('credential_ids') as HTMLInputElement).value, + credentials, }) .then((result) => { (document.getElementById('credential_id') as HTMLInputElement).value = result.credentialId; diff --git a/app/javascript/packs/webauthn-setup.ts b/app/javascript/packs/webauthn-setup.ts index 2fe2a52119d..42cadf772d8 100644 --- a/app/javascript/packs/webauthn-setup.ts +++ b/app/javascript/packs/webauthn-setup.ts @@ -68,6 +68,8 @@ function webauthn() { result.attestationObject; (document.getElementById('client_data_json') as HTMLInputElement).value = result.clientDataJSON; + (document.getElementById('transports') as HTMLInputElement).value = + result.transports.join(); (document.getElementById('webauthn_form') as HTMLFormElement).submit(); }) .catch((err) => reloadWithError(err.name, { force: true })); diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 1844917f7f1..d4c618b6b6c 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -376,6 +376,12 @@ def account_rejected end end + def suspended_reset_password + with_user_locale(user) do + mail(to: email_address.email, subject: t('user_mailer.suspended_reset_password.subject')) + end + end + private def email_should_receive_nonessential_notifications?(email) diff --git a/app/models/webauthn_configuration.rb b/app/models/webauthn_configuration.rb index 8c866349110..772e1c5d3a6 100644 --- a/app/models/webauthn_configuration.rb +++ b/app/models/webauthn_configuration.rb @@ -3,6 +3,17 @@ class WebauthnConfiguration < ApplicationRecord validates :name, presence: true validates :credential_id, presence: true validates :credential_public_key, presence: true + validate :valid_transports + + # https://w3c.github.io/webauthn/#enum-transport + VALID_TRANSPORTS = %w[ + usb + nfc + ble + smart-card + hybrid + internal + ].to_set.freeze def self.roaming_authenticators self.where(platform_authenticator: [nil, false]) @@ -39,4 +50,11 @@ def self.selection_presenters(set) [] end end + + private + + def valid_transports + return if transports.blank? || (transports - VALID_TRANSPORTS.to_a).blank? + errors.add(:transports, I18n.t('errors.general'), type: :invalid_transports) + end end diff --git a/app/presenters/two_factor_auth_code/webauthn_authentication_presenter.rb b/app/presenters/two_factor_auth_code/webauthn_authentication_presenter.rb index 43f214a69b7..cbb72891f3c 100644 --- a/app/presenters/two_factor_auth_code/webauthn_authentication_presenter.rb +++ b/app/presenters/two_factor_auth_code/webauthn_authentication_presenter.rb @@ -3,7 +3,7 @@ module TwoFactorAuthCode class WebauthnAuthenticationPresenter < TwoFactorAuthCode::GenericDeliveryPresenter include ActionView::Helpers::TranslationHelper - attr_reader :credential_ids, :user_opted_remember_device_cookie + attr_reader :credentials, :user_opted_remember_device_cookie def initialize(data:, view:, service_provider:, remember_device_default: true, platform_authenticator: false) diff --git a/app/services/idv/phone_step.rb b/app/services/idv/phone_step.rb index 2265d1c8d19..caf6f1fc4c9 100644 --- a/app/services/idv/phone_step.rb +++ b/app/services/idv/phone_step.rb @@ -8,8 +8,8 @@ def initialize(idv_session:, trace_id:, analytics:, attempts_tracker:) end def submit(step_params) - return throttled_result if throttle.throttled? - throttle.increment! + return rate_limited_result if rate_limiter.limited? + rate_limiter.increment! self.step_params = step_params idv_session.previous_phone_step_params = step_params.slice( @@ -20,7 +20,7 @@ def submit(step_params) end def failure_reason - return :fail if throttle.throttled? + return :fail if rate_limiter.limited? return :no_idv_result if idv_result.nil? return :timeout if idv_result[:timed_out] return :jobfail if idv_result[:exception].present? @@ -108,11 +108,14 @@ def otp_delivery_preference preference.to_sym end - def throttle - @throttle ||= Throttle.new(user: idv_session.current_user, throttle_type: :proof_address) + def rate_limiter + @rate_limiter ||= RateLimiter.new( + user: idv_session.current_user, + rate_limit_type: :proof_address, + ) end - def throttled_result + def rate_limited_result @attempts_tracker.idv_phone_otp_sent_rate_limited @analytics.throttler_rate_limit_triggered(throttle_type: :proof_address, step_name: :phone) FormResponse.new(success: false) diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb index 13511246797..48b12bfbb5d 100644 --- a/app/services/idv/session.rb +++ b/app/services/idv/session.rb @@ -3,22 +3,23 @@ class Session VALID_SESSION_ATTRIBUTES = %i[ address_verification_mechanism applicant + flow_path go_back_path - verify_info_step_document_capture_session_uuid + gpo_code_verified idv_consent_given idv_phone_step_document_capture_session_uuid - vendor_phone_confirmation - user_phone_confirmation - gpo_code_verified + personal_key phone_for_mobile_flow pii previous_phone_step_params profile_confirmation profile_id profile_step_params - personal_key resolution_successful threatmetrix_review_status + user_phone_confirmation + vendor_phone_confirmation + verify_info_step_document_capture_session_uuid welcome_visited ].freeze diff --git a/app/services/idv/steps/doc_auth_base_step.rb b/app/services/idv/steps/doc_auth_base_step.rb index 8d244a258d1..05563836e3a 100644 --- a/app/services/idv/steps/doc_auth_base_step.rb +++ b/app/services/idv/steps/doc_auth_base_step.rb @@ -53,19 +53,19 @@ def hybrid_flow_mobile? user_id_from_token.present? end - def throttled_response + def rate_limited_response @flow.analytics.throttler_rate_limit_triggered( throttle_type: :idv_doc_auth, ) @flow.irs_attempts_api_tracker.idv_document_upload_rate_limited - redirect_to throttled_url + redirect_to rate_limited_url DocAuth::Response.new( success: false, errors: { limit: I18n.t('errors.doc_auth.throttled_heading') }, ) end - def throttled_url + def rate_limited_url idv_session_errors_throttled_url end diff --git a/app/services/marketing_site.rb b/app/services/marketing_site.rb index 5d3874869bf..cfa6796b9ce 100644 --- a/app/services/marketing_site.rb +++ b/app/services/marketing_site.rb @@ -6,6 +6,7 @@ class MarketingSite HELP_CENTER_ARTICLES = %w[ authentication-methods/which-authentication-method-should-i-use creating-an-account/authentication-application + manage-your-account/personal-key signing-in/what-is-a-hardware-security-key verify-your-identity/accepted-state-issued-identification verify-your-identity/how-to-add-images-of-your-state-issued-id diff --git a/app/services/otp_rate_limiter.rb b/app/services/otp_rate_limiter.rb index b0aa75d31d7..192536da69f 100644 --- a/app/services/otp_rate_limiter.rb +++ b/app/services/otp_rate_limiter.rb @@ -15,15 +15,15 @@ def exceeded_otp_send_limit? end def max_requests_reached? - throttle.throttled? + rate_limiter.limited? end def rate_limit_period_expired? - throttle.expired? + rate_limiter.expired? end def reset_count_and_otp_last_sent_at - throttle.reset! + rate_limiter.reset! end def lock_out_user @@ -31,15 +31,15 @@ def lock_out_user end def increment - throttle.increment! + rate_limiter.increment! end def otp_last_sent_at - throttle.attempted_at + rate_limiter.attempted_at end - def throttle - @throttle ||= Throttle.new(throttle_type: :phone_otp, target: throttle_key) + def rate_limiter + @rate_limiter ||= RateLimiter.new(rate_limit_type: :phone_otp, target: rate_limit_key) end private @@ -58,7 +58,7 @@ def phone_fingerprint @phone_fingerprint ||= Pii::Fingerprinter.fingerprint(PhoneFormatter.format(phone)) end - def throttle_key + def rate_limit_key "#{phone_fingerprint}:#{phone_confirmed}" end end diff --git a/app/services/throttle.rb b/app/services/rate_limiter.rb similarity index 71% rename from app/services/throttle.rb rename to app/services/rate_limiter.rb index 577e7900244..6e7a0f8b656 100644 --- a/app/services/throttle.rb +++ b/app/services/rate_limiter.rb @@ -1,24 +1,24 @@ # This class is similar to RedisRateLimiter, but differs in that -# the throttle period begins once the maximum number of allowed +# the rate limit period begins once the maximum number of allowed # attempts has been reached. -class Throttle - attr_reader :throttle_type +class RateLimiter + attr_reader :rate_limit_type - def initialize(throttle_type:, user: nil, target: nil) - @throttle_type = throttle_type + def initialize(rate_limit_type:, user: nil, target: nil) + @rate_limit_type = rate_limit_type @user = user @target = target - unless Throttle.throttle_config.key?(throttle_type) + unless RateLimiter.rate_limit_config.key?(rate_limit_type) raise ArgumentError, - 'throttle_type is not valid' + 'rate_limit_type is not valid' end if @user.blank? && @target.blank? - raise ArgumentError, 'Throttle must have a user or a target, but neither were provided' + raise ArgumentError, 'RateLimiter must have a user or a target, but neither were provided' end if @user.present? && @target.present? - raise ArgumentError, 'Throttle must have a user or a target, but both were provided' + raise ArgumentError, 'RateLimiter must have a user or a target, but both were provided' end if target && !target.is_a?(String) @@ -35,7 +35,7 @@ def attempts @redis_attempts.to_i end - def throttled? + def limited? !expired? && maxed? end @@ -49,13 +49,13 @@ def attempted_at def expires_at return Time.zone.now if attempted_at.blank? - attempted_at + Throttle.attempt_window_in_minutes(throttle_type).minutes + attempted_at + RateLimiter.attempt_window_in_minutes(rate_limit_type).minutes end def remaining_count - return 0 if throttled? + return 0 if limited? - Throttle.max_attempts(throttle_type) - attempts + RateLimiter.max_attempts(rate_limit_type) - attempts end def expired? @@ -63,11 +63,11 @@ def expired? end def maxed? - attempts && attempts >= Throttle.max_attempts(throttle_type) + attempts && attempts >= RateLimiter.max_attempts(rate_limit_type) end def increment! - return if throttled? + return if limited? value = nil REDIS_THROTTLE_POOL.with do |client| @@ -75,7 +75,7 @@ def increment! multi.incr(key) multi.expire( key, - Throttle.attempt_window_in_minutes(throttle_type).minutes.seconds.to_i, + RateLimiter.attempt_window_in_minutes(rate_limit_type).minutes.seconds.to_i, ) end end @@ -86,7 +86,7 @@ def increment! attempts end - # Retrieve the current state of the throttle from Redis + # Retrieve the current state of the rate limit from Redis # We use EXPIRETIME to calculate when the action was last attempted. def fetch_state! value = nil @@ -105,7 +105,7 @@ def fetch_state! else @redis_attempted_at = ActiveSupport::TimeZone['UTC'].at(expiretime).in_time_zone(Time.zone) - - Throttle.attempt_window_in_minutes(throttle_type).minutes + RateLimiter.attempt_window_in_minutes(rate_limit_type).minutes end self @@ -120,15 +120,16 @@ def reset! @redis_attempted_at = nil end - def increment_to_throttled! - value = Throttle.max_attempts(throttle_type) + def increment_to_limited! + value = RateLimiter.max_attempts(rate_limit_type) now = Time.zone.now REDIS_THROTTLE_POOL.with do |client| client.set( key, value, - exat: now.to_i + Throttle.attempt_window_in_minutes(throttle_type).minutes.seconds.to_i, + exat: now.to_i + + RateLimiter.attempt_window_in_minutes(rate_limit_type).minutes.seconds.to_i, ) end @@ -138,31 +139,32 @@ def increment_to_throttled! attempts end + # still uses throttle terminology because of persisted data in redis def key if @user - "throttle:throttle:#{@user.id}:#{throttle_type}" + "throttle:throttle:#{@user.id}:#{rate_limit_type}" else - "throttle:throttle:#{@target}:#{throttle_type}" + "throttle:throttle:#{@target}:#{rate_limit_type}" end end - def self.attempt_window_in_minutes(throttle_type) - throttle_config.dig(throttle_type, :attempt_window) + def self.attempt_window_in_minutes(rate_limit_type) + rate_limit_config.dig(rate_limit_type, :attempt_window) end - def self.max_attempts(throttle_type) - throttle_config.dig(throttle_type, :max_attempts) + def self.max_attempts(rate_limit_type) + rate_limit_config.dig(rate_limit_type, :max_attempts) end - def self.throttle_config + def self.rate_limit_config if Rails.env.production? - CACHED_THROTTLE_CONFIG + CACHED_RATE_LIMIT_CONFIG else - load_throttle_config + load_rate_limit_config end end - def self.load_throttle_config + def self.load_rate_limit_config { idv_doc_auth: { max_attempts: IdentityConfig.store.doc_auth_max_attempts, @@ -215,5 +217,5 @@ def self.load_throttle_config }.with_indifferent_access end - CACHED_THROTTLE_CONFIG = self.load_throttle_config.with_indifferent_access.freeze + CACHED_RATE_LIMIT_CONFIG = self.load_rate_limit_config.with_indifferent_access.freeze end diff --git a/app/services/request_password_reset.rb b/app/services/request_password_reset.rb index aa2b588d369..15f82819a04 100644 --- a/app/services/request_password_reset.rb +++ b/app/services/request_password_reset.rb @@ -21,11 +21,16 @@ def perform private def send_reset_password_instructions - throttle = Throttle.new(user: user, throttle_type: :reset_password_email) - throttle.increment! - if throttle.throttled? + rate_limiter = RateLimiter.new(user: user, rate_limit_type: :reset_password_email) + rate_limiter.increment! + if rate_limiter.limited? analytics.throttler_rate_limit_triggered(throttle_type: :reset_password_email) irs_attempts_api_tracker.forgot_password_email_rate_limited(email: email) + elsif user.suspended? + UserMailer.with( + user: user, + email_address: email_address_record, + ).suspended_reset_password.deliver_now_or_later else token = user.set_reset_password_token UserMailer.with(user: user, email_address: email_address_record).reset_password_instructions( diff --git a/app/views/accounts/_pii.html.erb b/app/views/accounts/_pii.html.erb index ec2b963ecaf..e8d867ea91b 100644 --- a/app/views/accounts/_pii.html.erb +++ b/app/views/accounts/_pii.html.erb @@ -4,17 +4,13 @@
<%= t('account.re_verify.banner') %> - <% if use_reauthentication_route %> - <%= render ButtonComponent.new( - action: ->(**tag_options, &block) do - button_to(account_reauthentication_path, **tag_options, &block) - end, - method: :post, - class: 'usa-button usa-button--unstyled', - ).with_content(t('account.re_verify.footer')) %> - <% else %> - <%= link_to(t('account.re_verify.footer'), user_password_confirm_path) %> - <% end %> + <%= render ButtonComponent.new( + action: ->(**tag_options, &block) do + button_to(account_reauthentication_path, **tag_options, &block) + end, + method: :post, + class: 'usa-button usa-button--unstyled', + ).with_content(t('account.re_verify.footer')) %>
diff --git a/app/views/accounts/show.html.erb b/app/views/accounts/show.html.erb index db3e36d8c58..77e94d34a4b 100644 --- a/app/views/accounts/show.html.erb +++ b/app/views/accounts/show.html.erb @@ -96,6 +96,5 @@ <% if @presenter.show_pii_partial? %> <%= render 'accounts/pii', pii: @presenter.pii, - locked_for_session: @presenter.locked_for_session, - use_reauthentication_route: @use_reauthentication_route %> + locked_for_session: @presenter.locked_for_session %> <% end %> diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index 6492c7f8c6f..051a3b8fc2d 100644 --- a/app/views/devise/sessions/new.html.erb +++ b/app/views/devise/sessions/new.html.erb @@ -1,11 +1,4 @@ <% title t('titles.visitors.index') %> -<% if @browser_is_ie11 %> - <%= render AlertComponent.new( - type: :warning, - class: 'margin-bottom-2', - message: t('account.login.ie_not_supported', date: I18n.l(IdentityConfig.store.ie11_support_end_date, format: :event_date)), - ) %> -<% end %> <%= render 'shared/maintenance_window_alert' %> <% if decorated_session.sp_name %> diff --git a/app/views/shared/_personal_key.html.erb b/app/views/shared/_personal_key.html.erb index a78a49432ca..ef347ccab5d 100644 --- a/app/views/shared/_personal_key.html.erb +++ b/app/views/shared/_personal_key.html.erb @@ -7,13 +7,6 @@ %> -<%= render AccordionComponent.new do |c| %> - <% c.with_header { t('forms.personal_key_partial.explanation.header') } %> - <% t('forms.personal_key_partial.explanation.text').each do |paragraph| %> -

<%= paragraph %>

- <% end %> -<% end %> - <%= simple_form_for('', url: update_path) do |f| %>

@@ -21,15 +14,19 @@

-

+

<%= t('forms.personal_key_partial.acknowledgement.text') %>

- +

+ <%= new_tab_link_to( + t('forms.personal_key_partial.acknowledgement.help_link_text'), + MarketingSite.help_center_article_url( + category: 'manage-your-account', + article: 'personal-key', + ), + ) %> +

<%= render ClickObserverComponent.new(event_name: 'IdV: personal key acknowledgment toggled') do %> <%= render ValidatedFieldComponent.new( diff --git a/app/views/two_factor_authentication/webauthn_verification/show.html.erb b/app/views/two_factor_authentication/webauthn_verification/show.html.erb index 32c901f78c4..3833d889ba2 100644 --- a/app/views/two_factor_authentication/webauthn_verification/show.html.erb +++ b/app/views/two_factor_authentication/webauthn_verification/show.html.erb @@ -16,7 +16,7 @@ }, ) do |f| %> <%= hidden_field_tag :user_challenge, user_session[:webauthn_challenge].to_json, id: 'user_challenge' %> - <%= hidden_field_tag :credential_ids, @presenter.credential_ids, id: 'credential_ids' %> + <%= hidden_field_tag :credentials, @presenter.credentials, id: 'credentials' %> <%= hidden_field_tag :credential_id, '', id: 'credential_id' %> <%= hidden_field_tag :authenticator_data, '', id: 'authenticator_data' %> <%= hidden_field_tag :signature, '', id: 'signature' %> diff --git a/app/views/user_mailer/suspended_reset_password.html.erb b/app/views/user_mailer/suspended_reset_password.html.erb new file mode 100644 index 00000000000..aa33ddbe304 --- /dev/null +++ b/app/views/user_mailer/suspended_reset_password.html.erb @@ -0,0 +1,13 @@ + + + + + + +
+   +
+ +

+<%= t('user_mailer.suspended_reset_password.message', support_code: IdentityConfig.store.account_suspended_support_code, contact_number: IdentityConfig.store.idv_contact_phone_number) %> +

diff --git a/app/views/users/webauthn_setup/new.html.erb b/app/views/users/webauthn_setup/new.html.erb index 716ca4a870b..f04e0a4739b 100644 --- a/app/views/users/webauthn_setup/new.html.erb +++ b/app/views/users/webauthn_setup/new.html.erb @@ -29,6 +29,7 @@ <%= hidden_field_tag :webauthn_public_key, '', id: 'webauthn_public_key' %> <%= hidden_field_tag :attestation_object, '', id: 'attestation_object' %> <%= hidden_field_tag :client_data_json, '', id: 'client_data_json' %> + <%= hidden_field_tag :transports, '', id: 'transports' %> <%= hidden_field_tag :platform_authenticator, @platform_authenticator, id: 'platform_authenticator' %> <%= render ValidatedFieldComponent.new( form: f, diff --git a/config/application.yml.default b/config/application.yml.default index 6935ba2f47e..cd8b1ef3ef1 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -24,6 +24,7 @@ aamva_verification_url: https://example.org:12345/verification/url all_redirect_uris_cache_duration_minutes: 2 account_reset_token_valid_for_days: 1 account_reset_wait_period_days: 1 +account_suspended_support_code: EFGHI acuant_maintenance_window_start: acuant_maintenance_window_finish: acuant_assure_id_password: '' @@ -132,7 +133,6 @@ idv_send_link_attempt_window_in_minutes: 10 idv_send_link_max_attempts: 5 idv_tmx_test_csp_disabled_emails: '[]' idv_tmx_test_js_disabled_emails: '[]' -ie11_support_end_date: '2022-12-31' idv_sp_required: false in_person_capture_secondary_id_enabled: false in_person_email_reminder_early_benchmark_in_days: 11 @@ -148,8 +148,10 @@ in_person_enrollments_ready_job_max_number_of_messages: 10 in_person_enrollments_ready_job_visibility_timeout_seconds: 30 in_person_enrollments_ready_job_wait_time_seconds: 20 in_person_results_delay_in_hours: 1 +in_person_ssn_info_controller_enabled: false in_person_completion_survey_url: 'https://login.gov' in_person_usps_outage_message_enabled: false +in_person_send_proofing_notifications_enabled: false in_person_stop_expiring_enrollments: false include_slo_in_saml_metadata: false key_pair_generation_percent: 0 @@ -251,7 +253,6 @@ rack_mini_profiler: false rack_timeout_service_timeout_seconds: 15 rails_mailer_previews_enabled: false reauthn_window: 120 -reauthentication_for_second_factor_management_enabled: true recaptcha_enterprise_api_key: '' recaptcha_enterprise_project_id: '' recaptcha_site_key_v2: '' @@ -328,7 +329,6 @@ verify_personal_key_max_attempts: 5 version_headers_enabled: false use_dashboard_service_providers: false use_kms: false -use_reauthentication_route: true usps_confirmation_max_days: 10 usps_ipp_password: '' usps_ipp_client_id: '' @@ -463,7 +463,6 @@ production: phone_recaptcha_mock_validator: false piv_cac_verify_token_secret: session_encryptor_alert_enabled: true - reauthentication_for_second_factor_management_enabled: false recurring_jobs_disabled_names: "[]" redis_throttle_url: redis://redis.login.gov.internal:6379/1 redis_url: redis://redis.login.gov.internal:6379 @@ -480,7 +479,6 @@ production: skip_encryption_allowed_list: '["urn:gov:gsa:SAML:2.0.profiles:sp:sso:dev", "urn:gov:gsa:SAML:2.0.profiles:sp:sso:int"]' state_tracking_enabled: false telephony_adapter: pinpoint - use_reauthentication_route: false use_kms: true usps_confirmation_max_days: 30 usps_upload_sftp_directory: '' diff --git a/config/locales/account/en.yml b/config/locales/account/en.yml index e88d7e934a4..9b7313a98cb 100644 --- a/config/locales/account/en.yml +++ b/config/locales/account/en.yml @@ -71,7 +71,6 @@ en: delete_account: Delete regenerate_personal_key: Reset login: - ie_not_supported: Internet Explorer 11 is no longer supported as of %{date} 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 0862196622e..aab4476a348 100644 --- a/config/locales/account/es.yml +++ b/config/locales/account/es.yml @@ -72,7 +72,6 @@ es: delete_account: Eliminar regenerate_personal_key: Restablecer login: - ie_not_supported: Internet Explorer 11 dejó de ser compatible a partir del %{date} 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 9143b4fd973..4c23df35007 100644 --- a/config/locales/account/fr.yml +++ b/config/locales/account/fr.yml @@ -77,7 +77,6 @@ fr: delete_account: Effacer regenerate_personal_key: Réinitialiser login: - ie_not_supported: Internet Explorer 11 n’est plus pris en charge à partir du %{date} 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/forms/en.yml b/config/locales/forms/en.yml index 7f2a33b8a20..003bc399741 100644 --- a/config/locales/forms/en.yml +++ b/config/locales/forms/en.yml @@ -70,24 +70,15 @@ en: confirmation_label: Personal key download: Download (text file) instructions: Please confirm you have a copy of your personal key by entering it below. - required_checkbox: Please check this box to continue. + required_checkbox: I saved my personal key in a safe place. title: Enter your personal key personal_key_partial: acknowledgement: - bullets: - - You’ll lose access to your account - - You’ll need to verify your identity again header: You need your personal key if you forget your password. Keep it safe and don’t share it with anyone. - text: 'If you reset your password without your personal key:' - explanation: - header: What is a personal key? - text: - - A personal key “locks” your personal information with your account. - It’s the only way to unlock your information if you lose or forget - your password. - - When you reset your password, Login.gov will ask for your personal - key to make sure you are you - not someone pretending to be you. + help_link_text: Learn more about the personal key + text: If you reset your password without your personal key, you’ll need to + verify your identity again. header: Save your personal key phone: buttons: diff --git a/config/locales/forms/es.yml b/config/locales/forms/es.yml index 81ddbb2d8ee..5b1bfd4b9c2 100644 --- a/config/locales/forms/es.yml +++ b/config/locales/forms/es.yml @@ -75,25 +75,15 @@ es: download: Descargar (archivo de texto) instructions: Confirme que tiene una copia de su clave personal ingresándola a continuación. - required_checkbox: Marque esta casilla para continuar. + required_checkbox: He guardado mi clave personal en un lugar seguro. title: Ingrese su clave personal personal_key_partial: acknowledgement: - bullets: - - Perderás el acceso a tu cuenta - - Tendrás que verificar tu identidad nuevamente header: Necesitarás tu clave personal si olvidas tu contraseña. Mantenla en un lugar seguro y no la compartas con nadie. - text: 'Si restableces tu contraseña sin tu clave personal:' - explanation: - header: '¿Qué es una clave personal?' - text: - - Una clave personal “bloquea” la información personal de tu cuenta. - Es la única manera de desbloquear tu información si pierdes u - olvidas tu contraseña. - - Si restableces tu contraseña, Login.gov te pedirá tu clave personal - para asegurarse de que se trata de ti, y no de alguien que quiere - hacerse pasar por ti. + help_link_text: Más información sobre la clave personal + text: 'Si restableces tu contraseña sin tu clave personal, tendrás que volver a + verificar tu identidad.' header: Guarda tu clave personal phone: buttons: diff --git a/config/locales/forms/fr.yml b/config/locales/forms/fr.yml index 7d970224aae..32c9c2aa17a 100644 --- a/config/locales/forms/fr.yml +++ b/config/locales/forms/fr.yml @@ -77,25 +77,15 @@ fr: download: Télécharger (fichier texte) instructions: Veuillez confirmer que vous avez une copie de votre clé personnelle en l’entrant ci-dessous. - required_checkbox: Veuillez cocher cette case pour continuer. + required_checkbox: J’ai conservé ma clé personnelle en lieu sûr. title: Entrez votre clé personnelle personal_key_partial: acknowledgement: - bullets: - - Vous perdrez l’accès à votre compte - - Vous allez devoir vérifier à nouveau votre identité header: En cas d’oubli de votre mot de passe, vous aurez besoin de votre clé personnelle. Gardez-la en sécurité et ne la partagez avec personne. - text: 'Si vous réinitialisez votre mot de passe sans votre clé personnelle:' - explanation: - header: Qu’est-ce qu’une clé personnelle? - text: - - Une clé personnelle « verrouille » vos informations personnelles - avec votre compte. C’est le seul moyen de déverrouiller vos - informations si vous perdez ou oubliez votre mot de passe. - - Lors de la réinitialisation de votre mot de passe, Login.gov vous - demandera votre clé personnelle pour s’assurer que vous êtes bien - vous, et non quelqu’un qui se fait passer pour vous. + help_link_text: En savoir plus sur la clé personnelle + text: Si vous réinitialisez votre mot de passe sans votre clé personnelle, vous + devrez à nouveau vérifier votre identité. header: Sauvegardez votre clé personnelle phone: buttons: diff --git a/config/locales/user_mailer/en.yml b/config/locales/user_mailer/en.yml index 47455638ed7..36c6dbde09a 100644 --- a/config/locales/user_mailer/en.yml +++ b/config/locales/user_mailer/en.yml @@ -284,3 +284,8 @@ en: in with this email address, you can ignore this message. link_text: Go to %{app_name} reset_password_html: If you can’t remember your password, go to %{app_name} to reset it. + suspended_reset_password: + message: There was an issue resetting your password. Please give our contact + center a call at %{contact_number} and provide this code - + %{support_code}. + subject: We couldn’t reset your password diff --git a/config/locales/user_mailer/es.yml b/config/locales/user_mailer/es.yml index b6b8b12f536..dfe89bf2622 100644 --- a/config/locales/user_mailer/es.yml +++ b/config/locales/user_mailer/es.yml @@ -304,3 +304,8 @@ es: una sesión con este email, puede ignorar este mensaje. link_text: Ir a %{app_name} reset_password_html: Si no recuerda su contraseña, vaya a %{app_name} para restablecerla. + suspended_reset_password: + message: Se produjo un problema al restablecer su contraseña. Llame a nuestro + centro de atención al número %{contact_number} y proporcione este código + - %{support_code}. + subject: No pudimos restablecer su contraseña diff --git a/config/locales/user_mailer/fr.yml b/config/locales/user_mailer/fr.yml index 46e5293f5ee..c4575e15f2a 100644 --- a/config/locales/user_mailer/fr.yml +++ b/config/locales/user_mailer/fr.yml @@ -316,3 +316,8 @@ fr: link_text: Allez à %{app_name} reset_password_html: Si vous ne vous souvenez plus de votre mot de passe, allez à %{app_name} pour le réinitialiser. + suspended_reset_password: + message: La réinitialisation de votre mot de passe a posé un problème. Veuillez + appeler notre centre d’appels au %{contact_number} et fournir ce code - + %{support_code}. + subject: Nous n’avons pas pu réinitialiser votre mot de passe diff --git a/db/primary_migrate/20230622125954_add_transports_to_webauthn_configurations.rb b/db/primary_migrate/20230622125954_add_transports_to_webauthn_configurations.rb new file mode 100644 index 00000000000..4621accc1a5 --- /dev/null +++ b/db/primary_migrate/20230622125954_add_transports_to_webauthn_configurations.rb @@ -0,0 +1,7 @@ +class AddTransportsToWebauthnConfigurations < ActiveRecord::Migration[7.0] + disable_ddl_transaction! + + def change + add_column :webauthn_configurations, :transports, :string, array: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 80c5901bf36..614eb799d1a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -623,6 +623,7 @@ t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.boolean "platform_authenticator" + t.string "transports", array: true t.index ["user_id"], name: "index_webauthn_configurations_on_user_id" end diff --git a/lib/feature_management.rb b/lib/feature_management.rb index a6505c81713..77e010139db 100644 --- a/lib/feature_management.rb +++ b/lib/feature_management.rb @@ -74,10 +74,6 @@ def self.show_no_pii_banner? Identity::Hostdata.in_datacenter? && Identity::Hostdata.domain != 'login.gov' end - def self.use_reauthentication_route? - IdentityConfig.store.use_reauthentication_route - end - def self.enable_saml_cert_rotation? IdentityConfig.store.saml_secret_rotation_enabled end diff --git a/lib/identity_config.rb b/lib/identity_config.rb index aa7da3762f8..e00e5fa1284 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -97,6 +97,7 @@ def self.build_store(config_map) config.add(:all_redirect_uris_cache_duration_minutes, type: :integer) config.add(:account_reset_token_valid_for_days, type: :integer) config.add(:account_reset_wait_period_days, type: :integer) + config.add(:account_suspended_support_code, type: :string) config.add(:acuant_maintenance_window_start, type: :timestamp, allow_nil: true) config.add(:acuant_maintenance_window_finish, type: :timestamp, allow_nil: true) config.add(:acuant_assure_id_password) @@ -224,7 +225,6 @@ def self.build_store(config_map) config.add(:idv_sp_required, type: :boolean) config.add(:idv_tmx_test_csp_disabled_emails, type: :json) config.add(:idv_tmx_test_js_disabled_emails, type: :json) - config.add(:ie11_support_end_date, type: :timestamp) config.add(:in_person_capture_secondary_id_enabled, type: :boolean) config.add(:in_person_email_reminder_early_benchmark_in_days, type: :integer) config.add(:in_person_email_reminder_final_benchmark_in_days, type: :integer) @@ -239,8 +239,10 @@ def self.build_store(config_map) config.add(:in_person_enrollments_ready_job_visibility_timeout_seconds, type: :integer) config.add(:in_person_enrollments_ready_job_wait_time_seconds, type: :integer) config.add(:in_person_results_delay_in_hours, type: :integer) + config.add(:in_person_ssn_info_controller_enabled, type: :boolean) config.add(:in_person_completion_survey_url, type: :string) config.add(:in_person_usps_outage_message_enabled, type: :boolean) + config.add(:in_person_send_proofing_notifications_enabled, type: :boolean) config.add(:in_person_stop_expiring_enrollments, type: :boolean) config.add(:include_slo_in_saml_metadata, type: :boolean) config.add(:lexisnexis_base_url, type: :string) @@ -356,7 +358,6 @@ def self.build_store(config_map) config.add(:rack_timeout_service_timeout_seconds, type: :integer) config.add(:rails_mailer_previews_enabled, type: :boolean) config.add(:reauthn_window, type: :integer) - config.add(:reauthentication_for_second_factor_management_enabled, type: :boolean) config.add(:recaptcha_enterprise_project_id, type: :string) config.add(:recaptcha_enterprise_api_key, type: :string) config.add(:recaptcha_site_key_v2, type: :string) @@ -442,7 +443,6 @@ def self.build_store(config_map) config.add(:get_usps_proofing_results_job_cron, type: :string) config.add(:get_usps_proofing_results_job_reprocess_delay_minutes, type: :integer) config.add(:get_usps_proofing_results_job_request_delay_milliseconds, type: :integer) - config.add(:use_reauthentication_route, type: :boolean) config.add(:usps_upload_sftp_directory, type: :string) config.add(:usps_upload_sftp_host, type: :string) config.add(:usps_upload_sftp_password, type: :string) diff --git a/spec/controllers/accounts/personal_keys_controller_spec.rb b/spec/controllers/accounts/personal_keys_controller_spec.rb index 528c86c674d..e87e1765687 100644 --- a/spec/controllers/accounts/personal_keys_controller_spec.rb +++ b/spec/controllers/accounts/personal_keys_controller_spec.rb @@ -5,7 +5,7 @@ it 'require recent reauthn' do expect(subject).to have_actions( :before, - :confirm_recently_authenticated, + :confirm_recently_authenticated_2fa, :prompt_for_password_if_pii_locked, ) end diff --git a/spec/controllers/concerns/rate_limit_concern_spec.rb b/spec/controllers/concerns/rate_limit_concern_spec.rb index 8020d96a902..b34aa79b3c7 100644 --- a/spec/controllers/concerns/rate_limit_concern_spec.rb +++ b/spec/controllers/concerns/rate_limit_concern_spec.rb @@ -33,7 +33,7 @@ def update end end - context 'user is not throttled' do + context 'user is not rate limited' do let(:user) { create(:user, :fully_registered) } it 'does not redirect' do @@ -44,10 +44,10 @@ def update end end - context 'with idv_doc_auth throttle (DocumentCapture)' do - it 'redirects to idv_doc_auth throttled error page' do - throttle = Throttle.new(user: user, throttle_type: :idv_doc_auth) - throttle.increment_to_throttled! + context 'with idv_doc_auth rate_limiter (DocumentCapture)' do + it 'redirects to idv_doc_auth rate limited error page' do + rate_limiter = RateLimiter.new(user: user, rate_limit_type: :idv_doc_auth) + rate_limiter.increment_to_limited! get :show @@ -55,10 +55,10 @@ def update end end - context 'with idv_resolution throttle (VerifyInfo)' do - it 'redirects to idv_resolution throttled error page' do - throttle = Throttle.new(user: user, throttle_type: :idv_resolution) - throttle.increment_to_throttled! + context 'with idv_resolution rate_limiter (VerifyInfo)' do + it 'redirects to idv_resolution rate limited error page' do + rate_limiter = RateLimiter.new(user: user, rate_limit_type: :idv_resolution) + rate_limiter.increment_to_limited! get :show @@ -66,13 +66,13 @@ def update end end - context 'with proof_address throttle (PhoneStep)' do + context 'with proof_address rate_limiter (PhoneStep)' do before do - throttle = Throttle.new(user: user, throttle_type: :proof_address) - throttle.increment_to_throttled! + rate_limiter = RateLimiter.new(user: user, rate_limit_type: :proof_address) + rate_limiter.increment_to_limited! end - it 'redirects to proof_address throttled error page' do + it 'redirects to proof_address rate limited error page' do get :show expect(response).to redirect_to idv_phone_errors_failure_url diff --git a/spec/controllers/concerns/reauthentication_required_concern_spec.rb b/spec/controllers/concerns/reauthentication_required_concern_spec.rb index 1095b5d4fc7..f4557a6d9cd 100644 --- a/spec/controllers/concerns/reauthentication_required_concern_spec.rb +++ b/spec/controllers/concerns/reauthentication_required_concern_spec.rb @@ -3,51 +3,6 @@ RSpec.describe ReauthenticationRequiredConcern, type: :controller do let(:user) { create(:user, :fully_registered, email: 'old_email@example.com') } - describe '#confirm_recently_authenticated' do - controller ApplicationController do - include ReauthenticationRequiredConcern - - before_action :confirm_recently_authenticated - - def index - render plain: 'Hello' - end - end - - before(:each) do - stub_sign_in(user) - allow(IdentityConfig.store).to receive( - :reauthentication_for_second_factor_management_enabled, - ).and_return(false) - end - - context 'recently authenticated' do - it 'allows action' do - get :index - - expect(response.body).to eq 'Hello' - end - end - - context 'authenticated outside the authn window' do - before do - controller.user_session[:authn_at] -= IdentityConfig.store.reauthn_window - end - - it 'redirects to password confirmation' do - get :index - - expect(response).to redirect_to user_password_confirm_url - end - - it 'sets context to authentication' do - get :index - - expect(controller.user_session[:context]).to eq 'reauthentication' - end - end - end - describe '#confirm_recently_authenticated_2fa' do controller ApplicationController do include ReauthenticationRequiredConcern diff --git a/spec/controllers/idv/capture_doc_status_controller_spec.rb b/spec/controllers/idv/capture_doc_status_controller_spec.rb index a6909af3369..47ac01691fd 100644 --- a/spec/controllers/idv/capture_doc_status_controller_spec.rb +++ b/spec/controllers/idv/capture_doc_status_controller_spec.rb @@ -59,12 +59,12 @@ end end - context 'when the user is throttled' do + context 'when the user is rate limited' do before do - Throttle.new(throttle_type: :idv_doc_auth, user: user).increment_to_throttled! + RateLimiter.new(rate_limit_type: :idv_doc_auth, user: user).increment_to_limited! end - it 'returns throttled with redirect' do + it 'returns rate_limited with redirect' do get :show expect(response.status).to eq(429) diff --git a/spec/controllers/idv/document_capture_controller_spec.rb b/spec/controllers/idv/document_capture_controller_spec.rb index 34a8631360d..370ddce6c0a 100644 --- a/spec/controllers/idv/document_capture_controller_spec.rb +++ b/spec/controllers/idv/document_capture_controller_spec.rb @@ -5,8 +5,7 @@ let(:flow_session) do { 'document_capture_session_uuid' => 'fd14e181-6fb1-4cdc-92e0-ef66dad0df4e', - :threatmetrix_session_id => 'c90ae7a5-6629-4e77-b97c-f1987c2df7d0', - :flow_path => 'standard' } + :threatmetrix_session_id => 'c90ae7a5-6629-4e77-b97c-f1987c2df7d0' } end let(:user) { create(:user) } @@ -23,6 +22,7 @@ stub_sign_in(user) stub_analytics stub_attempts_tracker + subject.idv_session.flow_path = 'standard' end describe 'before_actions' do @@ -99,7 +99,7 @@ context 'hybrid handoff step is not complete' do it 'redirects to hybrid handoff' do - flow_session.delete(:flow_path) + subject.idv_session.flow_path = nil get :show @@ -123,11 +123,11 @@ get :show end - context 'user is rate_limited' do + context 'user is rate limited' do it 'redirects to rate limited page' do user = create(:user) - Throttle.new(throttle_type: :idv_doc_auth, user: user).increment_to_throttled! + RateLimiter.new(rate_limit_type: :idv_doc_auth, user: user).increment_to_limited! allow(subject).to receive(:current_user).and_return(user) get :show diff --git a/spec/controllers/idv/gpo_verify_controller_spec.rb b/spec/controllers/idv/gpo_verify_controller_spec.rb index 7db2a9d2531..b9d5cae96ec 100644 --- a/spec/controllers/idv/gpo_verify_controller_spec.rb +++ b/spec/controllers/idv/gpo_verify_controller_spec.rb @@ -58,8 +58,8 @@ expect(assigns(:user_can_request_another_gpo_code)).to eql(true) end - it 'shows throttled page is user is throttled' do - Throttle.new(throttle_type: :verify_gpo_key, user: user).increment_to_throttled! + it 'shows rate limited page if user is rate limited' do + RateLimiter.new(rate_limit_type: :verify_gpo_key, user: user).increment_to_limited! action @@ -85,12 +85,12 @@ end end - context 'with throttle reached' do + context 'with rate limit reached' do before do - Throttle.new(throttle_type: :verify_gpo_key, user: user).increment_to_throttled! + RateLimiter.new(rate_limit_type: :verify_gpo_key, user: user).increment_to_limited! end - it 'renders throttled page' do + it 'renders rate limited page' do expect(@analytics).to receive(:track_event).with( 'IdV: GPO verification visited', ).once @@ -329,7 +329,7 @@ end end - context 'with throttle reached' do + context 'with rate limit reached' do let(:submitted_otp) { 'a-wrong-otp' } it 'renders the index page to show errors' do diff --git a/spec/controllers/idv/hybrid_handoff_controller_spec.rb b/spec/controllers/idv/hybrid_handoff_controller_spec.rb index 69d8c59cd88..585d71884a2 100644 --- a/spec/controllers/idv/hybrid_handoff_controller_spec.rb +++ b/spec/controllers/idv/hybrid_handoff_controller_spec.rb @@ -85,7 +85,7 @@ context 'hybrid_handoff already visited' do it 'redirects to document_capture in standard flow' do - subject.user_session['idv/doc_auth'][:flow_path] = 'standard' + subject.idv_session.flow_path = 'standard' get :show @@ -93,7 +93,7 @@ end it 'redirects to link_sent in hybrid flow' do - subject.user_session['idv/doc_auth'][:flow_path] = 'hybrid' + subject.idv_session.flow_path = 'hybrid' get :show @@ -103,7 +103,7 @@ context 'redo document capture' do it 'does not redirect in standard flow' do - subject.user_session['idv/doc_auth'][:flow_path] = 'standard' + subject.idv_session.flow_path = 'standard' get :show, params: { redo: true } @@ -111,7 +111,7 @@ end it 'does not redirect in hybrid flow' do - subject.user_session['idv/doc_auth'][:flow_path] = 'hybrid' + subject.idv_session.flow_path = 'hybrid' get :show, params: { redo: true } @@ -119,7 +119,7 @@ end it 'redirects to document_capture on a mobile device' do - subject.user_session['idv/doc_auth'][:flow_path] = 'standard' + subject.idv_session.flow_path = 'standard' subject.user_session['idv/doc_auth'][:skip_upload_step] = true get :show, params: { redo: true } diff --git a/spec/controllers/idv/image_uploads_controller_spec.rb b/spec/controllers/idv/image_uploads_controller_spec.rb index f1c34aab70a..9f6f103165d 100644 --- a/spec/controllers/idv/image_uploads_controller_spec.rb +++ b/spec/controllers/idv/image_uploads_controller_spec.rb @@ -186,7 +186,7 @@ context 'throttling' do it 'returns remaining_attempts with error' do params.delete(:front) - Throttle.new(throttle_type: :idv_doc_auth, user: user).increment! + RateLimiter.new(rate_limit_type: :idv_doc_auth, user: user).increment! action @@ -195,14 +195,14 @@ { success: false, errors: [{ field: 'front', message: 'Please fill in this field.' }], - remaining_attempts: Throttle.max_attempts(:idv_doc_auth) - 2, + remaining_attempts: RateLimiter.max_attempts(:idv_doc_auth) - 2, result_failed: false, ocr_pii: nil, }, ) end - context 'when throttled' do + context 'when rate limited' do let(:redirect_url) { idv_session_errors_throttled_url } let(:error_json) do { @@ -216,7 +216,7 @@ end before do - Throttle.new(throttle_type: :idv_doc_auth, user: user).increment_to_throttled! + RateLimiter.new(rate_limit_type: :idv_doc_auth, user: user).increment_to_limited! action end @@ -238,7 +238,7 @@ end it 'tracks events' do - Throttle.new(throttle_type: :idv_doc_auth, user: user).increment_to_throttled! + RateLimiter.new(rate_limit_type: :idv_doc_auth, user: user).increment_to_limited! stub_analytics stub_attempts_tracker 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 a02aaf946f3..c19ca5bfaf6 100644 --- a/spec/controllers/idv/in_person/verify_info_controller_spec.rb +++ b/spec/controllers/idv/in_person/verify_info_controller_spec.rb @@ -166,7 +166,7 @@ and_return(10) end - it 'throttles them all' do + it 'rate limits them all' do put :update subject.idv_session.verify_info_step_document_capture_session_uuid = nil put :update diff --git a/spec/controllers/idv/link_sent_controller_spec.rb b/spec/controllers/idv/link_sent_controller_spec.rb index 57807ef955e..0ca1f6a8c5f 100644 --- a/spec/controllers/idv/link_sent_controller_spec.rb +++ b/spec/controllers/idv/link_sent_controller_spec.rb @@ -5,8 +5,7 @@ let(:flow_session) do { 'document_capture_session_uuid' => 'fd14e181-6fb1-4cdc-92e0-ef66dad0df4e', - :threatmetrix_session_id => 'c90ae7a5-6629-4e77-b97c-f1987c2df7d0', - :flow_path => 'hybrid' } + :threatmetrix_session_id => 'c90ae7a5-6629-4e77-b97c-f1987c2df7d0' } end let(:user) { create(:user) } @@ -14,6 +13,7 @@ before do allow(subject).to receive(:flow_session).and_return(flow_session) stub_sign_in(user) + subject.idv_session.flow_path = 'hybrid' stub_analytics stub_attempts_tracker allow(@analytics).to receive(:track_event) @@ -76,7 +76,7 @@ context '#confirm_hybrid_handoff_complete' do context 'no flow_path' do it 'redirects to idv_hybrid_handoff_url' do - flow_session[:flow_path] = nil + subject.idv_session.flow_path = nil get :show @@ -86,7 +86,7 @@ context 'flow_path is standard' do it 'redirects to idv_document_capture_url' do - flow_session[:flow_path] = 'standard' + subject.idv_session.flow_path = 'standard' get :show diff --git a/spec/controllers/idv/otp_verification_controller_spec.rb b/spec/controllers/idv/otp_verification_controller_spec.rb index faf7e3d871c..ea9e9604155 100644 --- a/spec/controllers/idv/otp_verification_controller_spec.rb +++ b/spec/controllers/idv/otp_verification_controller_spec.rb @@ -89,6 +89,61 @@ end end + context 'the user is going through in person proofing' do + before(:each) do + create(:in_person_enrollment, :establishing, user: user) + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled). + and_return(true) + end + + context 'the user uses sms otp' do + it 'does not save the phone number if the feature flag is off' do + put :update, params: otp_code_param + + phone_config = user.establishing_in_person_enrollment&.notification_phone_configuration + expect(phone_config).to_not be_present + end + + it 'saves the sms notification number to the enrollment' do + expect(IdentityConfig.store).to receive(:in_person_send_proofing_notifications_enabled). + and_return(true) + + put :update, params: otp_code_param + + phone_config = user.establishing_in_person_enrollment&.notification_phone_configuration + expect(phone_config).to be_present + expect(phone_config.phone).to eq(phone) + end + end + + context 'the user uses voice otp' do + let(:phone_confirmation_session_properties) do + { + code: phone_confirmation_otp_code, + phone: phone, + delivery_method: :voice, + } + end + + it 'does not save the phone number if the feature flag is off' do + put :update, params: otp_code_param + + phone_config = user.establishing_in_person_enrollment&.notification_phone_configuration + expect(phone_config).to_not be_present + end + + it 'does not save the sms notification number to the enrollment' do + expect(IdentityConfig.store).to receive(:in_person_send_proofing_notifications_enabled). + and_return(true) + + put :update, params: otp_code_param + + phone_config = user.establishing_in_person_enrollment&.notification_phone_configuration + expect(phone_config).to_not be_present + end + end + end + it 'tracks an analytics event' do put :update, params: otp_code_param diff --git a/spec/controllers/idv/phone_controller_spec.rb b/spec/controllers/idv/phone_controller_spec.rb index 80a355db29b..274909c3cc7 100644 --- a/spec/controllers/idv/phone_controller_spec.rb +++ b/spec/controllers/idv/phone_controller_spec.rb @@ -3,7 +3,7 @@ RSpec.describe Idv::PhoneController do include IdvHelper - let(:max_attempts) { Throttle.max_attempts(:proof_address) } + let(:max_attempts) { RateLimiter.max_attempts(:proof_address) } let(:good_phone) { '+1 (703) 555-0000' } let(:bad_phone) do Proofing::Mock::AddressMockClient::UNVERIFIABLE_PHONE_NUMBER @@ -97,9 +97,9 @@ end end - context 'when the user is throttled' do + context 'when the user is rate limited' do before do - Throttle.new(throttle_type: :proof_address, user: user).increment_to_throttled! + RateLimiter.new(rate_limit_type: :proof_address, user: user).increment_to_limited! end it 'redirects to fail' do @@ -484,15 +484,15 @@ get :new end - context 'when the user is throttled by submission' do + context 'when the user is rate limited by submission' do before do stub_analytics user = create(:user, with: { phone: '+1 (415) 555-0130' }) stub_verify_steps_one_and_two(user) - throttle = Throttle.new(throttle_type: :proof_address, user: user) - throttle.increment_to_throttled! + rate_limiter = RateLimiter.new(rate_limit_type: :proof_address, user: user) + rate_limiter.increment_to_limited! put :create, params: { idv_phone_form: { phone: bad_phone } } end @@ -501,7 +501,7 @@ expect(response).to redirect_to idv_phone_errors_failure_url end - it 'tracks throttled event' do + it 'tracks rate limited event' do expect(@analytics).to have_logged_event( 'Throttler Rate Limit Triggered', { diff --git a/spec/controllers/idv/phone_errors_controller_spec.rb b/spec/controllers/idv/phone_errors_controller_spec.rb index 85b3d2bc316..dfe479cbc3e 100644 --- a/spec/controllers/idv/phone_errors_controller_spec.rb +++ b/spec/controllers/idv/phone_errors_controller_spec.rb @@ -131,9 +131,9 @@ end end - context 'with throttle attempts' do + context 'with rate limit attempts' do before do - Throttle.new(throttle_type: :proof_address, user: user).increment! + RateLimiter.new(rate_limit_type: :proof_address, user: user).increment! end it 'assigns remaining count' do @@ -160,11 +160,11 @@ it_behaves_like 'an idv phone errors controller action' - context 'with throttle attempts' do + context 'with rate limit attempts' do let(:user) { create(:user) } before do - Throttle.new(throttle_type: :proof_address, user: user).increment! + RateLimiter.new(rate_limit_type: :proof_address, user: user).increment! end it 'assigns remaining count' do @@ -181,11 +181,11 @@ it_behaves_like 'an idv phone errors controller action' - context 'with throttle attempts' do + context 'with rate limit attempts' do let(:user) { create(:user) } before do - Throttle.new(throttle_type: :proof_address, user: user).increment! + RateLimiter.new(rate_limit_type: :proof_address, user: user).increment! end it 'assigns remaining count' do @@ -212,11 +212,11 @@ it_behaves_like 'an idv phone errors controller action' - context 'while throttled' do + context 'while rate limited' do let(:user) { create(:user) } it 'assigns expiration time' do - Throttle.new(throttle_type: :proof_address, user: user).increment_to_throttled! + RateLimiter.new(rate_limit_type: :proof_address, user: user).increment_to_limited! get action expect(assigns(:expires_at)).to be_kind_of(Time) @@ -225,15 +225,15 @@ it 'logs an event' do freeze_time do attempted_at = Time.zone.now.utc - Throttle.new(throttle_type: :proof_address, user: user).increment_to_throttled! - throttle_window = Throttle.attempt_window_in_minutes(:proof_address).minutes + RateLimiter.new(rate_limit_type: :proof_address, user: user).increment_to_limited! + rate_limit_window = RateLimiter.attempt_window_in_minutes(:proof_address).minutes get action expect(@analytics).to have_received(:track_event).with( 'IdV: phone error visited', type: action, - throttle_expires_at: attempted_at + throttle_window, + throttle_expires_at: attempted_at + rate_limit_window, ) end end diff --git a/spec/controllers/idv/session_errors_controller_spec.rb b/spec/controllers/idv/session_errors_controller_spec.rb index ead5955b9e6..5abc75b1b5d 100644 --- a/spec/controllers/idv/session_errors_controller_spec.rb +++ b/spec/controllers/idv/session_errors_controller_spec.rb @@ -155,11 +155,11 @@ it_behaves_like 'an idv session errors controller action' - context 'with throttle attempts' do + context 'with rate limit attempts' do let(:user) { create(:user) } before do - Throttle.new(throttle_type: :proof_address, user: user).increment! + RateLimiter.new(rate_limit_type: :proof_address, user: user).increment! end it 'assigns remaining count' do @@ -231,11 +231,11 @@ it_behaves_like 'an idv session errors controller action' - context 'while throttled' do + context 'while rate limited' do let(:user) { create(:user) } before do - Throttle.new(throttle_type: :proof_address, user: user).increment_to_throttled! + RateLimiter.new(rate_limit_type: :proof_address, user: user).increment_to_limited! end it 'assigns expiration time' do @@ -271,7 +271,7 @@ it_behaves_like 'an idv session errors controller action' - context 'while throttled' do + context 'while rate limited' do let(:user) { build(:user) } let(:ssn) { '666666666' } @@ -280,10 +280,10 @@ end before do - Throttle.new( - throttle_type: :proof_ssn, + RateLimiter.new( + rate_limit_type: :proof_ssn, target: Pii::Fingerprinter.fingerprint(ssn), - ).increment_to_throttled! + ).increment_to_limited! controller.user_session['idv/doc_auth'] = { 'pii_from_doc' => { 'ssn' => ssn } } end @@ -312,11 +312,11 @@ it_behaves_like 'an idv session errors controller action' - context 'while throttled' do + context 'while rate limited' do let(:user) { create(:user) } before do - Throttle.new(throttle_type: :idv_doc_auth, user: user).increment_to_throttled! + RateLimiter.new(rate_limit_type: :idv_doc_auth, user: user).increment_to_limited! end it 'assigns expiration time' do diff --git a/spec/controllers/idv/ssn_controller_spec.rb b/spec/controllers/idv/ssn_controller_spec.rb index 69a3c7aaca7..e09163a90ed 100644 --- a/spec/controllers/idv/ssn_controller_spec.rb +++ b/spec/controllers/idv/ssn_controller_spec.rb @@ -6,8 +6,7 @@ let(:flow_session) do { 'document_capture_session_uuid' => 'fd14e181-6fb1-4cdc-92e0-ef66dad0df4e', 'pii_from_doc' => Idp::Constants::MOCK_IDV_APPLICANT.dup, - :threatmetrix_session_id => 'c90ae7a5-6629-4e77-b97c-f1987c2df7d0', - :flow_path => 'standard' } + :threatmetrix_session_id => 'c90ae7a5-6629-4e77-b97c-f1987c2df7d0' } end let(:ssn) { Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN[:ssn] } @@ -17,6 +16,7 @@ before do stub_sign_in(user) subject.user_session['idv/doc_auth'] = flow_session + subject.idv_session.flow_path = 'standard' stub_analytics stub_attempts_tracker allow(@analytics).to receive(:track_event) @@ -258,7 +258,7 @@ context 'when pii_from_doc is not present' do before do - flow_session[:flow_path] = 'standard' + subject.idv_session.flow_path = 'standard' flow_session.delete('pii_from_doc') end @@ -269,14 +269,14 @@ end it 'redirects to LinkSentController on hybrid flow' do - flow_session[:flow_path] = 'hybrid' + subject.idv_session.flow_path = 'hybrid' put :update expect(response.status).to eq 302 expect(response).to redirect_to idv_link_sent_url end it 'redirects to hybrid_handoff if there is no flow_path' do - flow_session[:flow_path] = nil + subject.idv_session.flow_path = nil put :update expect(response.status).to eq 302 expect(response).to redirect_to idv_hybrid_handoff_url diff --git a/spec/controllers/idv/verify_info_controller_spec.rb b/spec/controllers/idv/verify_info_controller_spec.rb index ff534a457c4..05ee06b76e7 100644 --- a/spec/controllers/idv/verify_info_controller_spec.rb +++ b/spec/controllers/idv/verify_info_controller_spec.rb @@ -7,8 +7,7 @@ { 'error_message' => nil, 'document_capture_session_uuid' => 'fd14e181-6fb1-4cdc-92e0-ef66dad0df4e', :pii_from_doc => Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN.dup, - 'threatmetrix_session_id' => 'c90ae7a5-6629-4e77-b97c-f1987c2df7d0', - :flow_path => 'standard' } + 'threatmetrix_session_id' => 'c90ae7a5-6629-4e77-b97c-f1987c2df7d0' } end let(:user) { create(:user) } @@ -26,6 +25,7 @@ before do allow(subject).to receive(:flow_session).and_return(flow_session) stub_idv_steps_before_verify_step(user) + subject.idv_session.flow_path = 'standard' end describe 'before_actions' do @@ -126,14 +126,14 @@ expect(response).to redirect_to(idv_ssn_url) end - context 'when the user is ssn throttled' do + context 'when the user is ssn rate limited' do before do - Throttle.new( + RateLimiter.new( target: Pii::Fingerprinter.fingerprint( Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN[:ssn], ), - throttle_type: :proof_ssn, - ).increment_to_throttled! + rate_limit_type: :proof_ssn, + ).increment_to_limited! end it 'redirects to ssn failure url' do @@ -150,15 +150,15 @@ end end - context 'when the user is proofing throttled' do + context 'when the user is proofing rate limited' do before do - Throttle.new( + RateLimiter.new( user: subject.current_user, - throttle_type: :idv_resolution, - ).increment_to_throttled! + rate_limit_type: :idv_resolution, + ).increment_to_limited! end - it 'redirects to throttled url' do + it 'redirects to rate limited url' do get :show expect(response).to redirect_to idv_session_errors_failure_url @@ -422,14 +422,14 @@ ) end - context 'when the user is ssn throttled' do + context 'when the user is ssn rate limited' do before do - Throttle.new( + RateLimiter.new( target: Pii::Fingerprinter.fingerprint( Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN[:ssn], ), - throttle_type: :proof_ssn, - ).increment_to_throttled! + rate_limit_type: :proof_ssn, + ).increment_to_limited! end it 'redirects to ssn failure url' do @@ -446,15 +446,15 @@ end end - context 'when the user is proofing throttled' do + context 'when the user is proofing rate limited' do before do - Throttle.new( + RateLimiter.new( user: subject.current_user, - throttle_type: :idv_resolution, - ).increment_to_throttled! + rate_limit_type: :idv_resolution, + ).increment_to_limited! end - it 'redirects to throttled url' do + it 'redirects to rate limited url' do put :update expect(response).to redirect_to idv_session_errors_failure_url diff --git a/spec/controllers/idv_controller_spec.rb b/spec/controllers/idv_controller_spec.rb index 940920bcebf..9beab394d08 100644 --- a/spec/controllers/idv_controller_spec.rb +++ b/spec/controllers/idv_controller_spec.rb @@ -54,7 +54,7 @@ :profile, user: user, ) - Throttle.new(throttle_type: :idv_resolution, user: user).increment_to_throttled! + RateLimiter.new(rate_limit_type: :idv_resolution, user: user).increment_to_limited! stub_sign_in(profile.user) end @@ -81,12 +81,12 @@ :profile, user: user, ) - Throttle.new(throttle_type: :idv_doc_auth, user: user).increment_to_throttled! + RateLimiter.new(rate_limit_type: :idv_doc_auth, user: user).increment_to_limited! stub_sign_in(profile.user) end - it 'redirects to throttled page' do + it 'redirects to rate limited page' do get :index expect(response).to redirect_to idv_session_errors_throttled_url @@ -100,12 +100,12 @@ :profile, user: user, ) - Throttle.new(throttle_type: :proof_address, user: user).increment_to_throttled! + RateLimiter.new(rate_limit_type: :proof_address, user: user).increment_to_limited! stub_sign_in(profile.user) end - it 'redirects to throttled page' do + it 'redirects to rate limited page' do get :index expect(response).to redirect_to idv_phone_errors_failure_url diff --git a/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb index 14e621014c5..9109b47b4e5 100644 --- a/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb @@ -22,10 +22,12 @@ end describe 'when signed in before 2fa' do + let(:user) { create(:user) } + before do stub_analytics stub_attempts_tracker - sign_in_before_2fa + sign_in_before_2fa(user) end describe 'GET show' do @@ -33,14 +35,15 @@ get :show expect(response).to redirect_to(user_two_factor_authentication_url) end + context 'with webauthn configured' do + let!(:webauthn_configuration) { create(:webauthn_configuration, user:) } + before do - allow_any_instance_of(TwoFactorAuthentication::WebauthnPolicy). - to receive(:enabled?). - and_return(true) allow(@analytics).to receive(:track_event) allow(@irs_attempts_api_tracker).to receive(:track_event) end + it 'tracks an analytics event' do get :show, params: { platform: true } result = { context: 'authentication', @@ -51,6 +54,20 @@ result, ) end + + it 'assigns presenter instance variable with initialized credentials' do + get :show, params: { platform: true } + + presenter = assigns(:presenter) + + expect(presenter).to be_a(TwoFactorAuthCode::WebauthnAuthenticationPresenter) + expect(presenter.credentials).to eq( + [ + id: webauthn_configuration.credential_id, + transports: webauthn_configuration.transports, + ].to_json, + ) + end end end @@ -101,10 +118,10 @@ it 'tracks a valid platform authenticator submission' do create( :webauthn_configuration, + :platform_authenticator, user: controller.current_user, credential_id: credential_id, credential_public_key: credential_public_key, - platform_authenticator: true, ) allow(WebauthnVerificationForm).to receive(:domain_name).and_return('localhost:3000') result = { context: 'authentication', @@ -172,10 +189,10 @@ and_return(true) create( :webauthn_configuration, + :platform_authenticator, user: controller.current_user, credential_id: credential_id, credential_public_key: credential_public_key, - platform_authenticator: true, ) end diff --git a/spec/controllers/users/two_factor_authentication_controller_spec.rb b/spec/controllers/users/two_factor_authentication_controller_spec.rb index d769ca75103..4ab40b4e664 100644 --- a/spec/controllers/users/two_factor_authentication_controller_spec.rb +++ b/spec/controllers/users/two_factor_authentication_controller_spec.rb @@ -619,7 +619,7 @@ def index end timeout = distance_of_time_in_words( - Throttle.attempt_window_in_minutes(:phone_confirmation).minutes, + RateLimiter.attempt_window_in_minutes(:phone_confirmation).minutes, ) expect(flash[:error]).to eq( @@ -672,7 +672,7 @@ def index end timeout = distance_of_time_in_words( - Throttle.attempt_window_in_minutes(:phone_confirmation).minutes, + RateLimiter.attempt_window_in_minutes(:phone_confirmation).minutes, ) expect(flash[:error]).to eq( diff --git a/spec/controllers/users/verify_personal_key_controller_spec.rb b/spec/controllers/users/verify_personal_key_controller_spec.rb index 0262334b471..cfe620dc3a5 100644 --- a/spec/controllers/users/verify_personal_key_controller_spec.rb +++ b/spec/controllers/users/verify_personal_key_controller_spec.rb @@ -36,8 +36,8 @@ expect(subject.flash[:info]).to eq(t('notices.account_reactivation')) end - it 'shows throttled page after being throttled' do - Throttle.new(throttle_type: :verify_personal_key, user: user).increment_to_throttled! + it 'shows rate limited page after being rate limited' do + RateLimiter.new(rate_limit_type: :verify_personal_key, user: user).increment_to_limited! get :new @@ -45,14 +45,14 @@ end end - context 'with throttle reached' do + context 'with rate limit reached' do let!(:profiles) { [create(:profile, :verified, :password_reset, user: user)] } before do - Throttle.new(throttle_type: :verify_personal_key, user: user).increment_to_throttled! + RateLimiter.new(rate_limit_type: :verify_personal_key, user: user).increment_to_limited! end - it 'renders throttled page' do + it 'renders rate limited page' do stub_analytics stub_attempts_tracker expect(@analytics).to receive(:track_event).with( @@ -156,8 +156,8 @@ end end - context 'with throttle reached' do - it 'renders throttled page' do + context 'with rate limit reached' do + it 'renders rate limited page' do stub_analytics stub_attempts_tracker expect(@analytics).to receive(:track_event).with( @@ -174,7 +174,7 @@ expect(@irs_attempts_api_tracker).to receive(:personal_key_reactivation_rate_limited).once - max_attempts = Throttle.max_attempts(:verify_personal_key) + max_attempts = RateLimiter.max_attempts(:verify_personal_key) max_attempts.times { post :create, params: personal_key_bad_params } expect(response).to render_template(:throttled) diff --git a/spec/controllers/users/webauthn_setup_controller_spec.rb b/spec/controllers/users/webauthn_setup_controller_spec.rb index e59ab7d82e1..67419933785 100644 --- a/spec/controllers/users/webauthn_setup_controller_spec.rb +++ b/spec/controllers/users/webauthn_setup_controller_spec.rb @@ -69,6 +69,7 @@ attestation_object: attestation_object, client_data_json: setup_client_data_json, name: 'mykey', + transports: 'usb', } end @@ -174,6 +175,7 @@ attestation_object: attestation_object, client_data_json: setup_client_data_json, name: 'mykey', + transports: 'usb', } end @@ -206,6 +208,7 @@ attestation_object: attestation_object, client_data_json: setup_client_data_json, name: 'mykey', + transports: 'usb', } end it 'should log expected events' do @@ -258,6 +261,7 @@ attestation_object: attestation_object, client_data_json: setup_client_data_json, name: 'mykey', + transports: 'internal,hybrid', platform_authenticator: 'true', } end @@ -306,6 +310,7 @@ attestation_object: attestation_object, client_data_json: setup_client_data_json, name: 'mykey', + transports: 'internal,hybrid', platform_authenticator: 'true', } end diff --git a/spec/decorators/mfa_context_spec.rb b/spec/decorators/mfa_context_spec.rb index 351ebceded8..1695a3132b4 100644 --- a/spec/decorators/mfa_context_spec.rb +++ b/spec/decorators/mfa_context_spec.rb @@ -229,7 +229,7 @@ it 'returns 2' do user = create(:user) create(:webauthn_configuration, user: user) - create(:webauthn_configuration, platform_authenticator: true, user: user) + create(:webauthn_configuration, :platform_authenticator, user: user) subject = described_class.new(user.reload) expect(subject.enabled_mfa_methods_count).to eq(2) diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 5b114d66a93..1423b2ae404 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -90,9 +90,9 @@ next unless user.webauthn_configurations.empty? user.webauthn_configurations << build( :webauthn_configuration, + :platform_authenticator, { user_id: -1, - platform_authenticator: true, }.merge( evaluator.with.slice(:name, :credential_id, :credential_public_key), ), @@ -103,7 +103,7 @@ next unless user.webauthn_configurations.empty? user.webauthn_configurations << build( :webauthn_configuration, - { platform_authenticator: true }. + :platform_authenticator, evaluator.with.slice(:name, :credential_id, :credential_public_key), ) end diff --git a/spec/factories/webauthn_configurations.rb b/spec/factories/webauthn_configurations.rb index 6fb2ce52aa9..aea4f872629 100644 --- a/spec/factories/webauthn_configurations.rb +++ b/spec/factories/webauthn_configurations.rb @@ -5,6 +5,12 @@ sequence(:name) { |n| "token #{n}" } sequence(:credential_id) { |n| "credential #{n}" } sequence(:credential_public_key) { |n| "key #{n}" } + transports { ['usb'] } user { association :user } + + trait :platform_authenticator do + platform_authenticator { true } + transports { ['internal', 'hybrid'] } + end end end diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb index c7295e76e73..87751c5f77f 100644 --- a/spec/features/idv/doc_auth/document_capture_spec.rb +++ b/spec/features/idv/doc_auth/document_capture_spec.rb @@ -72,7 +72,7 @@ end end - context 'throttles calls to acuant', allow_browser_log: true do + context 'rate limits calls to acuant', allow_browser_log: true do let(:fake_attempts_tracker) { IrsAttemptsApiTrackingHelper::FakeAttemptsTracker.new } before do allow_any_instance_of(ApplicationController).to receive( @@ -94,11 +94,11 @@ end end - it 'redirects to the throttled error page' do + it 'redirects to the rate limited error page' do freeze_time do attach_and_submit_images timeout = distance_of_time_in_words( - Throttle.attempt_window_in_minutes(:idv_doc_auth).minutes, + RateLimiter.attempt_window_in_minutes(:idv_doc_auth).minutes, ) message = strip_tags(t('errors.doc_auth.throttled_text_html', timeout: timeout)) expect(page).to have_content(message) @@ -106,7 +106,7 @@ end end - it 'logs the throttled analytics event for doc_auth' do + it 'logs the rate limited analytics event for doc_auth' do attach_and_submit_images expect(fake_analytics).to have_logged_event( 'Throttler Rate Limit Triggered', diff --git a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb index 727e8d000d9..de629566fd8 100644 --- a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb +++ b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb @@ -167,12 +167,12 @@ expect(page.find(':focus')).to match_css('.phone-input__number') end - it 'throttles sending the link' do + it 'rate limits sending the link' do user = user_with_2fa sign_in_and_2fa_user(user) complete_doc_auth_steps_before_hybrid_handoff_step timeout = distance_of_time_in_words( - Throttle.attempt_window_in_minutes(:idv_send_link).minutes, + RateLimiter.attempt_window_in_minutes(:idv_send_link).minutes, ) allow(IdentityConfig.store).to receive(:idv_send_link_max_attempts). and_return(idv_send_link_max_attempts) @@ -211,9 +211,9 @@ throttle_type: :idv_send_link, ) - # Manual expiration is needed for now since the Throttle uses + # Manual expiration is needed for now since the RateLimiter uses # Redis ttl instead of expiretime - Throttle.new(throttle_type: :idv_send_link, user: user).reset! + RateLimiter.new(rate_limit_type: :idv_send_link, user: user).reset! travel_to(Time.zone.now + idv_send_link_attempt_window_in_minutes.minutes) do fill_in :doc_auth_phone, with: '415-555-0199' click_send_link 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 750810c695b..db01993d916 100644 --- a/spec/features/idv/doc_auth/verify_info_step_spec.rb +++ b/spec/features/idv/doc_auth/verify_info_step_spec.rb @@ -181,7 +181,7 @@ end # proof_ssn_max_attempts is 10, vs 5 for resolution, so it doesn't get triggered - it 'throttles resolution and continues when it expires' do + it 'rate limits resolution and continues when it expires' do expect(fake_attempts_tracker).to receive(:idv_verification_rate_limited).at_least(1).times. with({ throttle_context: 'single-session' }) @@ -246,7 +246,7 @@ end end - it 'throttles ssn and continues when it expires' do + it 'rate limits ssn and continues when it expires' do expect(fake_attempts_tracker).to receive(:idv_verification_rate_limited).at_least(1).times. with({ throttle_context: 'multi-session' }) click_idv_continue diff --git a/spec/features/idv/phone_otp_rate_limiting_spec.rb b/spec/features/idv/phone_otp_rate_limiting_spec.rb index 8cf18b2a1e8..5f4e2bd8bca 100644 --- a/spec/features/idv/phone_otp_rate_limiting_spec.rb +++ b/spec/features/idv/phone_otp_rate_limiting_spec.rb @@ -10,7 +10,7 @@ start_idv_from_sp complete_idv_steps_before_phone_otp_verification_step(user) - (Throttle.max_attempts(:phone_otp) - 1).times do + (RateLimiter.max_attempts(:phone_otp) - 1).times do click_on t('links.two_factor_authentication.send_another_code') end @@ -67,8 +67,8 @@ def expect_rate_limit_to_expire(user) retry_minutes = IdentityConfig.store.lockout_period_in_minutes + 1 travel_to(retry_minutes.minutes.from_now) do # This is not good and we can likely drop it once we have upgraded to Redis 7 and switched - # Throttle to use EXPIRETIME rather than TTL - allow_any_instance_of(Throttle).to receive(:attempted_at).and_return( + # RateLimiter to use EXPIRETIME rather than TTL + allow_any_instance_of(RateLimiter).to receive(:attempted_at).and_return( retry_minutes.minutes.ago, ) start_idv_from_sp diff --git a/spec/features/idv/steps/confirmation_step_spec.rb b/spec/features/idv/steps/confirmation_step_spec.rb index db9f6c44ea4..e3e4571d74d 100644 --- a/spec/features/idv/steps/confirmation_step_spec.rb +++ b/spec/features/idv/steps/confirmation_step_spec.rb @@ -23,10 +23,18 @@ it 'allows the user to refresh and still displays the personal key' do # Visit the current path is the same as refreshing + expect(page).to have_content(t('idv.messages.confirm')) + expect(page).to have_content(t('forms.personal_key_partial.acknowledgement.header')) visit current_path + expect(page).not_to have_content(t('idv.messages.confirm')) expect(page).to have_content(t('forms.personal_key_partial.acknowledgement.header')) end + it 'displays information providing details about personal key' do + expect(page).to have_content(t('forms.personal_key_partial.acknowledgement.text')) + expect(page).to have_content(t('forms.personal_key_partial.acknowledgement.help_link_text')) + end + context 'verifying by gpo' do let(:address_verification_mechanism) { :gpo } diff --git a/spec/features/idv/steps/phone_step_spec.rb b/spec/features/idv/steps/phone_step_spec.rb index 4a6ff06afd3..4ed28ba7da1 100644 --- a/spec/features/idv/steps/phone_step_spec.rb +++ b/spec/features/idv/steps/phone_step_spec.rb @@ -227,7 +227,7 @@ end before do - (Throttle.max_attempts(:proof_address) - 1).times do + (RateLimiter.max_attempts(:proof_address) - 1).times do fill_out_phone_form_fail click_idv_continue_for_step(:phone) click_on t('idv.failure.phone.warning.try_again_button') diff --git a/spec/features/remember_device/webauthn_spec.rb b/spec/features/remember_device/webauthn_spec.rb index aeb5ae41b44..88ad0ea152b 100644 --- a/spec/features/remember_device/webauthn_spec.rb +++ b/spec/features/remember_device/webauthn_spec.rb @@ -78,8 +78,8 @@ def remember_device_and_sign_out_user before do create( :webauthn_configuration, + :platform_authenticator, user: user, - platform_authenticator: true, credential_id: credential_id, credential_public_key: credential_public_key, ) diff --git a/spec/features/visitors/password_recovery_spec.rb b/spec/features/visitors/password_recovery_spec.rb index 554649423c4..14b66cc2c3e 100644 --- a/spec/features/visitors/password_recovery_spec.rb +++ b/spec/features/visitors/password_recovery_spec.rb @@ -264,7 +264,7 @@ expect(current_path).to eq new_user_password_path end - it 'throttles reset passwords requests' do + it 'rate limits reset passwords requests' do user = create(:user, :fully_registered) email = user.email diff --git a/spec/features/visitors/resend_email_confirmation_spec.rb b/spec/features/visitors/resend_email_confirmation_spec.rb index 0a349d8138e..74804aa7e01 100644 --- a/spec/features/visitors/resend_email_confirmation_spec.rb +++ b/spec/features/visitors/resend_email_confirmation_spec.rb @@ -19,7 +19,7 @@ expect(unread_emails_for(user.email)).to be_present end - scenario 'user throttled sending confirmation emails' do + scenario 'user rate limited sending confirmation emails' do user.save! email = user.email diff --git a/spec/features/visitors/sign_up_with_email_spec.rb b/spec/features/visitors/sign_up_with_email_spec.rb index 2df4db05d8e..b9aaf9316bf 100644 --- a/spec/features/visitors/sign_up_with_email_spec.rb +++ b/spec/features/visitors/sign_up_with_email_spec.rb @@ -67,7 +67,7 @@ end end - it 'throttles sending confirmations after limit is reached' do + it 'rate limits sending confirmations after limit is reached' do email = 'test@test.com' sign_up_with(email) diff --git a/spec/features/webauthn/management_spec.rb b/spec/features/webauthn/management_spec.rb index d007f620168..cd773560e3e 100644 --- a/spec/features/webauthn/management_spec.rb +++ b/spec/features/webauthn/management_spec.rb @@ -144,8 +144,8 @@ def expect_webauthn_platform_setup_error context 'with webauthn platform associations' do it 'displays the user supplied names of the platform authenticators' do - webauthn_config1 = create(:webauthn_configuration, user: user, platform_authenticator: true) - webauthn_config2 = create(:webauthn_configuration, user: user, platform_authenticator: true) + webauthn_config1 = create(:webauthn_configuration, :platform_authenticator, user:) + webauthn_config2 = create(:webauthn_configuration, :platform_authenticator, user:) sign_in_and_2fa_user(user) visit account_two_factor_authentication_path @@ -161,7 +161,7 @@ def expect_webauthn_platform_setup_error it 'allows the user to setup another key' do mock_webauthn_setup_challenge - create(:webauthn_configuration, user: user, platform_authenticator: true) + create(:webauthn_configuration, :platform_authenticator, user:) sign_in_and_2fa_user(user) @@ -194,7 +194,7 @@ def expect_webauthn_platform_setup_error it 'does not allows the user to setup another platform authenticator' do mock_webauthn_setup_challenge - create(:webauthn_configuration, user: user, platform_authenticator: true) + create(:webauthn_configuration, :platform_authenticator, user:) sign_in_and_2fa_user(user) @@ -204,7 +204,7 @@ def expect_webauthn_platform_setup_error end it 'allows user to delete a platform authenticator when another 2FA option is set up' do - webauthn_config = create(:webauthn_configuration, user: user, platform_authenticator: true) + webauthn_config = create(:webauthn_configuration, :platform_authenticator, user:) sign_in_and_2fa_user(user) visit account_two_factor_authentication_path @@ -223,7 +223,7 @@ def expect_webauthn_platform_setup_error end it 'allows the user to cancel deletion of the platform authenticator' do - webauthn_config = create(:webauthn_configuration, user: user, platform_authenticator: true) + webauthn_config = create(:webauthn_configuration, :platform_authenticator, user:) sign_in_and_2fa_user(user) visit account_two_factor_authentication_path @@ -240,7 +240,7 @@ def expect_webauthn_platform_setup_error end it 'prevents a user from deleting the last key' do - webauthn_config = create(:webauthn_configuration, user: user, platform_authenticator: true) + webauthn_config = create(:webauthn_configuration, :platform_authenticator, user:) sign_in_and_2fa_user(user) PhoneConfiguration.first.update(mfa_enabled: false) @@ -254,7 +254,7 @@ def expect_webauthn_platform_setup_error end it 'gives an error if name is taken and stays on the configuration screen' do - webauthn_config = create(:webauthn_configuration, user: user, platform_authenticator: true) + webauthn_config = create(:webauthn_configuration, :platform_authenticator, user:) mock_webauthn_setup_challenge sign_in_and_2fa_user(user) diff --git a/spec/forms/idv/api_image_upload_form_spec.rb b/spec/forms/idv/api_image_upload_form_spec.rb index 7821c376557..8f7190d0e7b 100644 --- a/spec/forms/idv/api_image_upload_form_spec.rb +++ b/spec/forms/idv/api_image_upload_form_spec.rb @@ -57,14 +57,14 @@ end end - context 'when throttled from submission' do + context 'when rate limited from submission' do it 'is not valid' do expect(irs_attempts_api_tracker).to receive(:idv_document_upload_rate_limited).with(no_args) - Throttle.new( - throttle_type: :idv_doc_auth, + RateLimiter.new( + rate_limit_type: :idv_doc_auth, user: document_capture_session.user, - ).increment_to_throttled! + ).increment_to_limited! form.submit expect(form.valid?).to eq(false) diff --git a/spec/forms/register_user_email_form_spec.rb b/spec/forms/register_user_email_form_spec.rb index 3c571fd3a81..dbafed2e4cb 100644 --- a/spec/forms/register_user_email_form_spec.rb +++ b/spec/forms/register_user_email_form_spec.rb @@ -34,7 +34,7 @@ ) end - it 'creates throttle events after reaching throttle limit' do + it 'creates rate_limiter events after reaching rate_limiter limit' do expect(attempts_tracker).to receive( :user_registration_email_submission_rate_limited, ).with( @@ -95,7 +95,7 @@ end.to change { existing_user.reload.email_language }.from('en').to('fr') end - it 'creates throttle events after reaching throttle limit' do + it 'creates rate_limiter events after reaching rate_limiter limit' do expect(attempts_tracker).to receive( :user_registration_email_submission_rate_limited, ).with( diff --git a/spec/forms/webauthn_setup_form_spec.rb b/spec/forms/webauthn_setup_form_spec.rb index 2fc99e3a1fe..bc6b024e663 100644 --- a/spec/forms/webauthn_setup_form_spec.rb +++ b/spec/forms/webauthn_setup_form_spec.rb @@ -5,18 +5,25 @@ let(:user) { create(:user) } let(:user_session) { { webauthn_challenge: webauthn_challenge } } + let(:domain_name) { 'localhost:3000' } + let(:params) do + { + attestation_object: attestation_object, + client_data_json: setup_client_data_json, + name: 'mykey', + platform_authenticator: false, + transports: 'usb', + } + end let(:subject) { WebauthnSetupForm.new(user, user_session) } + before do + allow(IdentityConfig.store).to receive(:domain_name).and_return(domain_name) + end + describe '#submit' do context 'when the input is valid' do it 'returns FormResponse with success: true and creates a webauthn configuration' do - allow(IdentityConfig.store).to receive(:domain_name).and_return('localhost:3000') - params = { - attestation_object: attestation_object, - client_data_json: setup_client_data_json, - name: 'mykey', - platform_authenticator: false, - } extra_attributes = { enabled_mfa_methods_count: 1, mfa_method_counts: { webauthn: 1 }, @@ -30,48 +37,68 @@ **extra_attributes, ) - expect(user.reload.webauthn_configurations.roaming_authenticators.count).to eq(1) - end + user.reload - it 'creates a platform authenticator if the platform_authenticator param is set' do - allow(IdentityConfig.store).to receive(:domain_name).and_return('localhost:3000') - params = { - attestation_object: attestation_object, - client_data_json: setup_client_data_json, - name: 'mykey', - platform_authenticator: true, - } - - result = subject.submit(protocol, params) - expect(result.extra[:multi_factor_auth_method]).to eq 'webauthn_platform' - - expect(user.reload.webauthn_configurations.platform_authenticators.count).to eq(1) + expect(user.webauthn_configurations.roaming_authenticators.count).to eq(1) + expect(user.webauthn_configurations.roaming_authenticators.first.transports).to eq(['usb']) end it 'sends a recovery information changed event' do - allow(IdentityConfig.store).to receive(:domain_name).and_return('localhost:3000') expect(PushNotification::HttpPush).to receive(:deliver). with(PushNotification::RecoveryInformationChangedEvent.new(user: user)) - params = { - attestation_object: attestation_object, - client_data_json: setup_client_data_json, - name: 'mykey', - platform_authenticator: false, - } - subject.submit(protocol, params) end + + context 'with platform authenticator' do + let(:params) do + super().merge(platform_authenticator: true, transports: 'internal,hybrid') + end + + it 'creates a platform authenticator' do + result = subject.submit(protocol, params) + expect(result.extra[:multi_factor_auth_method]).to eq 'webauthn_platform' + + user.reload + + expect(user.webauthn_configurations.platform_authenticators.count).to eq(1) + expect(user.webauthn_configurations.platform_authenticators.first.transports).to eq( + ['internal', 'hybrid'], + ) + end + end + + context 'with invalid transports' do + let(:params) { super().merge(transports: 'wrong') } + + it 'creates a webauthn configuration without transports' do + subject.submit(protocol, params) + + user.reload + + expect(user.webauthn_configurations.roaming_authenticators.first.transports).to be_nil + end + + it 'includes unknown transports in extra analytics' do + result = subject.submit(protocol, params) + + expect(result.to_h).to eq( + success: true, + errors: {}, + enabled_mfa_methods_count: 1, + mfa_method_counts: { webauthn: 1 }, + multi_factor_auth_method: 'webauthn', + pii_like_keypaths: [[:mfa_method_counts, :phone]], + unknown_transports: ['wrong'], + ) + end + end end - context 'when the input is invalid' do + context 'with invalid attestation response from domain' do + let(:domain_name) { 'example.com' } + it 'returns FormResponse with success: false' do - params = { - attestation_object: attestation_object, - client_data_json: setup_client_data_json, - name: 'mykey', - platform_authenticator: false, - } extra_attributes = { enabled_mfa_methods_count: 0, mfa_method_counts: {}, @@ -85,17 +112,26 @@ **extra_attributes, ) end + end - it 'returns false with an error when the attestation response raises an error' do - allow(IdentityConfig.store).to receive(:domain_name).and_return('localhost:3000') + context 'with missing transports' do + let(:params) { super().except(:transports) } + + it 'creates a webauthn configuration without transports' do + subject.submit(protocol, params) + + user.reload + + expect(user.webauthn_configurations.roaming_authenticators.first.transports).to be_nil + end + end + + context 'when the attestation response raises an error' do + before do allow(WebAuthn::AttestationStatement).to receive(:from).and_raise(StandardError) + end - params = { - attestation_object: attestation_object, - client_data_json: setup_client_data_json, - name: 'mykey', - platform_authenticator: false, - } + it 'returns false with an error when the attestation response raises an error' do extra_attributes = { enabled_mfa_methods_count: 0, mfa_method_counts: {}, diff --git a/spec/javascript/packages/document-capture-polling/index-spec.js b/spec/javascript/packages/document-capture-polling/index-spec.js index 8cd21f90e78..8f722a5f315 100644 --- a/spec/javascript/packages/document-capture-polling/index-spec.js +++ b/spec/javascript/packages/document-capture-polling/index-spec.js @@ -124,7 +124,7 @@ describe('DocumentCapturePolling', () => { expect(subject.elements.form.submit).to.have.been.called(); }); - it('redirects when throttled', async () => { + it('redirects when rate limited', async () => { sandbox .stub(window, 'fetch') .withArgs('/status') diff --git a/spec/lib/telephony/pinpoint/sms_sender_spec.rb b/spec/lib/telephony/pinpoint/sms_sender_spec.rb index 8969b46a38d..988291c0a3d 100644 --- a/spec/lib/telephony/pinpoint/sms_sender_spec.rb +++ b/spec/lib/telephony/pinpoint/sms_sender_spec.rb @@ -101,7 +101,7 @@ def ==(other) end end - context 'when the request is throttled' do + context 'when the request is rate limited' do let(:delivery_status) { 'THROTTLED' } it 'raises an opt out error' do diff --git a/spec/mailers/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb index 9460a4fff30..89b8e152aa0 100644 --- a/spec/mailers/previews/user_mailer_preview.rb +++ b/spec/mailers/previews/user_mailer_preview.rb @@ -32,6 +32,13 @@ def reset_password_instructions ) end + def suspended_reset_password + UserMailer.with( + user: user, + email_address: email_address_record, + ).suspended_reset_password + end + def password_changed UserMailer.with(user: user, email_address: email_address_record). password_changed(disavowal_token: SecureRandom.hex) diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 847e272b65a..13f95464e00 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -762,6 +762,35 @@ def expect_email_body_to_have_help_and_contact_links end end + describe '#suspended_reset_password' do + let(:mail) do + UserMailer.with(user: user, email_address: email_address). + suspended_reset_password + end + + it_behaves_like 'a system email' + it_behaves_like 'an email that respects user email locale preference' + + it 'sends to the specified email' do + expect(mail.to).to eq [email_address.email] + end + + it 'renders the subject' do + expect(mail.subject).to eq t('user_mailer.suspended_reset_password.subject') + end + + it 'renders the body' do + expect(mail.html_part.body). + to have_content( + t( + 'user_mailer.suspended_reset_password.message', + support_code: IdentityConfig.store.account_suspended_support_code, + contact_number: IdentityConfig.store.idv_contact_phone_number, + ), + ) + end + end + describe '#deliver_later' do it 'does not queue email if it potentially contains sensitive value' do user = create(:user) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index aa6de66bba3..3185e805989 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -774,7 +774,7 @@ end describe '#suspend!' do - context 'user is not already supsended' do + context 'user is not already suspended' do let(:mock_session_id) { SecureRandom.uuid } before do UpdateUser.new(user: user, attributes: { unique_session_id: mock_session_id }).call diff --git a/spec/models/webauthn_configuration_spec.rb b/spec/models/webauthn_configuration_spec.rb index 9d9431dd62b..28e856699f7 100644 --- a/spec/models/webauthn_configuration_spec.rb +++ b/spec/models/webauthn_configuration_spec.rb @@ -44,4 +44,48 @@ it { expect(mfa_enabled).to be_truthy } end + + describe '#transports' do + context 'with nil transports' do + before { subject.transports = nil } + + it { expect(subject).to be_valid } + end + + context 'with empty array transports' do + before { subject.transports = [] } + + it { expect(subject).to be_valid } + end + + context 'with single valid transport' do + before { subject.transports = ['ble'] } + + it { expect(subject).to be_valid } + end + + context 'with single invalid transport' do + before { subject.transports = ['wrong'] } + + it { expect(subject).not_to be_valid } + end + + context 'with multiple valid transports' do + before { subject.transports = ['ble', 'hybrid'] } + + it { expect(subject).to be_valid } + end + + context 'with multiple invalid transports' do + before { subject.transports = ['wrong', 'also wrong'] } + + it { expect(subject).not_to be_valid } + end + + context 'with multiple mixed validity transports' do + before { subject.transports = ['ble', 'wrong'] } + + it { expect(subject).not_to be_valid } + end + end end diff --git a/spec/presenters/image_upload_response_presenter_spec.rb b/spec/presenters/image_upload_response_presenter_spec.rb index 5c327ef2bd4..beb44410db3 100644 --- a/spec/presenters/image_upload_response_presenter_spec.rb +++ b/spec/presenters/image_upload_response_presenter_spec.rb @@ -105,7 +105,7 @@ end end - context 'throttled' do + context 'rate limited' do let(:extra_attributes) do { remaining_attempts: 0, flow_path: 'standard' } end diff --git a/spec/presenters/two_factor_auth_code/webauthn_authentication_presenter_spec.rb b/spec/presenters/two_factor_auth_code/webauthn_authentication_presenter_spec.rb index cba2c3decac..1e8f7c3f83b 100644 --- a/spec/presenters/two_factor_auth_code/webauthn_authentication_presenter_spec.rb +++ b/spec/presenters/two_factor_auth_code/webauthn_authentication_presenter_spec.rb @@ -5,10 +5,14 @@ let(:view) { ActionController::Base.new.view_context } let(:reauthn) {} - let(:presenter) do - TwoFactorAuthCode::WebauthnAuthenticationPresenter. - new(data: { reauthn: reauthn }, service_provider: nil, - view: view, platform_authenticator: platform_authenticator) + let(:credentials) { [] } + subject(:presenter) do + TwoFactorAuthCode::WebauthnAuthenticationPresenter.new( + data: { reauthn:, credentials: }, + service_provider: nil, + view: view, + platform_authenticator: platform_authenticator, + ) end let(:allow_user_to_switch_method) { false } @@ -223,6 +227,12 @@ end end + describe '#credentials' do + it 'returns credentials from initialized data' do + expect(presenter.credentials).to eq credentials + end + end + it 'handles multiple locales' do I18n.available_locales.each do |locale| I18n.locale = locale diff --git a/spec/services/idv/phone_step_spec.rb b/spec/services/idv/phone_step_spec.rb index 3435e7e856e..f95669945d7 100644 --- a/spec/services/idv/phone_step_spec.rb +++ b/spec/services/idv/phone_step_spec.rb @@ -52,7 +52,7 @@ end describe '#submit' do - let(:throttle) { Throttle.new(throttle_type: :proof_address, user: user) } + let(:rate_limiter) { RateLimiter.new(rate_limit_type: :proof_address, user: user) } let(:mock_vendor) do { vendor_name: 'AddressMock', @@ -118,23 +118,23 @@ it 'increments step attempts' do expect do subject.submit(phone: bad_phone) - end.to(change { throttle.fetch_state!.attempts }.by(1)) + end.to(change { rate_limiter.fetch_state!.attempts }.by(1)) end it 'increments step attempts when the vendor request times out' do expect do subject.submit(phone: timeout_phone) - end.to(change { throttle.fetch_state!.attempts }.by(1)) + end.to(change { rate_limiter.fetch_state!.attempts }.by(1)) end it 'does not increment step attempts when the vendor raises an exception' do expect do subject.submit(phone: fail_phone) - end.to(change { throttle.fetch_state!.attempts }.by(1)) + end.to(change { rate_limiter.fetch_state!.attempts }.by(1)) end - it 'logs a throttled attempts_tracker event' do - throttle.increment_to_throttled! + it 'logs a rate limited attempts_tracker event' do + rate_limiter.increment_to_limited! expect(@irs_attempts_api_tracker).to receive(:idv_phone_otp_sent_rate_limited) subject.submit(phone: bad_phone) @@ -198,7 +198,7 @@ context 'when there are not idv attempts remaining' do it 'returns :fail' do - Throttle.new(throttle_type: :proof_address, user: user).increment_to_throttled! + RateLimiter.new(rate_limit_type: :proof_address, user: user).increment_to_limited! subject.submit(phone: bad_phone) expect(subject.failure_reason).to eq(:fail) diff --git a/spec/services/otp_rate_limiter_spec.rb b/spec/services/otp_rate_limiter_spec.rb index 88c2ba2f0f4..27c6187d5a8 100644 --- a/spec/services/otp_rate_limiter_spec.rb +++ b/spec/services/otp_rate_limiter_spec.rb @@ -62,7 +62,7 @@ otp_rate_limiter.increment expect { otp_rate_limiter.increment }. - to change { otp_rate_limiter.throttle.attempts }.from(1).to(2) + to change { otp_rate_limiter.rate_limiter.attempts }.from(1).to(2) end end diff --git a/spec/services/rate_limiter_spec.rb b/spec/services/rate_limiter_spec.rb new file mode 100644 index 00000000000..9a4d629e454 --- /dev/null +++ b/spec/services/rate_limiter_spec.rb @@ -0,0 +1,191 @@ +require 'rails_helper' + +RSpec.describe RateLimiter do + let(:rate_limit_type) { :idv_doc_auth } + let(:max_attempts) { 3 } + let(:attempt_window) { 10 } + before(:each) do + allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(max_attempts) + allow(IdentityConfig.store).to receive(:doc_auth_attempt_window_in_minutes). + and_return(attempt_window) + end + + describe '.new' do + context 'when target is a string' do + subject(:for_target) { RateLimiter.new(target: target, rate_limit_type: rate_limit_type) } + + context 'target is not a string' do + it 'raises an error' do + expect { RateLimiter.new(target: 3, rate_limit_type: rate_limit_type) }. + to raise_error(ArgumentError) + end + end + end + + it 'throws an error when neither user nor target are provided' do + expect { RateLimiter.new(rate_limit_type: rate_limit_type) }. + to raise_error( + ArgumentError, + 'RateLimiter must have a user or a target, but neither were provided', + ) + end + + it 'throws an error when both user and target are provided' do + expect { RateLimiter.new(rate_limit_type: rate_limit_type) }. + to raise_error( + ArgumentError, + 'RateLimiter must have a user or a target, but neither were provided', + ) + end + + it 'throws an error for an invalid rate_limit_type' do + expect { RateLimiter.new(rate_limit_type: :abc_123, target: '1') }. + to raise_error( + ArgumentError, + 'rate_limit_type is not valid', + ) + end + end + + describe '.attempt_window_in_minutes' do + it 'returns configured attempt window for rate_limiter type' do + expect(RateLimiter.attempt_window_in_minutes(rate_limit_type)).to eq(attempt_window) + end + + it 'is indifferent to rate_limiter type stringiness' do + expect(RateLimiter.attempt_window_in_minutes(rate_limit_type.to_s)).to eq(attempt_window) + end + end + + describe '.max_attempts' do + it 'returns configured attempt window for rate_limiter type' do + expect(RateLimiter.max_attempts(rate_limit_type)).to eq(max_attempts) + end + + it 'is indifferent to rate_limiter type stringiness' do + expect(RateLimiter.max_attempts(rate_limit_type.to_s)).to eq(max_attempts) + end + end + + describe '#increment!' do + subject(:rate_limiter) { RateLimiter.new(target: 'aaa', rate_limit_type: :idv_doc_auth) } + let(:max_attempts) { 1 } # Picked up by before block at top of test file + + it 'increments attempts' do + expect(rate_limiter.attempts).to eq 0 + rate_limiter.increment! + expect(rate_limiter.attempts).to eq 1 + end + + it 'does nothing if already rate limited' do + expect(rate_limiter.attempts).to eq 0 + rate_limiter.increment! + expect(rate_limiter.attempts).to eq 1 + expect(rate_limiter.limited?).to eq(true) + current_expiration = rate_limiter.expires_at + travel 5.minutes do # move within 10 minute expiration window + rate_limiter.increment! + expect(rate_limiter.attempts).to eq 1 + expect(rate_limiter.expires_at).to eq current_expiration + end + end + end + + describe '#limited?' do + let(:rate_limit_type) { :idv_doc_auth } + let(:max_attempts) { RateLimiter.max_attempts(rate_limit_type) } + let(:attempt_window_in_minutes) { RateLimiter.attempt_window_in_minutes(rate_limit_type) } + + subject(:rate_limiter) { RateLimiter.new(target: '1', rate_limit_type: rate_limit_type) } + + it 'returns true if rate limited' do + max_attempts.times do + rate_limiter.increment! + end + + expect(rate_limiter.limited?).to eq(true) + end + + it 'returns false if the attempts < max_attempts' do + (max_attempts - 1).times do + expect(rate_limiter.limited?).to eq(false) + rate_limiter.increment! + end + + expect(rate_limiter.limited?).to eq(false) + end + + it 'returns false if the attempts <= max_attempts but the window is expired' do + max_attempts.times do + rate_limiter.increment! + end + + travel(attempt_window_in_minutes.minutes + 1) do + expect(rate_limiter.limited?).to eq(false) + end + end + end + + describe '#expires_at' do + let(:attempted_at) { nil } + let(:rate_limiter) { RateLimiter.new(target: '1', rate_limit_type: rate_limit_type) } + + context 'without having attempted' do + it 'returns current time' do + freeze_time do + expect(rate_limiter.expires_at).to eq(Time.zone.now) + end + end + end + + context 'with expired rate_limiter' do + it 'returns expiration time' do + rate_limiter.increment! + + travel_to(rate_limiter.attempted_at + 3.days) do + expect(rate_limiter.expires_at).to be_within(1.second). + of(rate_limiter.attempted_at + attempt_window.minutes) + end + end + end + + context 'with active rate_limiter' do + it 'returns expiration time' do + freeze_time do + rate_limiter.increment! + expect(rate_limiter.expires_at).to be_within(1.second). + of(rate_limiter.attempted_at + attempt_window.minutes) + end + end + end + end + + describe '#reset' do + let(:target) { '1' } + let(:subject) { described_class } + + subject(:rate_limiter) { RateLimiter.new(target: target, rate_limit_type: rate_limit_type) } + + it 'resets attempt count to 0' do + rate_limiter.increment! + + expect { rate_limiter.reset! }.to change { rate_limiter.attempts }.to(0) + end + end + + describe '#remaining_count' do + let(:target) { '1' } + let(:subject) { described_class } + + subject(:rate_limiter) { RateLimiter.new(target: target, rate_limit_type: rate_limit_type) } + + it 'returns maximium remaining attempts with zero attempts' do + expect(rate_limiter.remaining_count).to eq(RateLimiter.max_attempts(rate_limit_type)) + end + + it 'returns zero when rate_limiter limit is reached' do + rate_limiter.increment_to_limited! + expect(rate_limiter.remaining_count).to eq(0) + end + end +end diff --git a/spec/services/request_password_reset_spec.rb b/spec/services/request_password_reset_spec.rb index 6534cb77f8b..fc9f7c82b1c 100644 --- a/spec/services/request_password_reset_spec.rb +++ b/spec/services/request_password_reset_spec.rb @@ -62,7 +62,7 @@ end end - it 'sends password reset instructions' do + it 'sets password reset token' do expect { subject }. to(change { user.reload.reset_password_token }) end @@ -81,6 +81,57 @@ end end + context 'when the user is found and confirmed, but is suspended' do + subject(:perform) do + described_class.new( + email: email, + irs_attempts_api_tracker: irs_attempts_api_tracker, + ).perform + end + + before do + user.suspend! + allow(UserMailer).to receive(:reset_password_instructions). + and_wrap_original do |impl, user, email, options| + token = options.fetch(:token) + expect(token).to be_present + expect(Devise.token_generator.digest(User, :reset_password_token, token)). + to eq(user.reset_password_token) + + impl.call(user, email, **options) + end + allow(UserMailer).to receive(:suspended_reset_password). + and_wrap_original do |impl, user, email, options| + token = options.fetch(:token) + expect(token).not_to be_present + + impl.call(user, email, **options) + end + end + + it 'does not set a password reset token' do + expect { subject }. + not_to(change { user.reload.reset_password_token }) + end + + it 'sends an email to the suspended user' do + expect { subject }.to change { ActionMailer::Base.deliveries.count }.by(1) + end + + it 'does not send a recovery activated push event' do + expect(PushNotification::HttpPush).not_to receive(:deliver). + with(PushNotification::RecoveryActivatedEvent.new(user: user)) + + subject + end + + it 'does not call irs tracking method forgot_password_email_sent' do + subject + + expect(irs_attempts_api_tracker).not_to have_received(:forgot_password_email_sent) + end + end + context 'when the user is found, not privileged, and not yet confirmed' do it 'sends password reset instructions' do allow(UserMailer).to receive(:reset_password_instructions). @@ -164,7 +215,7 @@ context 'when the user requests password resets above the allowable threshold' do let(:analytics) { FakeAnalytics.new } - it 'throttles the email sending and logs a throttle event' do + it 'rate limits the email sending and logs a rate limit event' do max_attempts = IdentityConfig.store.reset_password_email_max_attempts (max_attempts - 1).times do @@ -178,7 +229,7 @@ to(change { user.reload.reset_password_token }) end - # extra time, throttled + # extra time, rate limited expect do RequestPasswordReset.new( email: email, @@ -197,7 +248,7 @@ ) end - it 'only sends a push notification when the attempts have not been throttled' do + it 'only sends a push notification when the attempts have not been rate limited' do max_attempts = IdentityConfig.store.reset_password_email_max_attempts expect(PushNotification::HttpPush).to receive(:deliver). @@ -215,7 +266,7 @@ to(change { user.reload.reset_password_token }) end - # extra time, throttled + # extra time, rate limited expect do RequestPasswordReset.new( email: email, diff --git a/spec/services/throttle_spec.rb b/spec/services/throttle_spec.rb deleted file mode 100644 index c97c007d345..00000000000 --- a/spec/services/throttle_spec.rb +++ /dev/null @@ -1,191 +0,0 @@ -require 'rails_helper' - -RSpec.describe Throttle do - let(:throttle_type) { :idv_doc_auth } - let(:max_attempts) { 3 } - let(:attempt_window) { 10 } - before(:each) do - allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(max_attempts) - allow(IdentityConfig.store).to receive(:doc_auth_attempt_window_in_minutes). - and_return(attempt_window) - end - - describe '.new' do - context 'when target is a string' do - subject(:for_target) { Throttle.new(target: target, throttle_type: throttle_type) } - - context 'target is not a string' do - it 'raises an error' do - expect { Throttle.new(target: 3, throttle_type: throttle_type) }. - to raise_error(ArgumentError) - end - end - end - - it 'throws an error when neither user nor target are provided' do - expect { Throttle.new(throttle_type: throttle_type) }. - to raise_error( - ArgumentError, - 'Throttle must have a user or a target, but neither were provided', - ) - end - - it 'throws an error when both user and target are provided' do - expect { Throttle.new(throttle_type: throttle_type) }. - to raise_error( - ArgumentError, - 'Throttle must have a user or a target, but neither were provided', - ) - end - - it 'throws an error for an invalid throttle_type' do - expect { Throttle.new(throttle_type: :abc_123, target: '1') }. - to raise_error( - ArgumentError, - 'throttle_type is not valid', - ) - end - end - - describe '.attempt_window_in_minutes' do - it 'returns configured attempt window for throttle type' do - expect(Throttle.attempt_window_in_minutes(throttle_type)).to eq(attempt_window) - end - - it 'is indifferent to throttle type stringiness' do - expect(Throttle.attempt_window_in_minutes(throttle_type.to_s)).to eq(attempt_window) - end - end - - describe '.max_attempts' do - it 'returns configured attempt window for throttle type' do - expect(Throttle.max_attempts(throttle_type)).to eq(max_attempts) - end - - it 'is indifferent to throttle type stringiness' do - expect(Throttle.max_attempts(throttle_type.to_s)).to eq(max_attempts) - end - end - - describe '#increment!' do - subject(:throttle) { Throttle.new(target: 'aaa', throttle_type: :idv_doc_auth) } - let(:max_attempts) { 1 } # Picked up by before block at top of test file - - it 'increments attempts' do - expect(throttle.attempts).to eq 0 - throttle.increment! - expect(throttle.attempts).to eq 1 - end - - it 'does nothing if already throttled' do - expect(throttle.attempts).to eq 0 - throttle.increment! - expect(throttle.attempts).to eq 1 - expect(throttle.throttled?).to eq(true) - current_expiration = throttle.expires_at - travel 5.minutes do # move within 10 minute expiration window - throttle.increment! - expect(throttle.attempts).to eq 1 - expect(throttle.expires_at).to eq current_expiration - end - end - end - - describe '#throttled?' do - let(:throttle_type) { :idv_doc_auth } - let(:max_attempts) { Throttle.max_attempts(throttle_type) } - let(:attempt_window_in_minutes) { Throttle.attempt_window_in_minutes(throttle_type) } - - subject(:throttle) { Throttle.new(target: '1', throttle_type: throttle_type) } - - it 'returns true if throttled' do - max_attempts.times do - throttle.increment! - end - - expect(throttle.throttled?).to eq(true) - end - - it 'returns false if the attempts < max_attempts' do - (max_attempts - 1).times do - expect(throttle.throttled?).to eq(false) - throttle.increment! - end - - expect(throttle.throttled?).to eq(false) - end - - it 'returns false if the attempts <= max_attempts but the window is expired' do - max_attempts.times do - throttle.increment! - end - - travel(attempt_window_in_minutes.minutes + 1) do - expect(throttle.throttled?).to eq(false) - end - end - end - - describe '#expires_at' do - let(:attempted_at) { nil } - let(:throttle) { Throttle.new(target: '1', throttle_type: throttle_type) } - - context 'without having attempted' do - it 'returns current time' do - freeze_time do - expect(throttle.expires_at).to eq(Time.zone.now) - end - end - end - - context 'with expired throttle' do - it 'returns expiration time' do - throttle.increment! - - travel_to(throttle.attempted_at + 3.days) do - expect(throttle.expires_at).to be_within(1.second). - of(throttle.attempted_at + attempt_window.minutes) - end - end - end - - context 'with active throttle' do - it 'returns expiration time' do - freeze_time do - throttle.increment! - expect(throttle.expires_at).to be_within(1.second). - of(throttle.attempted_at + attempt_window.minutes) - end - end - end - end - - describe '#reset' do - let(:target) { '1' } - let(:subject) { described_class } - - subject(:throttle) { Throttle.new(target: target, throttle_type: throttle_type) } - - it 'resets attempt count to 0' do - throttle.increment! - - expect { throttle.reset! }.to change { throttle.attempts }.to(0) - end - end - - describe '#remaining_count' do - let(:target) { '1' } - let(:subject) { described_class } - - subject(:throttle) { Throttle.new(target: target, throttle_type: throttle_type) } - - it 'returns maximium remaining attempts with zero attempts' do - expect(throttle.remaining_count).to eq(Throttle.max_attempts(throttle_type)) - end - - it 'returns zero when throttle limit is reached' do - throttle.increment_to_throttled! - expect(throttle.remaining_count).to eq(0) - end - end -end diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb index ec00dbfeaaa..5f679a3f5e3 100644 --- a/spec/support/features/session_helper.rb +++ b/spec/support/features/session_helper.rb @@ -336,7 +336,7 @@ def acknowledge_and_confirm_personal_key end def click_acknowledge_personal_key - checkbox_header = t('forms.validation.required_checkbox') + checkbox_header = t('forms.personal_key.required_checkbox') find('label', text: /#{checkbox_header}/).click click_continue end diff --git a/spec/support/idv_examples/max_attempts.rb b/spec/support/idv_examples/max_attempts.rb index bfb7a55f75e..ef0e19cdf47 100644 --- a/spec/support/idv_examples/max_attempts.rb +++ b/spec/support/idv_examples/max_attempts.rb @@ -44,7 +44,7 @@ context 'after completing one less than the max attempts' do it 'allows the user to continue if their last attempt is successful' do - (Throttle.max_attempts(:proof_address) - 1).times do + (RateLimiter.max_attempts(:proof_address) - 1).times do fill_out_phone_form_fail click_idv_continue_for_step(step) click_on t('idv.failure.phone.warning.try_again_button') @@ -57,7 +57,7 @@ end def perfom_maximum_allowed_idv_step_attempts(step) - (Throttle.max_attempts(:proof_address) - 1).times do + (RateLimiter.max_attempts(:proof_address) - 1).times do yield click_idv_continue_for_step(step) click_on t('idv.failure.phone.warning.try_again_button')