Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,12 @@ def authenticate_user
def handle_second_factor_locked_user(type:, context: nil)
analytics.multi_factor_auth_max_attempts

if context && UserSessionContext.confirmation_context?(context)
attempts_api_tracker.mfa_enroll_code_rate_limited(mfa_device_type: type)
if context
if UserSessionContext.confirmation_context?(context)
attempts_api_tracker.mfa_enroll_code_rate_limited(mfa_device_type: type)
elsif UserSessionContext.authentication_context?(context)
attempts_api_tracker.mfa_submission_code_rate_limited(mfa_device_type: type)
end
end

event = PushNotification::MfaLimitAccountLockedEvent.new(user: current_user)
Expand Down
2 changes: 2 additions & 0 deletions app/controllers/users/delete_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def delete
notify_user_via_email_of_deletion
notify_user_via_sms_of_deletion
analytics.account_delete_submitted(success: true)
attempts_api_tracker.logged_in_account_purged(success: true)
delete_user
sign_out
flash[:success] = t('devise.registrations.destroyed')
Expand All @@ -37,6 +38,7 @@ def confirm_current_password

flash.now[:error] = t('idv.errors.incorrect_password')
analytics.account_delete_submitted(success: false)
attempts_api_tracker.logged_in_account_purged(success: false)
render :show
end

Expand Down
4 changes: 4 additions & 0 deletions app/controllers/users/passwords_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ def update
)

result = @update_user_password_form.submit(user_password_params)
attempts_api_tracker.logged_in_password_change(
success: result.success?,
failure_reason: attempts_api_tracker.parse_failure_reason(result),
)

analytics.password_changed(**result)

Expand Down
10 changes: 9 additions & 1 deletion app/services/attempts_api/tracker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,15 @@ def track_event(event_type, metadata = {})
end

def parse_failure_reason(result)
return result.to_h[:error_details] || result.errors.presence
errors = result.to_h[:error_details]

if errors.present?
parsed_errors = errors.keys.index_with do |k|
errors[k].keys
end
end

parsed_errors || result.errors.presence
end

private
Expand Down
31 changes: 31 additions & 0 deletions app/services/attempts_api/tracker_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,26 @@ def email_and_password_auth(success:)
)
end

# @param [Boolean] success True if account successfully deleted
# A User deletes their Login.gov account
def logged_in_account_purged(success:)
track_event(
'logged-in-account-purged',
success:,
)
end

# @param [Boolean] success True if the password was successfully changed
# @param [Hash<Symbol,Array<Symbol>>] failure_reason if password was not successfully changed
# A logged-in user has attempted to change their password
def logged_in_password_change(success:, failure_reason: nil)
track_event(
:logged_in_password_change,
success: success,
failure_reason:,
)
end

# @param [Boolean] success
# A user has attempted to enroll the Backup Codes MFA method to their account
def mfa_enroll_backup_code(success:)
Expand All @@ -29,6 +49,7 @@ def mfa_enroll_totp(success:)
)
end

# Tracks when user submits registration password
# @param [String<'backup_code', 'otp', 'piv_cac', 'totp'>] mfa_device_type
# The user has exceeded the rate limit during enrollment
# and account has been locked
Expand All @@ -39,6 +60,16 @@ def mfa_enroll_code_rate_limited(mfa_device_type:)
)
end

# @param [String<'backup_code', 'otp', 'piv_cac', 'totp'>] mfa_device_type
# The user has exceeded the rate limit during verification
# and account has been locked
def mfa_submission_code_rate_limited(mfa_device_type:)
track_event(
'mfa-submission-code-rate-limited',
mfa_device_type:,
)
end

