diff --git a/app/controllers/account_reset/request_controller.rb b/app/controllers/account_reset/request_controller.rb index dc16c0d3c50..6603b3d26f0 100644 --- a/app/controllers/account_reset/request_controller.rb +++ b/app/controllers/account_reset/request_controller.rb @@ -13,7 +13,11 @@ def show end def create - create_account_reset_request + rate_limiter = RateLimiter.new(user: current_user, rate_limit_type: :account_reset_request) + rate_limiter.increment! + unless rate_limiter.limited? + create_account_reset_request + end flash[:email] = current_user.email_addresses.take.email redirect_to account_reset_confirm_request_url diff --git a/app/services/rate_limiter.rb b/app/services/rate_limiter.rb index 9554f9e54b5..054e281b03c 100644 --- a/app/services/rate_limiter.rb +++ b/app/services/rate_limiter.rb @@ -215,6 +215,10 @@ def self.rate_limit_config def self.load_rate_limit_config { + account_reset_request: { + max_attempts: IdentityConfig.store.account_reset_request_max_attempts, + attempt_window: IdentityConfig.store.account_reset_request_attempt_window_in_minutes, + }, idv_doc_auth: { max_attempts: IdentityConfig.store.doc_auth_max_attempts, attempt_window: IdentityConfig.store.doc_auth_attempt_window_in_minutes, diff --git a/app/views/account_reset/request/show.html.erb b/app/views/account_reset/request/show.html.erb index b3b5cd80ff6..c91393748ac 100644 --- a/app/views/account_reset/request/show.html.erb +++ b/app/views/account_reset/request/show.html.erb @@ -19,12 +19,15 @@

<%= t('account_reset.request.delete_email2_html', interval: @account_reset_deletion_period_interval) %>

<%= t('account_reset.request.continue_deletion') %>

-<%= button_to( - account_reset_request_path, - form_class: 'margin-top-4 margin-bottom-2', - class: 'usa-button usa-button--big usa-button--wide', - method: :post, - ) { t('account_reset.request.yes_continue') } %> + +<%= simple_form_for( + :account_reset, + url: account_reset_request_path, + method: 'POST', + ) do |f| %> + <%= f.submit(t('account_reset.request.yes_continue'), data: { disable_with: 'Confirming...' }, class: 'margin-bottom-2') %> +<% end %> + <%= button_to( root_url, method: :get, diff --git a/config/application.yml.default b/config/application.yml.default index be67bcdcba4..969fbff2183 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -24,6 +24,8 @@ aamva_verification_request_timeout: 5.0 aamva_verification_url: https://example.org:12345/verification/url account_creation_device_profiling: disabled account_reset_fraud_user_wait_period_days: +account_reset_request_attempt_window_in_minutes: 2 +account_reset_request_max_attempts: 2 account_reset_token_valid_for_days: 1 account_reset_wait_period_days: 1 account_suspended_support_code: EFGHI diff --git a/lib/identity_config.rb b/lib/identity_config.rb index dcf877e22e6..dc07cb87184 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -45,6 +45,8 @@ def self.store config.add(:account_reset_token_valid_for_days, type: :integer) config.add(:account_reset_wait_period_days, type: :integer) config.add(:account_reset_fraud_user_wait_period_days, type: :integer, allow_nil: true) + config.add(:account_reset_request_attempt_window_in_minutes, type: :integer) + config.add(:account_reset_request_max_attempts, type: :integer) config.add(:account_suspended_support_code, type: :string) config.add(:acuant_sdk_initialization_creds) config.add(:acuant_sdk_initialization_endpoint) diff --git a/spec/controllers/account_reset/request_controller_spec.rb b/spec/controllers/account_reset/request_controller_spec.rb index 4b08c79da24..88696c968ea 100644 --- a/spec/controllers/account_reset/request_controller_spec.rb +++ b/spec/controllers/account_reset/request_controller_spec.rb @@ -158,5 +158,66 @@ expect(response).to redirect_to authentication_methods_setup_url end + + context 'when the Yes, continue deletion... button is clicked multiple times' do + it 'rate limits submission and prevents multiple sms and emails' do + max_attempts = IdentityConfig.store.account_reset_request_max_attempts + user = create(:user, :fully_registered) + stub_sign_in_before_2fa(user) + stub_analytics + + post :create + post :create + + expect(@analytics).to have_logged_event( + 'Account Reset: request', + success: true, + sms_phone: true, + totp: false, + piv_cac: false, + email_addresses: 1, + request_id: 'fake-message-request-id', + message_id: 'fake-message-id', + ) + .exactly(max_attempts - 1) + .times + end + end + + context 'when returning to deletion page after previous submission expired' do + it 'allows the user to submit a deletion request' do + user = create(:user, :fully_registered) + stub_sign_in_before_2fa(user) + stub_analytics + + post :create + + expect(@analytics).to have_logged_event( + 'Account Reset: request', + success: true, + sms_phone: true, + totp: false, + piv_cac: false, + email_addresses: 1, + request_id: 'fake-message-request-id', + message_id: 'fake-message-id', + ) + + travel_to(Time.zone.now + 2.days) do + post :create + + expect(@analytics).to have_logged_event( + 'Account Reset: request', + success: true, + sms_phone: true, + totp: false, + piv_cac: false, + email_addresses: 1, + request_id: 'fake-message-request-id', + message_id: 'fake-message-id', + ) + end + end + end end end