diff --git a/app/controllers/mfa_confirmation_controller.rb b/app/controllers/mfa_confirmation_controller.rb
index a5f21c3ccef..8cff30851c6 100644
--- a/app/controllers/mfa_confirmation_controller.rb
+++ b/app/controllers/mfa_confirmation_controller.rb
@@ -17,7 +17,7 @@ def skip
pii_like_keypaths: [[:mfa_method_counts, :phone]],
success: true,
)
- redirect_to after_mfa_setup_path
+ redirect_to after_skip_path
end
def new
@@ -74,4 +74,16 @@ def handle_max_password_attempts_reached
def mfa_context
@mfa_context ||= MfaContext.new(current_user)
end
+
+ def after_skip_path
+ if backup_code_confirmation_needed?
+ confirm_backup_codes_path
+ else
+ after_mfa_setup_path
+ end
+ end
+
+ def backup_code_confirmation_needed?
+ !MfaPolicy.new(current_user).multiple_factors_enabled? && user_backup_codes_configured?
+ end
end
diff --git a/app/controllers/users/backup_code_setup_controller.rb b/app/controllers/users/backup_code_setup_controller.rb
index 5ce1e0bb794..c75c18dcb19 100644
--- a/app/controllers/users/backup_code_setup_controller.rb
+++ b/app/controllers/users/backup_code_setup_controller.rb
@@ -60,6 +60,8 @@ def reminder
flash.now[:success] = t('notices.authenticated_successfully')
end
+ def confirm_backup_codes; end
+
private
def track_backup_codes_created
diff --git a/app/views/users/backup_code_setup/confirm_backup_codes.html.erb b/app/views/users/backup_code_setup/confirm_backup_codes.html.erb
new file mode 100644
index 00000000000..417a159c234
--- /dev/null
+++ b/app/views/users/backup_code_setup/confirm_backup_codes.html.erb
@@ -0,0 +1,37 @@
+<% title t('titles.backup_codes') %>
+
+<%= render PageHeadingComponent.new.with_content(t('titles.backup_codes')) %>
+
+
+ <%= t('two_factor_authentication.backup_codes.warning_html') %>
+
+
+
+ <%= t('two_factor_authentication.backup_codes.instructions', app_name: APP_NAME) %>
+
+
+
+
+ <%= render ButtonComponent.new(
+ action: ->(**tag_options, &block) { link_to(sign_up_completed_path, **tag_options, &block) },
+ big: true,
+ full_width: true,
+ class: 'margin-bottom-205',
+ ).with_content(t('two_factor_authentication.backup_codes.saved_backup_codes')) %>
+
+
+
+
+ <%= render ButtonComponent.new(
+ action: ->(**tag_options, &block) { link_to(backup_code_regenerate_path, **tag_options, &block) },
+ big: true,
+ full_width: true,
+ outline: true,
+ ).with_content(t('two_factor_authentication.backup_codes.new_backup_codes')) %>
+
+
+
+<%= render PageFooterComponent.new do %>
+ <%= link_to t('two_factor_authentication.backup_codes.add_another_authentication_option'), second_mfa_setup_path %>
+<% end %>
+
diff --git a/config/locales/titles/en.yml b/config/locales/titles/en.yml
index 87d7f0db36e..3d2ca35ff32 100644
--- a/config/locales/titles/en.yml
+++ b/config/locales/titles/en.yml
@@ -5,6 +5,7 @@ en:
account_locked: Account temporarily locked
add_info:
phone: Add a phone number
+ backup_codes: Don’t lose your backup codes
confirmations:
delete: Please confirm
new: Resend confirmation instructions for your account
diff --git a/config/locales/titles/es.yml b/config/locales/titles/es.yml
index f56a3424207..d3fa8aff619 100644
--- a/config/locales/titles/es.yml
+++ b/config/locales/titles/es.yml
@@ -5,6 +5,7 @@ es:
account_locked: Cuenta bloqueada temporalmente
add_info:
phone: Agregar un número de teléfono
+ backup_codes: No pierda sus códigos de respaldo
confirmations:
delete: Por favor confirmar
new: Reenviar instrucciones de confirmación de su cuenta
diff --git a/config/locales/titles/fr.yml b/config/locales/titles/fr.yml
index edb0ced0138..b8251e03a23 100644
--- a/config/locales/titles/fr.yml
+++ b/config/locales/titles/fr.yml
@@ -5,6 +5,7 @@ fr:
account_locked: Compte temporairement verrouillé
add_info:
phone: Ajouter un numéro de téléphone
+ backup_codes: Ne perdez pas vos codes de sauvegarde
confirmations:
delete: Veuillez confirmer
new: Envoyer les instructions de confirmation pour votre compte
diff --git a/config/locales/two_factor_authentication/en.yml b/config/locales/two_factor_authentication/en.yml
index cefa57a0e6d..5ea93b0b5fd 100644
--- a/config/locales/two_factor_authentication/en.yml
+++ b/config/locales/two_factor_authentication/en.yml
@@ -19,6 +19,16 @@ en:
backup_code_header_text: Enter your backup security code
backup_code_prompt: You can use this security code once. After you enter it,
you’ll need to use a new key.
+ backup_codes:
+ add_another_authentication_option: '‹ Add another authentication option'
+ instructions: If you don’t have access to another device, keep your backup codes
+ safe. If you lose your backup codes, you won’t be able to sign into
+ %{app_name}.
+ new_backup_codes: I need new backup codes
+ saved_backup_codes: I’ve saved my backup codes
+ warning_html: You’ve only set up backup codes on your account.
+ If you have access to another device, such as a phone, protect
+ your account with another authentication method.
choose_another_option: '‹ Choose another option'
header_text: Enter your one-time code
important_alert_icon: important alert icon
diff --git a/config/locales/two_factor_authentication/es.yml b/config/locales/two_factor_authentication/es.yml
index 1edd7c183eb..f481229d053 100644
--- a/config/locales/two_factor_authentication/es.yml
+++ b/config/locales/two_factor_authentication/es.yml
@@ -19,6 +19,16 @@ es:
backup_code_header_text: Ingrese su código de seguridad de respaldo
backup_code_prompt: Puede utilizar este código de seguridad una vez. Después de
ingresarlo, deberá usar una nueva clave.
+ backup_codes:
+ add_another_authentication_option: '‹ Añada otra opción de autenticación'
+ instructions: Si no tiene acceso a otro dispositivo, guarde bien sus códigos de
+ respaldo. No podrá iniciar sesión en %{app_name} si pierde sus códigos
+ de respaldo.
+ new_backup_codes: Necesito nuevos códigos de respaldo
+ saved_backup_codes: Ya guardé mis códigos de respaldo
+ warning_html: Solo ha configurado códigos de respaldo en su cuenta.
+ Si tiene acceso a otro dispositivo, como un celular, proteja su
+ cuenta mediante otro método de autenticación.
choose_another_option: '‹ Elige otra opción'
header_text: Introduzca su código único
important_alert_icon: ícono de aviso importante
diff --git a/config/locales/two_factor_authentication/fr.yml b/config/locales/two_factor_authentication/fr.yml
index 41de2c0ecb1..c0ebdf77f14 100644
--- a/config/locales/two_factor_authentication/fr.yml
+++ b/config/locales/two_factor_authentication/fr.yml
@@ -20,6 +20,17 @@ fr:
backup_code_header_text: Entrez votre code de sécurité de secours
backup_code_prompt: Vous pouvez utiliser ce code de sécurité une fois. Après
l’avoir entré, vous devrez utiliser une nouvelle clé.
+ backup_codes:
+ add_another_authentication_option: '‹ Ajouter une autre option d’authentification'
+ instructions: Si vous n’avez pas accès à un autre appareil, conservez vos codes
+ de sauvegarde en lieu sûr. Si vous perdez vos codes de sauvegarde, vous
+ ne pourrez plus vous connecter à %{app_name}.
+ new_backup_codes: J’ai besoin de nouveaux codes de sauvegarde
+ saved_backup_codes: J’ai sauvegardé mes codes de sauvegarde
+ warning_html: Vous n’avez configuré que des codes de sauvegarde sur
+ votre compte. Si vous avez accès à un autre appareil, tel qu’un
+ téléphone, protégez votre compte à l’aide d’une autre méthode
+ d’authentification.
choose_another_option: '‹ Choisissez une autre option'
header_text: Entrez votre code à usage unique
important_alert_icon: Icône d’alerte importante
diff --git a/config/routes.rb b/config/routes.rb
index 102b15a28c5..fa422471050 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -273,6 +273,7 @@
get '/backup_code_delete' => 'users/backup_code_setup#confirm_delete'
get '/backup_code_create' => 'users/backup_code_setup#confirm_create'
delete '/backup_code_delete' => 'users/backup_code_setup#delete'
+ get '/confirm_backup_codes' => 'users/backup_code_setup#confirm_backup_codes'
get '/piv_cac_delete' => 'users/piv_cac_setup#confirm_delete'
get '/auth_app_delete' => 'users/totp_setup#confirm_delete'
diff --git a/spec/features/multi_factor_authentication/mfa_cta_spec.rb b/spec/features/multi_factor_authentication/mfa_cta_spec.rb
index 6d64285f75c..3dadce29904 100644
--- a/spec/features/multi_factor_authentication/mfa_cta_spec.rb
+++ b/spec/features/multi_factor_authentication/mfa_cta_spec.rb
@@ -8,15 +8,38 @@
it 'displays a banner after configuring a single MFA method' do
visit_idp_from_sp_with_ial1(:oidc)
user = sign_up_and_set_password
- select_2fa_option('backup_code')
+ select_2fa_option('phone')
click_continue
+ fill_in :new_phone_form_phone, with: '3015551212'
+ click_send_one_time_code
+
+ fill_in_code_with_last_phone_otp
+ click_submit_default
+
+ expect(page).to have_content(t('notices.phone_confirmed'))
+
click_button t('mfa.skip')
expect(page).to have_current_path(sign_up_completed_path)
expect(MfaPolicy.new(user).multiple_factors_enabled?).to eq false
expect(page).to have_content(t('mfa.second_method_warning.text'))
end
+ it 'displays a banner after confirming that backup codes are saved' do
+ visit_idp_from_sp_with_ial1(:oidc)
+ user = sign_up_and_set_password
+ select_2fa_option('backup_code')
+ click_continue
+
+ click_button t('mfa.skip')
+ expect(MfaPolicy.new(user).multiple_factors_enabled?).to eq false
+ expect(page).to have_current_path(confirm_backup_codes_path)
+
+ acknowledge_backup_code_confirmation
+
+ expect(page).to have_content(t('mfa.second_method_warning.text'))
+ end
+
it 'does not display a banner after configuring multiple MFA methods' do
visit_idp_from_sp_with_ial1(:oidc)
sign_up_and_set_password
@@ -41,6 +64,9 @@
set_up_mfa_with_backup_codes
click_button t('mfa.skip')
+
+ expect(page).to have_current_path(confirm_backup_codes_path)
+ acknowledge_backup_code_confirmation
click_link(t('mfa.second_method_warning.link'))
expect(page).to have_current_path(second_mfa_setup_path)
end
diff --git a/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb b/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb
index b1aa0e315ae..caa558dae38 100644
--- a/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb
+++ b/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb
@@ -58,18 +58,17 @@
end
end
- it 'directs backup code only users to the SP during sign up' do
+ it 'directs to SP after backup code confirmation' do
visit_idp_from_sp_with_ial1(:oidc)
sign_up_and_set_password
select_2fa_option('backup_code')
click_continue
skip_second_mfa_prompt
- expect(page).to have_current_path(sign_up_completed_path)
+ expect(page).to have_current_path(confirm_backup_codes_path)
+ acknowledge_backup_code_confirmation
- click_agree_and_continue
-
- expect(current_url).to start_with('http://localhost:7654/auth/result')
+ expect(current_path).to eq(sign_up_completed_path)
end
context 'when the user needs a backup code reminder' do
diff --git a/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb b/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb
index 2b36b87f275..37d9c080ecd 100644
--- a/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb
+++ b/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb
@@ -80,19 +80,20 @@
expect(current_path).to eq authentication_methods_setup_path
- click_2fa_option('backup_code')
+ click_2fa_option('phone')
click_continue
- expect(current_path).to eq backup_code_setup_path
+ expect(page).
+ to have_content t('titles.phone_setup')
click_continue
- expect(page).to have_link(t('components.download_button.label'))
-
- click_continue
+ fill_in 'new_phone_form_phone', with: '301-555-1212'
+ click_send_one_time_code
- expect(page).to have_content(t('notices.backup_codes_configured'))
+ fill_in_code_with_last_phone_otp
+ click_submit_default
expect(page).to have_current_path(
auth_method_confirmation_path,
@@ -136,6 +137,42 @@
end
end
+ context 'when backup codes are the only selected option' do
+ before do
+ sign_in_before_2fa
+
+ expect(current_path).to eq authentication_methods_setup_path
+
+ click_2fa_option('backup_code')
+
+ click_continue
+
+ expect(current_path).to eq backup_code_setup_path
+
+ click_continue
+
+ expect(page).to have_link(t('components.download_button.label'))
+
+ click_continue
+
+ expect(page).to have_current_path(
+ auth_method_confirmation_path,
+ )
+
+ click_button t('mfa.skip')
+ end
+
+ it 'goes to the next page after user confirms that they have saved their backup codes' do
+ acknowledge_backup_code_confirmation
+ expect(page).to have_current_path account_path
+ end
+
+ it 'regenerates backup codes path if a user clicks that they need new backup codes' do
+ click_link t('two_factor_authentication.backup_codes.new_backup_codes')
+ expect(page).to have_current_path backup_code_regenerate_path
+ end
+ end
+
def click_2fa_option(option)
find("label[for='two_factor_options_form_selection_#{option}']").click
end
diff --git a/spec/features/users/sign_up_spec.rb b/spec/features/users/sign_up_spec.rb
index 2b6e537eeff..77adaa9a0dc 100644
--- a/spec/features/users/sign_up_spec.rb
+++ b/spec/features/users/sign_up_spec.rb
@@ -197,7 +197,6 @@ def clipboard_text
set_up_2fa_with_backup_codes
skip_second_mfa_prompt
- expect(page).to have_current_path account_path
visit add_phone_path
expect(page).to have_current_path add_phone_path
end
@@ -386,4 +385,16 @@ def clipboard_text
select_2fa_option('piv_cac')
expect(page).to_not have_content(t('two_factor_authentication.piv_cac_fallback.question'))
end
+
+ it 'allows a user to sign up with backup codes and add methods after without reauthentication' do
+ sign_in_user
+ set_up_2fa_with_backup_codes
+ skip_second_mfa_prompt
+
+ acknowledge_backup_code_confirmation
+
+ expect(page).to have_current_path account_path
+ visit add_phone_path
+ expect(page).to have_current_path add_phone_path
+ end
end
diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb
index 9cc2ef5f4ec..ec00dbfeaaa 100644
--- a/spec/support/features/session_helper.rb
+++ b/spec/support/features/session_helper.rb
@@ -712,5 +712,9 @@ def expect_branded_experience
# Check for branded experience as being the header containing the Login.gov and partner logos
expect(page).to have_css(".page-header--basic img[alt='#{APP_NAME}'] ~ img")
end
+
+ def acknowledge_backup_code_confirmation
+ click_on t('two_factor_authentication.backup_codes.saved_backup_codes')
+ end
end
end