diff --git a/app/assets/stylesheets/email.css.scss b/app/assets/stylesheets/email.css.scss index 2015a1661d0..38d0dfc7a89 100644 --- a/app/assets/stylesheets/email.css.scss +++ b/app/assets/stylesheets/email.css.scss @@ -1,4 +1,7 @@ -@use 'uswds-core' as *; +@use 'uswds-core' as * with ( + $theme-table-border-color: 'base-lighter', + $theme-table-header-background-color: 'base-lightest' +); @use 'variables/app' as *; @use 'variables/email' as *; @@ -198,6 +201,10 @@ h6 { @include u-font('sans', 'md'); } +.font-family-mono { + font-family: monospace; +} + .margin-bottom-0 { @include u-margin-bottom(0); } @@ -269,3 +276,30 @@ h6 { @extend %usa-list-item; } } + +.usa-table { + @include usa-table; + + border-collapse: separate; + border-spacing: 0; + + tbody td { + border-top: 0; + } + + thead th:first-child { + border-top-left-radius: units(0.5); + } + + thead th:last-child { + border-top-right-radius: units(0.5); + } + + tbody tr:last-child td:first-child { + border-bottom-left-radius: units(0.5); + } + + tbody tr:last-child td:last-child { + border-bottom-right-radius: units(0.5); + } +} diff --git a/app/controllers/concerns/two_factor_authenticatable_methods.rb b/app/controllers/concerns/two_factor_authenticatable_methods.rb index b39536f9e65..633ab19be53 100644 --- a/app/controllers/concerns/two_factor_authenticatable_methods.rb +++ b/app/controllers/concerns/two_factor_authenticatable_methods.rb @@ -98,6 +98,10 @@ def handle_remember_device_preference(remember_device_preference) # You can pass in any "type" with a corresponding I18n key in # two_factor_authentication.invalid_#{type} def handle_invalid_otp(type:, context: nil) + if context == UserSessionContext::AUTHENTICATION_CONTEXT + handle_invalid_verification_for_authentication_context + end + update_invalid_user flash.now[:error] = invalid_otp_error(type) @@ -148,6 +152,10 @@ def update_invalid_user current_user.increment_second_factor_attempts_count! end + def handle_invalid_verification_for_authentication_context + create_user_event(:sign_in_unsuccessful_2fa) + end + def handle_valid_verification_for_confirmation_context(auth_method:) mark_user_session_authenticated(auth_method:, authentication_type: :valid_2fa_confirmation) reset_second_factor_attempts_count 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 d7818e091e1..72835132906 100644 --- a/app/controllers/two_factor_authentication/backup_code_verification_controller.rb +++ b/app/controllers/two_factor_authentication/backup_code_verification_controller.rb @@ -50,6 +50,7 @@ def presenter_for_two_factor_authentication_method end def handle_invalid_backup_code + handle_invalid_verification_for_authentication_context update_invalid_user flash.now[:error] = t('two_factor_authentication.invalid_backup_code') diff --git a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb index f4cd119bc25..066fd0cb46f 100644 --- a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb +++ b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb @@ -68,6 +68,7 @@ def handle_valid_webauthn end def handle_invalid_webauthn(result) + handle_invalid_verification_for_authentication_context flash[:error] = result.first_error_message if platform_authenticator? diff --git a/app/controllers/users/piv_cac_login_controller.rb b/app/controllers/users/piv_cac_login_controller.rb index 9e9b542ef16..5765154dad7 100644 --- a/app/controllers/users/piv_cac_login_controller.rb +++ b/app/controllers/users/piv_cac_login_controller.rb @@ -89,6 +89,7 @@ def next_step end def process_invalid_submission + handle_invalid_verification_for_authentication_context session[:needs_to_setup_piv_cac_after_sign_in] = true if piv_cac_login_form.valid_token? process_token_with_error diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 19ff2a46794..215e8785f8f 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -135,6 +135,37 @@ def new_device_sign_in(date:, location:, device_name:, disavowal_token:) end end + # @param [Array] events Array of sign-in Event records (event types "sign_in_before_2fa", + # "sign_in_after_2fa", "sign_in_unsuccessful_2fa") + # @param [String] disavowal_token Token to generate URL for disavowing event + def new_device_sign_in_after_2fa(events:, disavowal_token:) + with_user_locale(user) do + @events = events + @disavowal_token = disavowal_token + + mail( + to: email_address.email, + subject: t('user_mailer.new_device_sign_in_after_2fa.subject', app_name: APP_NAME), + ) + end + end + + # @param [Array] events Array of sign-in Event records (event types "sign_in_before_2fa", + # "sign_in_after_2fa", "sign_in_unsuccessful_2fa") + # @param [String] disavowal_token Token to generate URL for disavowing event + def new_device_sign_in_before_2fa(events:, disavowal_token:) + with_user_locale(user) do + @events = events + @disavowal_token = disavowal_token + @failed_times = events.count { |event| event.event_type == 'sign_in_unsuccessful_2fa' } + + mail( + to: email_address.email, + subject: t('user_mailer.new_device_sign_in_before_2fa.subject', app_name: APP_NAME), + ) + end + end + def personal_key_regenerated with_user_locale(user) do mail(to: email_address.email, subject: t('user_mailer.personal_key_regenerated.subject')) diff --git a/app/models/event.rb b/app/models/event.rb index 43e2e22ca89..dbbc65465bd 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -25,6 +25,7 @@ class Event < ApplicationRecord email_deleted: 20, phone_added: 21, password_invalidated: 22, + sign_in_unsuccessful_2fa: 23, } validates :event_type, presence: true diff --git a/app/services/marketing_site.rb b/app/services/marketing_site.rb index 42b9c7be9bf..b3eb7655839 100644 --- a/app/services/marketing_site.rb +++ b/app/services/marketing_site.rb @@ -7,6 +7,7 @@ class UnknownArticleException < StandardError; end HELP_CENTER_ARTICLES = %w[ get-started/authentication-options + manage-your-account/add-or-change-your-authentication-method manage-your-account/personal-key trouble-signing-in/face-or-touch-unlock verify-your-identity/accepted-identification-documents diff --git a/app/views/user_mailer/new_device_sign_in_after_2fa.html.erb b/app/views/user_mailer/new_device_sign_in_after_2fa.html.erb new file mode 100644 index 00000000000..a1d9f81298a --- /dev/null +++ b/app/views/user_mailer/new_device_sign_in_after_2fa.html.erb @@ -0,0 +1,26 @@ +