# @param [Boolean] success
# @param [Hash<Symbol,Array<Symbol>>] failure_reason
# Tracks when user submits registration password
Expand Down
4 changes: 2 additions & 2 deletions docs/attempts-api/schemas/events/SignInEvents.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ properties:
$ref: './sign-in/MfaLoginWebauthnPlatformSubmitted.yml'
mfa-login-webauthn-roaming-submitted:
$ref: './sign-in/MfaLoginWebauthnRoamingSubmitted.yml'
mfa-submitted-rate-limited:
$ref: './sign-in/MfaSubmissionRateLimited.yml'
mfa-submission-code-rate-limited:
$ref: './sign-in/MfaSubmissionCodeRateLimited.yml'
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ allOf:
enum:
- pwned
- too_short
password_confirmation:
type: array
description: Errors that explain the failure
items:
type: string
enum:
- pwned
- too_short
- mismatch
success:
type: boolean
description: |
Expand Down
12 changes: 10 additions & 2 deletions spec/controllers/sign_up/passwords_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@
password_confirmation: { too_short: true },
}
end
let(:attempts_failure_reason) do
{
password: [:too_short],
password_confirmation: [:too_short],
}
end

it 'tracks an invalid password event' do
subject
Expand All @@ -135,7 +141,7 @@
it 'creates attempts event' do
expect(@attempts_api_tracker).to receive(:user_registration_password_submitted).with(
success: false,
failure_reason: error_details,
failure_reason: attempts_failure_reason,
)
subject
end
Expand All @@ -150,6 +156,8 @@
}
end

let(:failure_reason) { { password_confirmation: [:mismatch] } }

it 'tracks invalid password_confirmation error' do
subject

Expand All @@ -165,7 +173,7 @@
it 'creates attempts event' do
expect(@attempts_api_tracker).to receive(:user_registration_password_submitted).with(
success: false,
failure_reason: error_details,
failure_reason:,
)
subject
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@

context 'with authentication context' do
it 'tracks the max attempts event' do
expect(@attempts_api_tracker).to receive(:mfa_submission_code_rate_limited).with(
mfa_device_type: 'backup_code',
)

expect(PushNotification::HttpPush).to receive(:deliver)
.with(PushNotification::MfaLimitAccountLockedEvent.new(user: subject.current_user))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,10 @@

context 'with authentication context' do
it 'tracks the event' do
expect(@attempts_api_tracker).to receive(:mfa_submission_code_rate_limited).with(
mfa_device_type: 'otp',
)

expect(PushNotification::HttpPush).to receive(:deliver)
.with(PushNotification::MfaLimitAccountLockedEvent.new(user: controller.current_user))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@

context 'with authentication context' do
it 'tracks the max attempts event' do
expect(@attempts_api_tracker).to receive(:mfa_submission_code_rate_limited).with(
mfa_device_type: 'personal_key',
)

expect(PushNotification::HttpPush).to receive(:deliver)
.with(PushNotification::MfaLimitAccountLockedEvent.new(user: subject.current_user))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,10 @@

context 'with authentication context' do
it 'tracks the event' do
expect(@attempts_api_tracker).to receive(:mfa_submission_code_rate_limited).with(
mfa_device_type: 'piv_cac',
)

expect(PushNotification::HttpPush).to receive(:deliver)
.with(PushNotification::MfaLimitAccountLockedEvent.new(user: subject.current_user))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@

context 'with authentication context' do
it 'tracks the event' do
expect(@attempts_api_tracker).to receive(:mfa_submission_code_rate_limited).with(
mfa_device_type: 'totp',
)

expect(PushNotification::HttpPush).to receive(:deliver)
.with(PushNotification::MfaLimitAccountLockedEvent.new(user: subject.current_user))

Expand Down
4 changes: 4 additions & 0 deletions spec/controllers/users/delete_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
it 'logs a failed submit' do
user = stub_signed_in_user
stub_analytics(user:)
stub_attempts_tracker
expect(@attempts_api_tracker).to receive(:logged_in_account_purged).with(success: false)

delete

Expand Down Expand Up @@ -82,6 +84,8 @@
it 'logs a succesful submit' do
user = stub_signed_in_user
stub_analytics(user:)
stub_attempts_tracker
expect(@attempts_api_tracker).to receive(:logged_in_account_purged).with(success: true)

delete

Expand Down
Loading