diff --git a/app/controllers/concerns/two_factor_authenticatable_methods.rb b/app/controllers/concerns/two_factor_authenticatable_methods.rb index 9c70b576be4..e56a650379a 100644 --- a/app/controllers/concerns/two_factor_authenticatable_methods.rb +++ b/app/controllers/concerns/two_factor_authenticatable_methods.rb @@ -17,11 +17,19 @@ def authenticate_user authenticate_user!(force: true) end - def handle_second_factor_locked_user(type) + def handle_second_factor_locked_user(type:, context: nil) analytics.multi_factor_auth_max_attempts event = PushNotification::MfaLimitAccountLockedEvent.new(user: current_user) PushNotification::HttpPush.deliver(event) handle_max_attempts(type + '_login_attempts') + + if context + if UserSessionContext.authentication_context?(context) + irs_attempts_api_tracker.mfa_login_rate_limited(type: type) + elsif UserSessionContext.confirmation_context?(context) + irs_attempts_api_tracker.mfa_enroll_rate_limited(type: type) + end + end end def handle_too_many_otp_sends @@ -108,13 +116,13 @@ def two_factor_authentication_method # Method will be renamed in the next refactor. # You can pass in any "type" with a corresponding I18n key in # two_factor_authentication.invalid_#{type} - def handle_invalid_otp(type: 'otp') + def handle_invalid_otp(type:, context: nil) update_invalid_user flash.now[:error] = invalid_otp_error(type) if decorated_user.locked_out? - handle_second_factor_locked_user(type) + handle_second_factor_locked_user(context: context, type: type) else render_show_after_invalid end @@ -124,6 +132,8 @@ def invalid_otp_error(type) case type when 'otp' t('two_factor_authentication.invalid_otp') + when 'totp' + t('two_factor_authentication.invalid_otp') when 'personal_key' t('two_factor_authentication.invalid_personal_key') when 'piv_cac' diff --git a/app/controllers/two_factor_authentication/backup_code_verification_controller.rb b/app/controllers/two_factor_authentication/backup_code_verification_controller.rb index 883b4ef474d..9ec76045cea 100644 --- a/app/controllers/two_factor_authentication/backup_code_verification_controller.rb +++ b/app/controllers/two_factor_authentication/backup_code_verification_controller.rb @@ -53,7 +53,7 @@ def handle_invalid_backup_code flash.now[:error] = t('two_factor_authentication.invalid_backup_code') if decorated_user.locked_out? - handle_second_factor_locked_user('backup_code') + handle_second_factor_locked_user(context: context, type: 'backup_code') else render_show_after_invalid end diff --git a/app/controllers/two_factor_authentication/otp_verification_controller.rb b/app/controllers/two_factor_authentication/otp_verification_controller.rb index d794b94ef13..3b662280d53 100644 --- a/app/controllers/two_factor_authentication/otp_verification_controller.rb +++ b/app/controllers/two_factor_authentication/otp_verification_controller.rb @@ -22,7 +22,7 @@ def create if result.success? handle_valid_otp else - handle_invalid_otp + handle_invalid_otp(context: context, type: 'otp') end end diff --git a/app/controllers/two_factor_authentication/personal_key_verification_controller.rb b/app/controllers/two_factor_authentication/personal_key_verification_controller.rb index 6ec0fe51e4f..3f8dc40d29d 100644 --- a/app/controllers/two_factor_authentication/personal_key_verification_controller.rb +++ b/app/controllers/two_factor_authentication/personal_key_verification_controller.rb @@ -40,7 +40,7 @@ def handle_result(result) generate_new_personal_key_for_verified_users_otherwise_retire_the_key_and_ensure_two_mfa handle_valid_otp else - handle_invalid_otp(type: 'personal_key') + handle_invalid_otp(context: context, type: 'personal_key') end end diff --git a/app/controllers/two_factor_authentication/piv_cac_verification_controller.rb b/app/controllers/two_factor_authentication/piv_cac_verification_controller.rb index 5177c656e35..e36aae6a8b7 100644 --- a/app/controllers/two_factor_authentication/piv_cac_verification_controller.rb +++ b/app/controllers/two_factor_authentication/piv_cac_verification_controller.rb @@ -57,7 +57,7 @@ def handle_valid_piv_cac def handle_invalid_piv_cac clear_piv_cac_information - handle_invalid_otp(type: 'piv_cac') + handle_invalid_otp(context: context, type: 'piv_cac') end # This overrides the method in TwoFactorAuthenticatable so that we diff --git a/app/controllers/two_factor_authentication/totp_verification_controller.rb b/app/controllers/two_factor_authentication/totp_verification_controller.rb index 4b2dae910d3..7a50fd5e33c 100644 --- a/app/controllers/two_factor_authentication/totp_verification_controller.rb +++ b/app/controllers/two_factor_authentication/totp_verification_controller.rb @@ -24,7 +24,7 @@ def create if result.success? handle_valid_otp else - handle_invalid_otp + handle_invalid_otp(context: context, type: 'totp') end end diff --git a/app/presenters/two_factor_auth_code/max_attempts_reached_presenter.rb b/app/presenters/two_factor_auth_code/max_attempts_reached_presenter.rb index 718de9e35b3..e6e06fef4fe 100644 --- a/app/presenters/two_factor_auth_code/max_attempts_reached_presenter.rb +++ b/app/presenters/two_factor_auth_code/max_attempts_reached_presenter.rb @@ -19,6 +19,10 @@ def locked_reason t('two_factor_authentication.max_otp_login_attempts_reached') when 'otp_requests' t('two_factor_authentication.max_otp_requests_reached') + when 'totp_login_attempts' + t('two_factor_authentication.max_otp_login_attempts_reached') + when 'totp_requests' + t('two_factor_authentication.max_otp_requests_reached') when 'personal_key_login_attempts' t('two_factor_authentication.max_personal_key_login_attempts_reached') when 'piv_cac_login_attempts' diff --git a/app/services/irs_attempts_api/tracker_events.rb b/app/services/irs_attempts_api/tracker_events.rb index 9a3726fdb30..3cdf52c8883 100644 --- a/app/services/irs_attempts_api/tracker_events.rb +++ b/app/services/irs_attempts_api/tracker_events.rb @@ -97,6 +97,16 @@ def mfa_enroll_piv_cac( ) end + # @param [String] type - the type of multi-factor authentication used + # The user has exceeded the rate limit during enrollment + # and account has been locked + def mfa_enroll_rate_limited(type:) + track_event( + :mfa_enroll_rate_limited, + type: type, + ) + end + # Tracks when the user has attempted to enroll the TOTP MFA method to their account # @param [Boolean] success def mfa_enroll_totp(success:) @@ -158,8 +168,8 @@ def mfa_login_phone_otp_submitted(reauthentication:, success:) end # Tracks when the user has attempted to log in with the piv cac MFA method to their account - # @param [String] subject_dn # @param [Boolean] success + # @param [String] subject_dn # @param [Hash>] failure_reason def mfa_login_piv_cac( success:, @@ -174,6 +184,16 @@ def mfa_login_piv_cac( ) end + # @param [String] type - the type of multi-factor authentication used + # The user has exceeded the rate limit during verification + # and account has been locked + def mfa_login_rate_limited(type:) + track_event( + :mfa_login_rate_limited, + type: type, + ) + end + # Tracks when the user has attempted to log in with the TOTP MFA method to access their account # @param [Boolean] success def mfa_login_totp(success:) diff --git a/spec/controllers/two_factor_authentication/backup_code_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/backup_code_verification_controller_spec.rb index bf0c728934d..673f1867c2c 100644 --- a/spec/controllers/two_factor_authentication/backup_code_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/backup_code_verification_controller_spec.rb @@ -141,6 +141,10 @@ expect(@analytics).to receive(:track_event). with('Multi-Factor Authentication: max attempts reached') + + expect(@irs_attempts_api_tracker).to receive(:mfa_login_rate_limited). + with(type: 'backup_code') + expect(PushNotification::HttpPush).to receive(:deliver). with(PushNotification::MfaLimitAccountLockedEvent.new(user: subject.current_user)) diff --git a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb index a33a207b234..4906718d01d 100644 --- a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb @@ -179,6 +179,9 @@ expect(@irs_attempts_api_tracker).to receive(:mfa_login_phone_otp_submitted). with({ reauthentication: false, success: false }) + expect(@irs_attempts_api_tracker).to receive(:mfa_login_rate_limited). + with(type: 'otp') + post :create, params: { code: '12345', otp_delivery_preference: 'sms' } @@ -235,7 +238,7 @@ with('User marked authenticated', authentication_type: :valid_2fa) expect(@irs_attempts_api_tracker).to receive(:mfa_login_phone_otp_submitted). - with({ reauthentication: false, success: true }) + with(reauthentication: false, success: true) post :create, params: { code: subject.current_user.reload.direct_otp, @@ -380,7 +383,7 @@ controller.user_session[:phone_id] = phone_id expect(@irs_attempts_api_tracker).to receive(:mfa_enroll_phone_otp_submitted). - with({ success: true }) + with(success: true) post( :create, @@ -410,7 +413,7 @@ context 'user enters an invalid code' do before do expect(@irs_attempts_api_tracker).to receive(:mfa_enroll_phone_otp_submitted). - with({ success: false }) + with(success: false) post( :create, @@ -461,6 +464,19 @@ expect(@analytics).to have_received(:track_event). with('Multi-Factor Authentication Setup', properties) end + + context 'user has exceeded the maximum number of attempts' do + it 'tracks the attempt event' do + allow_any_instance_of(User).to receive(:max_login_attempts?).and_return(true) + sign_in_before_2fa + + stub_attempts_tracker + expect(@irs_attempts_api_tracker).to receive(:mfa_enroll_rate_limited). + with(type: 'otp') + + post :create, params: { code: '12345', otp_delivery_preference: 'sms' } + end + end end context 'user does not include a code parameter' do diff --git a/spec/controllers/two_factor_authentication/personal_key_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/personal_key_verification_controller_spec.rb index f1fa846d7a4..8e81fa14877 100644 --- a/spec/controllers/two_factor_authentication/personal_key_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/personal_key_verification_controller_spec.rb @@ -152,11 +152,16 @@ } stub_analytics + stub_attempts_tracker expect(@analytics).to receive(:track_mfa_submit_event). with(properties) expect(@analytics).to receive(:track_event). with('Multi-Factor Authentication: max attempts reached') + + expect(@irs_attempts_api_tracker).to receive(:mfa_login_rate_limited). + with(type: 'personal_key') + expect(PushNotification::HttpPush).to receive(:deliver). with(PushNotification::MfaLimitAccountLockedEvent.new(user: subject.current_user)) diff --git a/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb index d73e05c37a0..a0cb3ccbf6a 100644 --- a/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb @@ -192,6 +192,9 @@ expect(@analytics).to receive(:track_event). with('Multi-Factor Authentication: enter PIV CAC visited', attributes) + expect(@irs_attempts_api_tracker).to receive(:mfa_login_rate_limited). + with(type: 'piv_cac') + submit_attributes = { success: false, errors: { type: 'user.piv_cac_mismatch' }, diff --git a/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb index 78d6a68b3e9..4765db3cde2 100644 --- a/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb @@ -97,6 +97,10 @@ with('Multi-Factor Authentication: max attempts reached') expect(@irs_attempts_api_tracker).to receive(:track_event). with(:mfa_login_totp, success: false) + + expect(@irs_attempts_api_tracker).to receive(:mfa_login_rate_limited). + with(type: 'totp') + expect(PushNotification::HttpPush).to receive(:deliver). with(PushNotification::MfaLimitAccountLockedEvent.new(user: subject.current_user))