+ <%= t('user_mailer.new_device_sign_in_after_2fa.info_p1', app_name: APP_NAME) %> +

+ +

+ <%= t('user_mailer.new_device_sign_in_after_2fa.info_p2') %> +

+ +

+ <%= t( + 'user_mailer.new_device_sign_in_after_2fa.info_p3_html', + reset_password_link_html: link_to( + t('user_mailer.new_device_sign_in_after_2fa.reset_password'), + event_disavowal_url(disavowal_token: @disavowal_token), + ), + authentication_methods_link_html: link_to( + t('user_mailer.new_device_sign_in_after_2fa.authentication_methods'), + MarketingSite.help_center_article_url( + category: 'manage-your-account', + article: 'add-or-change-your-authentication-method', + ), + ), + ) %> +

+ +<%= render 'user_mailer/shared/new_device_sign_in_attempts' %> diff --git a/app/views/user_mailer/new_device_sign_in_before_2fa.html.erb b/app/views/user_mailer/new_device_sign_in_before_2fa.html.erb new file mode 100644 index 00000000000..e7f59bcd4f4 --- /dev/null +++ b/app/views/user_mailer/new_device_sign_in_before_2fa.html.erb @@ -0,0 +1,19 @@ +

+ <%= t('user_mailer.new_device_sign_in_before_2fa.info_p1_html', count: @failed_times, app_name: APP_NAME) %> +

+ +

+ <%= t('user_mailer.new_device_sign_in_before_2fa.info_p2') %> +

+ +

+ <%= t( + 'user_mailer.new_device_sign_in_before_2fa.info_p3_html', + reset_password_link_html: link_to( + t('user_mailer.new_device_sign_in_before_2fa.reset_password'), + event_disavowal_url(disavowal_token: @disavowal_token), + ), + ) %> +

