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
36 changes: 35 additions & 1 deletion app/assets/stylesheets/email.css.scss
Original file line number Diff line number Diff line change
@@ -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 *;

Expand Down Expand Up @@ -198,6 +201,10 @@ h6 {
@include u-font('sans', 'md');
}

.font-family-mono {
font-family: monospace;
}

.margin-bottom-0 {
@include u-margin-bottom(0);
}
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
1 change: 1 addition & 0 deletions app/controllers/users/piv_cac_login_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions app/mailers/user_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,37 @@ def new_device_sign_in(date:, location:, device_name:, disavowal_token:)
end
end

# @param [Array<Hash>] 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<Hash>] 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'))
Expand Down
1 change: 1 addition & 0 deletions app/models/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions app/services/marketing_site.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions app/views/user_mailer/new_device_sign_in_after_2fa.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<p class="margin-bottom-2">
<%= t('user_mailer.new_device_sign_in_after_2fa.info_p1', app_name: APP_NAME) %>
</p>

<p class="margin-bottom-2">
<%= t('user_mailer.new_device_sign_in_after_2fa.info_p2') %>
</p>

<p>
<%= 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',
),
),
) %>
</p>

<%= render 'user_mailer/shared/new_device_sign_in_attempts' %>
19 changes: 19 additions & 0 deletions app/views/user_mailer/new_device_sign_in_before_2fa.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<p class="margin-bottom-2">
<%= t('user_mailer.new_device_sign_in_before_2fa.info_p1_html', count: @failed_times, app_name: APP_NAME) %>
</p>

<p class="margin-bottom-2">
<%= t('user_mailer.new_device_sign_in_before_2fa.info_p2') %>
</p>

<p>
<%= 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),
),
) %>
</p>

<%= render 'user_mailer/shared/new_device_sign_in_attempts' %>
26 changes: 26 additions & 0 deletions app/views/user_mailer/shared/_new_device_sign_in_attempts.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<% @events.group_by { |event| IpGeocoder.new(event.device.last_ip).location }.each do |location, events| %>
<div class="margin-top-5">
<table class="usa-table">
<thead>
<tr>
<th scope="col" class="font-family-mono">
<%= t('user_mailer.new_device_sign_in_attempts.new_sign_in_from', location:) %>
</th>
</tr>
</thead>
<tbody>
<% events.each do |event| %>
<tr>
<td class="font-family-mono">
<%# 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]) %><br />
<%= EasternTimePresenter.new(event.created_at) %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
1 change: 1 addition & 0 deletions config/locales/event_types/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions config/locales/event_types/es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions config/locales/event_types/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
26 changes: 26 additions & 0 deletions config/locales/user_mailer/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <strong>failed to authenticate</strong>.
other: Your %{app_name} email and password were used to sign in from a new
device but <strong>failed to authenticate %{count} times</strong>
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
Expand Down
29 changes: 29 additions & 0 deletions config/locales/user_mailer/es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <strong>la autenticación dio
error</strong>.
other: Su correo electrónico y su contraseña de %{app_name} se usaron para
ingresar desde un nuevo dispositivo, pero <strong>error al autenticar
%{count} veces</strong>
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,
Expand Down
29 changes: 29 additions & 0 deletions config/locales/user_mailer/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
<strong>l’authentification a échoué</strong>.
other: Votre adresse électronique et votre mot de passe %{app_name} ont été
utilisés pour vous connecter à partir d’un nouvel appareil, mais
<strong>l’authentification a échoué %{count} reprises</strong>.
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}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading