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