+ +<%= render 'user_mailer/shared/new_device_sign_in_attempts' %> diff --git a/app/views/user_mailer/shared/_new_device_sign_in_attempts.html.erb b/app/views/user_mailer/shared/_new_device_sign_in_attempts.html.erb new file mode 100644 index 00000000000..09a5fb83791 --- /dev/null +++ b/app/views/user_mailer/shared/_new_device_sign_in_attempts.html.erb @@ -0,0 +1,26 @@ +<% @events.group_by { |event| IpGeocoder.new(event.device.last_ip).location }.each do |location, events| %> +
+ + + + + + + + <% events.each do |event| %> + + + + <% end %> + +
+ <%= t('user_mailer.new_device_sign_in_attempts.new_sign_in_from', location:) %> +
+ <%# i18n-tasks-use t('user_mailer.new_device_sign_in_attempts.events.sign_in_after_2fa') %> + <%# i18n-tasks-use t('user_mailer.new_device_sign_in_attempts.events.sign_in_before_2fa') %> + <%# i18n-tasks-use t('user_mailer.new_device_sign_in_attempts.events.sign_in_unsuccessful_2fa') %> + <%= t(event.event_type, scope: [:user_mailer, :new_device_sign_in_attempts, :events]) %>
+ <%= EasternTimePresenter.new(event.created_at) %> +
+
+<% end %> diff --git a/config/locales/event_types/en.yml b/config/locales/event_types/en.yml index d56e1d17811..cfa6adea444 100644 --- a/config/locales/event_types/en.yml +++ b/config/locales/event_types/en.yml @@ -24,5 +24,6 @@ en: piv_cac_enabled: PIV/CAC card associated sign_in_after_2fa: Signed in with second factor sign_in_before_2fa: Signed in with password + sign_in_unsuccessful_2fa: Failed to authenticate webauthn_key_added: Hardware security key added webauthn_key_removed: Hardware security key removed diff --git a/config/locales/event_types/es.yml b/config/locales/event_types/es.yml index 628e54848fa..727f2581fd8 100644 --- a/config/locales/event_types/es.yml +++ b/config/locales/event_types/es.yml @@ -24,5 +24,6 @@ es: piv_cac_enabled: Tarjeta PIV/CAC asociada sign_in_after_2fa: Inicia sesión con segundo factor sign_in_before_2fa: Inicia sesión con contraseña + sign_in_unsuccessful_2fa: Error al autenticar webauthn_key_added: Clave de seguridad de hardware añadido webauthn_key_removed: Clave de seguridad de hardware eliminada diff --git a/config/locales/event_types/fr.yml b/config/locales/event_types/fr.yml index 3eca7777284..568c692b19b 100644 --- a/config/locales/event_types/fr.yml +++ b/config/locales/event_types/fr.yml @@ -24,5 +24,6 @@ fr: piv_cac_enabled: Carte PIV/CAC associée sign_in_after_2fa: Signé avec deuxième facteur sign_in_before_2fa: Connecté avec mot de passe + sign_in_unsuccessful_2fa: Échec de l’authentification webauthn_key_added: Clé de sécurité ajoutée webauthn_key_removed: Clé de sécurité retirée diff --git a/config/locales/user_mailer/en.yml b/config/locales/user_mailer/en.yml index 41429550033..8e1b12bb688 100644 --- a/config/locales/user_mailer/en.yml +++ b/config/locales/user_mailer/en.yml @@ -218,6 +218,32 @@ en: %{contact_link_html}. info: 'Your %{app_name} account was just used to sign in on a new device.' subject: New sign-in with your %{app_name} account + new_device_sign_in_after_2fa: + authentication_methods: authentication methods + info_p1: Your %{app_name} email and password were used to sign-in and + authenticate on a new device. + info_p2: If you recognize this activity, you don’t need to do anything. + info_p3_html: If this wasn’t you, %{reset_password_link_html} and change your + %{authentication_methods_link_html} immediately. + reset_password: reset your password + subject: New sign-in and authentication with your %{app_name} account + new_device_sign_in_attempts: + events: + sign_in_after_2fa: Authenticated + sign_in_before_2fa: Signed in with password + sign_in_unsuccessful_2fa: Failed to authenticate + new_sign_in_from: New sign-in potentially located in %{location} + new_device_sign_in_before_2fa: + info_p1_html: + one: Your %{app_name} email and password were used to sign in from a new device + but failed to authenticate. + other: Your %{app_name} email and password were used to sign in from a new + device but failed to authenticate %{count} times + info_p2: If you recognize this activity, you don’t need to do anything. + info_p3_html: Two-factor authentication protects your account from unauthorized + access. If this wasn’t you, %{reset_password_link_html} immediately. + reset_password: reset your password + subject: New sign-in with your %{app_name} account password_changed: disavowal_link: reset your password help_html: If you did not make this change, %{disavowal_link_html}. For more diff --git a/config/locales/user_mailer/es.yml b/config/locales/user_mailer/es.yml index e71f33c9287..17f14995f53 100644 --- a/config/locales/user_mailer/es.yml +++ b/config/locales/user_mailer/es.yml @@ -232,6 +232,35 @@ es: visite el %{app_name_html} %{help_link_html} o el %{contact_link_html}. info: 'Su cuenta %{app_name} acaba de iniciar sesión en un nuevo dispositivo.' subject: Nuevo initio de sesion con su %{app_name} cuenta + new_device_sign_in_after_2fa: + authentication_methods: métodos de autenticación + info_p1: Su correo electrónico y su contraseña de %{app_name} se usaron para + iniciar sesión y para la autenticación desde un nuevo dispositivo. + info_p2: Si reconoce esta actividad, no tiene que hacer nada. + info_p3_html: Si no fue usted, %{reset_password_link_html} y cambie sus + %{authentication_methods_link_html} inmediatamente. + reset_password: restablezca la contraseña + subject: Nuevo inicio de sesión y autenticación con su cuenta de %{app_name} + new_device_sign_in_attempts: + events: + sign_in_after_2fa: Autenticado + sign_in_before_2fa: Inicia sesión con contraseña + sign_in_unsuccessful_2fa: Error al autenticar + new_sign_in_from: Nuevo inicio de sesión potencialmente ubicado en %{location} + new_device_sign_in_before_2fa: + info_p1_html: + one: Su correo electrónico y su contraseña de %{app_name} se usaron para + ingresar desde un nuevo dispositivo, pero la autenticación dio + error. + other: Su correo electrónico y su contraseña de %{app_name} se usaron para + ingresar desde un nuevo dispositivo, pero error al autenticar + %{count} veces + info_p2: Si reconoce esta actividad, no tiene que hacer nada. + info_p3_html: La autenticación de dos factores protege su cuenta de accesos no + autorizados. Si no fue usted, %{reset_password_link_html} + inmediatamente. + reset_password: restablezca la contraseña + subject: Nuevo inicio de sesión con su cuenta de %{app_name} password_changed: disavowal_link: restablecer su contraseña help_html: Si no realizó este cambio, %{disavowal_link_html}. Para más ayuda, diff --git a/config/locales/user_mailer/fr.yml b/config/locales/user_mailer/fr.yml index cf220fa3213..9c6205723df 100644 --- a/config/locales/user_mailer/fr.yml +++ b/config/locales/user_mailer/fr.yml @@ -239,6 +239,35 @@ fr: %{app_name_html} ou %{contact_link_html}. info: 'Votre compte %{app_name} a été connecté sur un nouvel appareil.' subject: Nouvelle connexion avec votre compte %{app_name} + new_device_sign_in_after_2fa: + authentication_methods: méthodes d’authentification + info_p1: Votre adresse e-mail et votre mot de passe %{app_name} ont été utilisés + pour se connecter et s’authentifier sur un nouvel appareil. + info_p2: Si vous reconnaissez cette activité, vous n’avez rien à faire. + info_p3_html: Si ce n’est pas vous, %{reset_password_link_html} et modifiez + immédiatement vos %{authentication_methods_link_html}. + reset_password: réinitialisez votre mot de passe + subject: Nouvelle connexion et authentification avec votre compte %{app_name} + new_device_sign_in_attempts: + events: + sign_in_after_2fa: Signé avec deuxième facteur + sign_in_before_2fa: Connecté avec mot de passe + sign_in_unsuccessful_2fa: Échec de l’authentification + new_sign_in_from: Nouvelle connexion potentiellement située à %{location} + new_device_sign_in_before_2fa: + info_p1_html: + one: Votre adresse électronique et votre mot de passe %{app_name} ont été + utilisés pour vous connecter à partir d’un nouvel appareil, mais + l’authentification a échoué. + other: Votre adresse électronique et votre mot de passe %{app_name} ont été + utilisés pour vous connecter à partir d’un nouvel appareil, mais + l’authentification a échoué %{count} reprises. + info_p2: Si vous reconnaissez cette activité, vous n’avez rien à faire. + info_p3_html: L’authentification à deux facteurs protège votre compte contre + tout accès non autorisé. Si ce n’est pas vous, + %{reset_password_link_html}. + reset_password: réinitialisez immédiatement votre mot de passe + subject: Nouvelle connexion avec votre compte %{app_name} password_changed: disavowal_link: réinitialiser votre mot de passe help_html: Si vous n’avez pas effectué ce changement, %{disavowal_link_html}. 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 8df4dc200b2..5d353308aff 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 @@ -213,6 +213,12 @@ post :create, params: payload end + + it 'records unsuccessful 2fa event' do + expect(controller).to receive(:create_user_event).with(:sign_in_unsuccessful_2fa) + + post :create, params: payload + end end end end 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 380284b7f4a..56c9d199f4e 100644 --- a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb @@ -122,8 +122,10 @@ end describe '#create' do - let(:parsed_phone) { Phonelib.parse(subject.current_user.default_phone_configuration.phone) } + let(:parsed_phone) { Phonelib.parse(controller.current_user.default_phone_configuration.phone) } context 'when the user enters an invalid OTP during authentication context' do + subject(:response) { post :create, params: { code: '12345', otp_delivery_preference: 'sms' } } + before do sign_in_before_2fa controller.user_session[:mfa_selections] = ['sms'] @@ -155,13 +157,11 @@ expect(@irs_attempts_api_tracker).to receive(:mfa_login_phone_otp_submitted). with({ reauthentication: false, success: false }) - - post :create, params: - { code: '12345', - otp_delivery_preference: 'sms' } end it 'increments second_factor_attempts_count' do + response + expect(controller.current_user.reload.second_factor_attempts_count).to eq 1 end @@ -170,13 +170,23 @@ end it 'displays flash error message' do + response + expect(flash[:error]).to eq t('two_factor_authentication.invalid_otp') end it 'does not set auth_method and requires 2FA' do + response + expect(controller.user_session[:auth_events]).to eq nil expect(controller.user_session[TwoFactorAuthenticatable::NEED_AUTHENTICATION]).to eq true end + + it 'records unsuccessful 2fa event' do + expect(controller).to receive(:create_user_event).with(:sign_in_unsuccessful_2fa) + + response + end end context 'when the user enters an invalid OTP during reauthentication context' 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 9ec58b054c0..a10da783e3f 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 @@ -233,6 +233,12 @@ post :create, params: payload end + + it 'records unsuccessful 2fa event' do + expect(controller).to receive(:create_user_event).with(:sign_in_unsuccessful_2fa) + + post :create, params: payload + end end it 'does not generate a new personal key if the user enters an invalid key' do 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 a57eba0bd01..9612e4f5a08 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 @@ -19,7 +19,7 @@ before(:each) do session_info = { piv_cac_nonce: nonce } - allow(subject).to receive(:user_session).and_return(session_info) + allow(controller).to receive(:user_session).and_return(session_info) allow(PivCacService).to receive(:decode_token).with('good-token').and_return( 'uuid' => user.piv_cac_configurations.first.x509_dn_uuid, 'subject' => x509_subject, @@ -163,14 +163,16 @@ end context 'when the user presents an invalid piv/cac' do + subject(:response) { get :show, params: { token: 'bad-token' } } + before do stub_sign_in_before_2fa(user) - - get :show, params: { token: 'bad-token' } end it 'increments second_factor_attempts_count' do - expect(subject.current_user.reload.second_factor_attempts_count).to eq 1 + response + + expect(controller.current_user.reload.second_factor_attempts_count).to eq 1 end it 'redirects to the piv/cac entry screen' do @@ -178,16 +180,28 @@ end it 'displays flash error message' do + response + expect(flash[:error]).to eq t('two_factor_authentication.invalid_piv_cac') end it 'resets the piv/cac session information' do - expect(subject.user_session[:decrypted_x509]).to be_nil + response + + expect(controller.user_session[:decrypted_x509]).to be_nil end it 'does not set auth_method and requires 2FA' do - expect(subject.user_session[:auth_events]).to eq nil - expect(subject.user_session[TwoFactorAuthenticatable::NEED_AUTHENTICATION]).to eq true + response + + expect(controller.user_session[:auth_events]).to eq nil + expect(controller.user_session[TwoFactorAuthenticatable::NEED_AUTHENTICATION]).to eq true + end + + it 'records unsuccessful 2fa event' do + expect(controller).to receive(:create_user_event).with(:sign_in_unsuccessful_2fa) + + response end end 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 bfe42ca4446..ea71d27e16e 100644 --- a/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb @@ -111,16 +111,19 @@ end context 'when the user enters an invalid TOTP' do + subject(:response) { post :create, params: { code: 'abc' } } + before do sign_in_before_2fa - user = subject.current_user + user = controller.current_user @secret = user.generate_totp_secret Db::AuthAppConfiguration.create(user, @secret, nil, 'foo') - post :create, params: { code: 'abc' } end it 'increments second_factor_attempts_count' do - expect(subject.current_user.reload.second_factor_attempts_count).to eq 1 + response + + expect(controller.current_user.reload.second_factor_attempts_count).to eq 1 end it 're-renders the TOTP entry screen' do @@ -128,12 +131,22 @@ end it 'displays flash error message' do + response + expect(flash[:error]).to eq t('two_factor_authentication.invalid_otp') end it 'does not set auth_method and still requires 2FA' do - expect(subject.user_session[:auth_events]).to eq nil - expect(subject.user_session[TwoFactorAuthenticatable::NEED_AUTHENTICATION]).to eq true + response + + expect(controller.user_session[:auth_events]).to eq nil + expect(controller.user_session[TwoFactorAuthenticatable::NEED_AUTHENTICATION]).to eq true + end + + it 'records unsuccessful 2fa event' do + expect(controller).to receive(:create_user_event).with(:sign_in_unsuccessful_2fa) + + response end end 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 668b48b6eb2..4e0940a508d 100644 --- a/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb @@ -266,6 +266,7 @@ new_device: nil } expect(@analytics).to receive(:track_mfa_submit_event). with(result) + expect(controller).to receive(:create_user_event).with(:sign_in_unsuccessful_2fa) patch :confirm, params: params end diff --git a/spec/controllers/users/piv_cac_login_controller_spec.rb b/spec/controllers/users/piv_cac_login_controller_spec.rb index 4167e3b92a0..9ed2668a552 100644 --- a/spec/controllers/users/piv_cac_login_controller_spec.rb +++ b/spec/controllers/users/piv_cac_login_controller_spec.rb @@ -22,8 +22,11 @@ let(:token) { 'TEST:abcdefg' } context 'an invalid token' do - before { get :new, params: { token: token } } + subject(:response) { get :new, params: { token: token } } + it 'tracks the login attempt' do + response + expect(@analytics).to have_logged_event( :piv_cac_login, errors: {}, @@ -35,6 +38,12 @@ it 'redirects to the error url' do expect(response).to redirect_to(login_piv_cac_error_url(error: 'token.bad')) end + + it 'records unsuccessful 2fa event' do + expect(controller).to receive(:create_user_event).with(:sign_in_unsuccessful_2fa) + + response + end end context 'with a valid token' do diff --git a/spec/mailers/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb index c39f6e1b2b7..7c1ad4a4a2b 100644 --- a/spec/mailers/previews/user_mailer_preview.rb +++ b/spec/mailers/previews/user_mailer_preview.rb @@ -64,6 +64,56 @@ def new_device_sign_in ) end + def new_device_sign_in_after_2fa + UserMailer.with(user: user, email_address: email_address_record).new_device_sign_in_after_2fa( + events: [ + unsaveable( + Event.new( + event_type: :sign_in_before_2fa, + created_at: Time.zone.now - 2.minutes, + user:, + device: user.devices.first, + ), + ), + unsaveable( + Event.new( + event_type: :sign_in_after_2fa, + created_at: Time.zone.now, + user:, + device: user.devices.first, + ), + ), + ], + disavowal_token: SecureRandom.hex, + ) + end + + def new_device_sign_in_before_2fa + UserMailer.with(user: user, email_address: email_address_record).new_device_sign_in_before_2fa( + events: [ + unsaveable( + Event.new( + event_type: :sign_in_before_2fa, + created_at: Time.zone.now - 2.minutes, + user:, + device: user.devices.first, + ), + ), + *Array.new((params['failed_times'] || 1).to_i) do + unsaveable( + Event.new( + event_type: :sign_in_unsuccessful_2fa, + created_at: Time.zone.now, + user:, + device: user.devices.first, + ), + ) + end, + ], + disavowal_token: SecureRandom.hex, + ) + end + def personal_key_regenerated UserMailer.with(user: user, email_address: email_address_record).personal_key_regenerated end @@ -222,7 +272,19 @@ def account_reinstated private def user - unsaveable(User.new(email_addresses: [email_address_record])) + @user ||= unsaveable( + User.new( + email_addresses: [email_address_record], + devices: [ + unsaveable( + Device.new( + user_agent: Faker::Internet.user_agent, + last_ip: Faker::Internet.ip_v4_address, + ), + ), + ], + ), + ) end def user_with_pending_gpo_letter